볼 클래스
이 튜토리얼에서는 캔버스 애니메이션에서 JavaScript 클래스를 사용하는 방법을 설명하며, 초보자를 위한 내용이 아닙니다. 먼저 조명 엔진 섹션을 학습하고 색상 순환 튜토리얼을 시도해 보는 것을 권장합니다.
그렇지만 여기서는 엄격한 클래스 구문을 사용하지 않습니다. 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. if (this.impact){ this.vy = -10; this.impact = false; }
// 9. Detect a future impact. if (this.by + this.radius + this.vy > 200){ this.vy = 0; this.impact = true; 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>
전체 바운스
섹션 제목: “전체 바운스”초기 복잡성을 해결하고 나면 더 고급 동작을 추가하기가 더 쉬워집니다. 다음 예시에서는 벽 충돌을 통합하고 볼이 계속 움직이도록 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> 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 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 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 속도를 반대로 바꿉니다.
때때로 볼이 충분한 속도를 얻어 다른 볼을 통과하여 거의 같은 공간을 차지하게 될 수 있습니다. 두 볼 위치를 조정하는 데 필요한 수학이 너무 복잡하므로 현재 확인 중인 볼 인스턴스를 단순히 제거합니다. 업데이트 함수가 최소 볼 수를 보장하므로 새 볼 인스턴스가 만들어지고 시뮬레이션이 계속됩니다.
<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> 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 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 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 (this.id == ele.id){ continue; } if (Math.abs(this.bx - ele.bx) < this.radius && Math.abs(this.by - ele.by) < this.radius){ effects.splice(i, 1); } 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. Finalize the x- and y-coordinates 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);
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(){ // 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 캔버스로 달성할 수 있는 것의 빙산의 일각에 불과합니다. 또한 아직 완성되지 않았다고 생각합니다. 완벽함을 추구한다면 볼 충돌이 실제로 발생하기 한 프레임 전에 감지되어 실제로 충돌하지 않는 경우가 많다는 것을 알 수 있습니다. 충돌은 에너지 전달 측면에서도 단순화되어 있어 결과적인 속도는 볼의 현재 속도만 고려하고 다른 볼의 에너지는 고려하지 않습니다. 마지막으로 루프당 각 볼의 위치를 여러 번 확인하는 것은 시간 복잡도 측면에서 비효율적이며 기본 메모이제이션으로 최적화할 수 있습니다.
결국 항상 효과에 비판적이 되십시오. 일반적으로 어떤 방식으로든 개선될 수 있으며, 평범한 효과와 훌륭한 효과의 차이는 종종 단순히 바라보며 적어도 한 번은 마음에 들지 않는 것을 허용하는 것에서 비롯됩니다.
마지막 단계별 튜토리얼은 미터 데이터를 캡처하여 효과를 활성화하고 비활성화하는 과정을 안내하는 콜백 섹션을 확인하십시오.