基本動畫
由於我們使用 JavaScript 來控制 <canvas> 元素,因此建立(互動式)動畫也變得非常容易。在本章中,我們將探討如何製作一些基本動畫。
最大的限制可能是,一旦繪製了圖形,它就會一直保持該狀態。如果我們需要移動它,則必須重繪它以及之前繪製的所有內容。重繪複雜的幀需要大量時間,並且效能高度依賴於其執行計算機的速度。
基本動畫步驟
繪製一幀所需的步驟如下
- 清除畫布 除非您要繪製的圖形填滿了整個畫布(例如背景影像),否則您需要清除先前繪製的所有圖形。最簡單的方法是使用
clearRect()方法。 - 儲存畫布狀態 如果您要更改影響畫布狀態的任何設定(例如樣式、變換等),並且希望確保每次繪製幀時都使用原始狀態,則需要儲存該原始狀態。
- 繪製動畫圖形 在此步驟中進行實際的幀渲染。
- 恢復畫布狀態 如果已儲存狀態,請在繪製新幀之前恢復它。
控制動畫
圖形透過直接使用畫布方法或呼叫自定義函式繪製到畫布上。在正常情況下,只有在指令碼執行完成後,我們才能在畫布上看到這些結果。例如,無法在 for 迴圈內完成動畫。
這意味著我們需要一種在一段時間內執行繪圖函式的方法。有兩種方法可以控制此類動畫。
計劃更新
首先是 setInterval()、setTimeout() 和 requestAnimationFrame() 函式,它們可用於在設定的時間段內呼叫特定函式。
setInterval()-
每
delay毫秒重複執行function指定的函式。 setTimeout()-
在
delay毫秒後執行function指定的函式。 requestAnimationFrame()-
告知瀏覽器您希望執行動畫,並請求瀏覽器在下一次重繪之前呼叫指定函式來更新動畫。
如果您不希望有任何使用者互動,可以使用 setInterval() 函式,該函式會重複執行提供的程式碼。如果我們想製作遊戲,可以使用鍵盤或滑鼠事件來控制動畫並使用 setTimeout()。透過使用 addEventListener() 設定監聽器,我們可以捕獲任何使用者互動並執行我們的動畫函式。
注意: 在下面的示例中,我們將使用 Window.requestAnimationFrame() 方法來控制動畫。requestAnimationFrame 方法透過在系統準備好繪製幀時呼叫動畫幀,為動畫提供了更平滑、更高效的方式。回撥次數通常為每秒 60 次,當在後臺標籤頁執行時,次數可能會降低。有關動畫迴圈的更多資訊,尤其是在遊戲方面,請參閱我們 遊戲開發區 中的文章 影片遊戲 Anatomy。
一個動畫太陽系
此示例演示了一個小型太陽系模型的動畫。
HTML
<canvas id="canvas" width="300" height="300"></canvas>
JavaScript
const sun = new Image();
const moon = new Image();
const earth = new Image();
const ctx = document.getElementById("canvas").getContext("2d");
function init() {
sun.src = "canvas_sun.png";
moon.src = "canvas_moon.png";
earth.src = "canvas_earth.png";
window.requestAnimationFrame(draw);
}
function draw() {
ctx.globalCompositeOperation = "destination-over";
ctx.clearRect(0, 0, 300, 300); // clear canvas
ctx.fillStyle = "rgb(0 0 0 / 40%)";
ctx.strokeStyle = "rgb(0 153 255 / 40%)";
ctx.save();
ctx.translate(150, 150);
// Earth
const time = new Date();
ctx.rotate(
((2 * Math.PI) / 60) * time.getSeconds() +
((2 * Math.PI) / 60000) * time.getMilliseconds(),
);
ctx.translate(105, 0);
ctx.fillRect(0, -12, 40, 24); // Shadow
ctx.drawImage(earth, -12, -12);
// Moon
ctx.save();
ctx.rotate(
((2 * Math.PI) / 6) * time.getSeconds() +
((2 * Math.PI) / 6000) * time.getMilliseconds(),
);
ctx.translate(0, 28.5);
ctx.drawImage(moon, -3.5, -3.5);
ctx.restore();
ctx.restore();
ctx.beginPath();
ctx.arc(150, 150, 105, 0, Math.PI * 2, false); // Earth orbit
ctx.stroke();
ctx.drawImage(sun, 0, 0, 300, 300);
window.requestAnimationFrame(draw);
}
init();
結果
一個動畫時鐘
此示例繪製了一個動畫時鐘,顯示您當前的時間。
HTML
<canvas id="canvas" width="150" height="150">The current time</canvas>
JavaScript
function clock() {
const now = new Date();
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.save();
ctx.clearRect(0, 0, 150, 150);
ctx.translate(75, 75);
ctx.scale(0.4, 0.4);
ctx.rotate(-Math.PI / 2);
ctx.strokeStyle = "black";
ctx.fillStyle = "white";
ctx.lineWidth = 8;
ctx.lineCap = "round";
// Hour marks
ctx.save();
for (let i = 0; i < 12; i++) {
ctx.beginPath();
ctx.rotate(Math.PI / 6);
ctx.moveTo(100, 0);
ctx.lineTo(120, 0);
ctx.stroke();
}
ctx.restore();
// Minute marks
ctx.save();
ctx.lineWidth = 5;
for (let i = 0; i < 60; i++) {
if (i % 5 !== 0) {
ctx.beginPath();
ctx.moveTo(117, 0);
ctx.lineTo(120, 0);
ctx.stroke();
}
ctx.rotate(Math.PI / 30);
}
ctx.restore();
const sec = now.getSeconds();
// To display a clock with a sweeping second hand, use:
// const sec = now.getSeconds() + now.getMilliseconds() / 1000;
const min = now.getMinutes();
const hr = now.getHours() % 12;
ctx.fillStyle = "black";
// Write image description
canvas.innerText = `The time is: ${hr}:${min}`;
// Write Hours
ctx.save();
ctx.rotate(
(Math.PI / 6) * hr + (Math.PI / 360) * min + (Math.PI / 21600) * sec,
);
ctx.lineWidth = 14;
ctx.beginPath();
ctx.moveTo(-20, 0);
ctx.lineTo(80, 0);
ctx.stroke();
ctx.restore();
// Write Minutes
ctx.save();
ctx.rotate((Math.PI / 30) * min + (Math.PI / 1800) * sec);
ctx.lineWidth = 10;
ctx.beginPath();
ctx.moveTo(-28, 0);
ctx.lineTo(112, 0);
ctx.stroke();
ctx.restore();
// Write seconds
ctx.save();
ctx.rotate((sec * Math.PI) / 30);
ctx.strokeStyle = "#D40000";
ctx.fillStyle = "#D40000";
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(-30, 0);
ctx.lineTo(83, 0);
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, 10, 0, Math.PI * 2, true);
ctx.fill();
ctx.beginPath();
ctx.arc(95, 0, 10, 0, Math.PI * 2, true);
ctx.stroke();
ctx.fillStyle = "transparent";
ctx.arc(0, 0, 3, 0, Math.PI * 2, true);
ctx.fill();
ctx.restore();
ctx.beginPath();
ctx.lineWidth = 14;
ctx.strokeStyle = "#325FA2";
ctx.arc(0, 0, 142, 0, Math.PI * 2, true);
ctx.stroke();
ctx.restore();
window.requestAnimationFrame(clock);
}
window.requestAnimationFrame(clock);
結果
注意: 儘管時鐘每秒僅更新一次,但動畫影像以每秒 60 幀(或您網頁瀏覽器的顯示重新整理率)進行更新。要顯示帶有掃動秒針的時鐘,請替換上面 const sec 的定義,使用已註釋掉的版本。
一個迴圈全景圖
在此示例中,全景圖從左向右滾動。我們使用的是從維基百科上找到的 優勝美地國家公園的圖片,但您可以使用任何比畫布大的圖片。
HTML
HTML 包含用於滾動影像的 <canvas>。請注意,此處指定的寬度和高度必須與 JavaScript 程式碼中的 canvasXSize 和 canvasYSize 變數的值匹配。
<canvas id="canvas" width="800" height="200"
>Yosemite National Park, meadow at the base of El Capitan</canvas
>
JavaScript
const img = new Image();
// User Variables - customize these to change the image being scrolled, its
// direction, and the speed.
img.src = "capitan_meadows_yosemite_national_park.jpg";
const canvasXSize = 800;
const canvasYSize = 200;
const speed = 30; // lower is faster
const scale = 1.05;
const y = -4.5; // vertical offset
// Main program
const dx = 0.75;
let imgW;
let imgH;
let x = 0;
let clearX;
let clearY;
let ctx;
img.onload = () => {
imgW = img.width * scale;
imgH = img.height * scale;
if (imgW > canvasXSize) {
// Image larger than canvas
x = canvasXSize - imgW;
}
// Check if image dimension is larger than canvas
clearX = Math.max(imgW, canvasXSize);
clearY = Math.max(imgH, canvasYSize);
// Get canvas context
ctx = document.getElementById("canvas").getContext("2d");
// Set refresh rate
return setInterval(draw, speed);
};
function draw() {
ctx.clearRect(0, 0, clearX, clearY); // clear the canvas
// If image is <= canvas size
if (imgW <= canvasXSize) {
// Reset, start from beginning
if (x > canvasXSize) {
x = -imgW + x;
}
// Draw additional image1
if (x > 0) {
ctx.drawImage(img, -imgW + x, y, imgW, imgH);
}
// Draw additional image2
if (x - imgW > 0) {
ctx.drawImage(img, -imgW * 2 + x, y, imgW, imgH);
}
} else {
// Image is > canvas size
// Reset, start from beginning
if (x > canvasXSize) {
x = canvasXSize - imgW;
}
// Draw additional image
if (x > canvasXSize - imgW) {
ctx.drawImage(img, x - imgW + 1, y, imgW, imgH);
}
}
// Draw image
ctx.drawImage(img, x, y, imgW, imgH);
// Amount to move
x += dx;
}
結果
滑鼠跟隨動畫
HTML
<canvas id="cw"
>Animation creating multi-colored disappearing stream of light that follow the
cursor as it moves over the image
</canvas>
CSS
#cw {
position: fixed;
z-index: -1;
}
body {
margin: 0;
padding: 0;
background-color: rgb(0 0 0 / 5%);
}
JavaScript
const canvas = document.getElementById("cw");
const context = canvas.getContext("2d");
context.globalAlpha = 0.5;
const cursor = {
x: innerWidth / 2,
y: innerHeight / 2,
};
let particlesArray = [];
generateParticles(101);
setSize();
anim();
addEventListener("mousemove", (e) => {
cursor.x = e.clientX;
cursor.y = e.clientY;
});
addEventListener(
"touchmove",
(e) => {
e.preventDefault();
cursor.x = e.touches[0].clientX;
cursor.y = e.touches[0].clientY;
},
{ passive: false },
);
addEventListener("resize", () => setSize());
function generateParticles(amount) {
for (let i = 0; i < amount; i++) {
particlesArray[i] = new Particle(
innerWidth / 2,
innerHeight / 2,
4,
generateColor(),
0.02,
);
}
}
function generateColor() {
let hexSet = "0123456789ABCDEF";
let finalHexString = "#";
for (let i = 0; i < 6; i++) {
finalHexString += hexSet[Math.ceil(Math.random() * 15)];
}
return finalHexString;
}
function setSize() {
canvas.height = innerHeight;
canvas.width = innerWidth;
}
function Particle(x, y, particleTrailWidth, strokeColor, rotateSpeed) {
this.x = x;
this.y = y;
this.particleTrailWidth = particleTrailWidth;
this.strokeColor = strokeColor;
this.theta = Math.random() * Math.PI * 2;
this.rotateSpeed = rotateSpeed;
this.t = Math.random() * 150;
this.rotate = () => {
const ls = {
x: this.x,
y: this.y,
};
this.theta += this.rotateSpeed;
this.x = cursor.x + Math.cos(this.theta) * this.t;
this.y = cursor.y + Math.sin(this.theta) * this.t;
context.beginPath();
context.lineWidth = this.particleTrailWidth;
context.strokeStyle = this.strokeColor;
context.moveTo(ls.x, ls.y);
context.lineTo(this.x, this.y);
context.stroke();
};
}
function anim() {
requestAnimationFrame(anim);
context.fillStyle = "rgb(0 0 0 / 5%)";
context.fillRect(0, 0, canvas.width, canvas.height);
particlesArray.forEach((particle) => particle.rotate());
}
結果
其他示例
- 高階動畫
-
我們將在下一章探討一些高階動畫技術和物理學。