Класс Ball
Это руководство объясняет использование классов JavaScript в анимациях canvas и не предназначено для начинающих. Рекомендуется сначала пройти наш раздел «Движок освещения» и попробовать руководство «Цветовой цикл».
При этом здесь не используется строгий синтаксис классов — это просто «синтаксический сахар» JavaScript, а лежащие в основе концепции остаются теми же. Для изучения более сложных анимаций рекомендуем ознакомиться с дополнительными материалами. В остальном будем придерживаться основ.
Класс — это обычная функция JavaScript. Она работает одинаково, но добавляет возможность привязки значений к конкретному экземпляру функции. В этом примере создаётся мяч, ведущий себя в соответствии с базовой физикой. Он отслеживает своё положение и скорость, а также «гравитацию», действующую на него.
Простой отскок
Заголовок раздела «Простой отскок»<head> <title>Simple Ball Bounce</title> <meta description="Ball bounce"/> <meta publisher="WhirlwindFX" /></head>
<body style="margin: 0; padding: 0;"> <canvas id="exCanvas" width="320" height="200"></canvas></body>
<script> // Get the canvas element from the DOM var c = document.getElementById("exCanvas"); var ctx = c.getContext("2d"); // Canvas variables var width = 320; var height = 200; // X-component of ball position let ballX = 160; // Y-component of ball position let ballY = 100; // Change in ballY over time. Positive values move down, negative move up let velocityY = 0; // Change in velocityY over time. let accelerationY = 1; // Effects array let effects = [];
function update() { // 1. Overwrite the previous frame ctx.fillStyle = background; ctx.fillRect(0, 0, 320,200);
// 2. Make sure there is one instance of the Ball class in the effects array if (effects.length < 1){ effects.push(new Ball()); }
// 3. Iterate through the effects array and call each element's draw function effects.forEach(ele => { ele.draw(); })
window.requestAnimationFrame(update); }
// 4. Declare the Ball class function Ball(){ // 5. Set instance variables this.bx = ballX; this.by = ballY; this.vy = velocityY; this.ay = accelerationY; this.radius = 15; this.impact = false; // 6. Define draw function this.draw = function(){ // 7. Change the y-velocity by the y-acceleration this.vy += this.ay;
// 8. If an impact with the ground has been detected, set the velocity upwards and set the impact boolean to false. This check happens before the next function so that we draw the impact frame instead of skipping it. if (this.impact){ this.vy = -10; this.impact = false; }
// 9. Detect a future impact. If the ball position plus its radius plus its velocity would put it under the floor, we run a few operations - if (this.by + this.radius + this.vy > 200){ // Set the velocity to 0 so the ball stays where we put it this frame. this.vy = 0; // Set impact to true so the next run-through draws the up-bounce this.impact = true; // Next frame draws the up-bounce. This frame detected a future collision. If we want to see the impact itself we have to force it. While the ball's outer radius is below the floor, we move the ball up. while (this.by + this.radius < 200){ this.by++; } } // 10. Change the y-component by the y-velocity. this.by += this.vy; // Draw shape. ctx.beginPath(); ctx.arc(this.bx, this.by, this.radius, 0, Math.PI * 2); ctx.fillStyle = "black" ctx.fill(); } }
window.requestAnimationFrame(update);
</script><head> <title>Simple Ball Bounce</title> <meta description="Ball bounce"/> <meta publisher="WhirlwindFX" /></head>
<body style="margin: 0; padding: 0;"> <canvas id="exCanvas" width="320" height="200"></canvas></body><script> // Get the canvas element from the DOM var c = document.getElementById("exCanvas"); var ctx = c.getContext("2d"); // Canvas variables var width = 320; var height = 200; // X-component of ball position let ballX = 160; // Y-component of ball position let ballY = 100; // Change in ballY over time. Positive values move down, negative move up let velocityY = 0; // Change in velocityY over time. let accelerationY = 1; // Effects array let effects = [];
function update() { // 1. Overwrite the previous frame ctx.fillStyle = background; ctx.fillRect(0, 0, 320,200);
// 2. Make sure there is one instance of the Ball class in the effects array if (effects.length < 1){ effects.push(new Ball()); }
// 3. Iterate through the effects array and call each element's draw function effects.forEach(ele => { ele.draw(); })
window.requestAnimationFrame(update); }
// 4. Declare the Ball class function Ball(){ // 5. Set instance variables this.bx = ballX; this.by = ballY; this.vy = velocityY; this.ay = accelerationY; this.radius = 15; this.impact = false; // 6. Define draw function this.draw = function(){ // 7. Change the y-velocity by the y-acceleration this.vy += this.ay;
// 8. If an impact with the ground has been detected, set the velocity upwards and set the impact boolean to false. This check happens before the next function so that we draw the impact frame instead of skipping it. if (this.impact){ this.vy = -10; this.impact = false; }
// 9. Detect a future impact. If the ball position plus its radius plus its velocity would put it under the floor, we run a few operations - if (this.by + this.radius + this.vy > 200){ // Set the velocity to 0 so the ball stays where we put it this frame. this.vy = 0; // Set impact to true so the next run-through draws the up-bounce this.impact = true; // Next frame draws the up-bounce. This frame detected a future collision. If we want to see the impact itself we have to force it. While the ball's outer radius is below the floor, we move the ball up. while (this.by + this.radius < 200){ this.by++; } } // 10. Change the y-component by the y-velocity. this.by += this.vy; // Draw shape. ctx.beginPath(); ctx.arc(this.bx, this.by, this.radius, 0, Math.PI * 2); ctx.fillStyle = "black" ctx.fill(); } }
window.requestAnimationFrame(update);
</script>function update() { // 1. Overwrite the previous frame ctx.fillStyle = background; ctx.fillRect(0, 0, 320,200);
// 2. Make sure there is one instance of the Ball class in the effects array if (effects.length < 1){ effects.push(new Ball()); }
// 3. Iterate through the effects array and call each element's draw function effects.forEach(ele => { ele.draw(); })
window.requestAnimationFrame(update); }// 4. Declare the Ball class function Ball(){ // 5. Set instance variables this.bx = ballX; this.by = ballY; this.vy = velocityY; this.ay = accelerationY; this.radius = 15; this.impact = false; // 6. Define draw function this.draw = function(){ // 7. Change the y-velocity by the y-acceleration this.vy += this.ay;
// 8. If an impact with the ground has been detected, set the velocity upwards and set the impact boolean to false. This check happens before the next function so that we draw the impact frame instead of skipping it. if (this.impact){ this.vy = -10; this.impact = false; }
// 9. Detect a future impact. If the ball position plus its radius plus its velocity would put it under the floor, we run a few operations - if (this.by + this.radius + this.vy > 200){ // Set the velocity to 0 so the ball stays where we put it this frame. this.vy = 0; // Set impact to true so the next run-through draws the up-bounce this.impact = true; // Next frame draws the up-bounce. This frame detected a future collision. If we want to see the impact itself we have to force it. While the ball's outer radius is below the floor, we move the ball up. while (this.by + this.radius < 200){ this.by++; } } // 10. Change the y-component by the y-velocity. this.by += this.vy; // Draw shape. ctx.beginPath(); ctx.arc(this.bx, this.by, this.radius, 0, Math.PI * 2); ctx.fillStyle = "black" ctx.fill(); } }
Полный отскок
Заголовок раздела «Полный отскок»После освоения начальной сложности добавить более продвинутое поведение проще. В следующем примере добавлены столкновения со стенами и случайные вариации по оси X для поддержания движения.
<head> <title>Full Bounce</title> <meta description="Snake-like climbing gradient effect"/> <meta publisher="WhirlwindFX" /></head>
<body style="margin: 0; padding: 0;"> <canvas id="exCanvas" width="320" height="200"></canvas></body>
<script> // Get the canvas element from the DOM var c = document.getElementById("exCanvas"); var ctx = c.getContext("2d"); var width = 320; var height = 200; var hue = 0; let ballX = 160; // 1. Added velocityX let velocityX = 0; let ballY = 100; let velocityY = 0; let accelerationY = 1; let effects = [];
function update() {
ctx.fillStyle = "white"; ctx.fillRect(0, 0, 320,200);
if (effects.length < 1){ effects.push(new Ball()); }
effects.forEach(ele => { ele.draw(); })
window.requestAnimationFrame(update); }
function Ball(){ this.bx = ballX; // 2. Give x a random initial velocity this.vx = 100 * Math.sin(Date.now()); this.by = ballY; this.vy = velocityY; this.ay = accelerationY; this.radius = 15; // 3. Add x and y-components to the impact boolean for horizontal contact this.impactX = false; this.impactY = false;
this.draw = function(){ this.vy += this.ay;
// 4. Each frame, add a small amount of random velocity in the x-direction to keep momentum going this.vx += Math.sin(Date.now());
if (this.impactY){ this.vy = -10; this.impactY = false; } // 5. If a horizontal impact is detected, reverse the x-velocity and subtract a small amount of force to prevent an out-of-control ball. Mirroring or adding to the force here can have interesting consequences. if (this.impactX){ this.vx *= -.9; this.impactX = false; }
if (this.by + this.radius + this.vy > height){ this.vy = 0; this.impactY = true; while (this.by + this.radius < height){ this.by++; } } // 6. Detect a horizontal impact left or right and adjust like we did the y-component. if (this.bx + this.radius + this.vx > width){ this.vx = 0; this.impactX = true; while (this.bx + this.radius > width){ this.bx--; } } else if (this.bx - this.radius + this.vx < 0){ this.vx = 0; this.impactX = true; while (this.bx - this.radius < 0){ this.bx++; } } // 7. Change the x- and y-components by their velocity. this.by += this.vy; this.bx += this.vx;
ctx.beginPath(); ctx.arc(this.bx, this.by, this.radius, 0, Math.PI * 2); ctx.fillStyle = "black" ctx.fill(); } }
window.requestAnimationFrame(update);
</script>
Несколько мячей
Заголовок раздела «Несколько мячей»Честно говоря, этот пример Сложный с большой буквы «С» и, вероятно, требует больше усилий, чем нужно для большинства базовых эффектов. Рассмотрим несколько сложных концепций.
Как мяч узнаёт, что столкнётся с другим? Каждый кадр каждый экземпляр класса мяча должен проверять положение каждого другого мяча. Сравнивая их позиции, можно определить, находятся ли два мяча в пределах радиуса друг друга, и затем инвертировать их скорости X и Y.
Иногда мяч может набрать достаточно скорости, чтобы пройти сквозь другой. Математика для корректировки позиций обоих мячей была бы слишком сложной, поэтому проверяемый экземпляр мяча просто удаляется. Поскольку функция update гарантирует минимальное количество мячей, создаётся новый экземпляр и симуляция продолжается.
Наконец, когда рассчитывать изменение скорости и позиции для каждого мяча? Здесь возможны несколько подходов, и оба могут работать.
<head> <title>Multiple Balls</title> <meta description="Snake-like climbing gradient effect"/> <meta publisher="WhirlwindFX" /></head>
<body style="margin: 0; padding: 0;"> <canvas id="exCanvas" width="320" height="200"></canvas></body>
<script> // Get the canvas element from the DOM var c = document.getElementById("exCanvas"); var ctx = c.getContext("2d"); var width = 320; var height = 200; var hue = 0; let ballX = 160; let velocityX = 0; let ballY = 100; let velocityY = 0; let accelerationY = 1; let effects = [];
function update() {
ctx.fillStyle = "white"; ctx.fillRect(0, 0, 320,200);
// 1. Increase the number of balls present in the effects array if (effects.length < 3){ effects.push(new Ball()); }
effects.forEach(ele => { ele.draw(); })
window.requestAnimationFrame(update); }
function Ball(){ this.bx = ballX; this.vx = 100 * Math.sin(Date.now()); this.by = ballY; this.vy = velocityY; this.ay = accelerationY; this.radius = 15; // 2. Add a unique ID to each ball so we can compare them to each other and not themselves. We have also removed "impactX" and "impactY" in favor of a better solution for more balls. this.id = Math.random();
this.draw = function(){ this.vy += this.ay; this.vx += Math.sin(Date.now());
if (this.by + this.radius + this.vy > height){ this.vy = -10; } else if (this.by - this.radius + this.vy < 0){ // 3. Add a "ceiling" to the canvas in case a ball collision causes the height to exceed 0y. this.vy = 1; } if (this.bx + this.radius + this.vx > width){ this.vx = -.9; } else if (this.bx - this.radius + this.vx < 0){ this.vx = -.9; } // 4. Iterate through each ball in the effects array to check for collisions for (let i = 0; i < effects.length; i++){ let ele = effects[i]; // If the ball we are checking against is this ball, skip this cycle if (this.id == ele.id){ continue; } // If the balls are clipping too much, remove this ball from the canvas if (Math.abs(this.bx - ele.bx) < this.radius && Math.abs(this.by - ele.by) < this.radius){ effects.splice(i, 1); } // If the balls will collide, reverse their velocities and subtract some force to account for elasticity if (Math.abs(this.bx + this.vx - ele.bx) < (this.radius * 2) && Math.abs(this.by + this.vy - ele.by) < (this.radius * 2)){ this.vx *= -.9; this.vy *= -.9; } }
this.by += this.vy; this.bx += this.vx;
// 5. Because complicated calcualtions for the velocities happen before this point, finalize the x- and y-coordinates by making sure they are bounded within the canvas. while (this.bx - this.radius < 0){ this.bx++ } while (this.bx + this.radius > width){ this.bx-- } while (this.by - this.radius < 0){ this.by++ } while (this.by + this.radius > height){ this.by-- }
ctx.beginPath(); ctx.arc(this.bx, this.by, this.radius, 0, Math.PI * 2); ctx.fillStyle = "black" ctx.fill(); } }
window.requestAnimationFrame(update);
</script>
Пользовательские элементы управления
Заголовок раздела «Пользовательские элементы управления»Наконец, добавим несколько базовых пользовательских элементов управления. Поскольку изменения оттенка уже подробно рассматривались, для сохранения простоты сосредоточимся на регулировке количества и радиуса мячей. Для этого требуется лишь несколько изменений в существующих функциях.
<head> <title>User Controls</title> <meta description="Snake-like climbing gradient effect"/> <meta publisher="WhirlwindFX" /> <!-- Add two numerical sliders: radius and amount ---> <meta property="radius" label="Size" type="number" min="5" max="100" default="20"> <meta property="amount" label="Amount" type="number" min="1" max="50" default="2"></head>function update() {
ctx.fillStyle = "white"; ctx.fillRect(0, 0, 320,200);
// Set the minimum effects to be the user-controlled amount. If the length of effects is greater than amount, pop an element off the end. if (effects.length < amount){ effects.push(new Ball()); } else if (effects.length > amount){ effects.pop(); }
effects.forEach(ele => { ele.draw(); })
window.requestAnimationFrame(update); }this.draw = function(){ // Finally, set the radius to be the user-controlled radius inside of the draw function each frame. this.radius = radius; this.vy += this.ay; this.vx += Math.sin(Date.now());
// . . . rest of function . . .}
Надеемся, вам понравилось это руководство по анимации классов JavaScript! Даже при такой сложности этот эффект — лишь верхушка айсберга возможностей HTML canvas. Кроме того, его нельзя считать окончательным. Стремясь к совершенству, можно заметить, что столкновения мячей обнаруживаются за один кадр до их наступления, поэтому они часто не происходят в действительности. Столкновения также упрощены в плане передачи энергии. Наконец, проверка позиции каждого мяча несколько раз за цикл неэффективна с точки зрения временной сложности и может быть оптимизирована с помощью мемоизации.
В конечном счёте всегда критически оценивайте свои эффекты. Их обычно можно улучшить, а разница между посредственным и отличным эффектом часто заключается лишь в том, чтобы просто посмотреть на него и позволить себе не быть им довольным хотя бы раз.
Для финального пошагового руководства ознакомьтесь с разделом Обратные вызовы, где подробно рассматривается процесс захвата данных счётчиков для активации и деактивации эффектов.