动画的基本步骤
我们可以通过一下的步骤来画出一帧:
- 清空 canvas
- 保存 canvas 状态
- 绘制动画图形
- 恢复 canvas 状态
操控动画
我们使用 canvas 提供的或自定义的方法来绘制内容,通常我们是在脚本执行结束后才能看到结果。因此,我们在 for 循环不太可能完成动画,我们只能看到动画的最后一帧。剪藏
为了实现动画,我们通常会使用 setInterval
和 setTimeout
方法来控制间隔多久绘制一次内容。
这个例子实现了一个动态时钟:
function clock(){
var now = new Date();
var ctx = document.getElementById('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";
// 绘制小时刻度
ctx.save();
for (var i=0;i<12;i++){
ctx.beginPath();
ctx.rotate(Math.PI/6);
ctx.moveTo(100,0);
ctx.lineTo(120,0);
ctx.stroke();
}
ctx.restore();
// 绘制分钟刻度
ctx.save();
ctx.lineWidth = 5;
for (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();
var sec = now.getSeconds();
var min = now.getMinutes();
var hr = now.getHours();
hr = hr>=12 ? hr-12 : hr;
ctx.fillStyle = "black";
// 绘制时针
ctx.save();
ctx.rotate( hr*(Math.PI/6) + (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();
// 绘制分针
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();
// 绘制秒针
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 = "rgba(0,0,0,0)";
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);
}
//告诉浏览器使用 clock 方法绘制动画
window.requestAnimationFrame(clock);
执行结果:
高级动画
添加更加符合物理的运动让动画看起来更高级
绘制小球
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var ball = {
x: 100,
y: 100,
radius: 25,
color: 'blue',
draw: function() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
}
};
ball.draw();
添加速率
function draw() {
ctx.clearRect(0,0, canvas.width, canvas.height);
ball.draw();
//改变下一次绘制的小球坐标
ball.x += ball.vx;
ball.y += ball.vy;
raf = window.requestAnimationFrame(draw);
}
边界
//添加边界
if (ball.y + ball.vy + ball.radius > canvas.height
|| ball.y + ball.vy - ball.radius < 0) {
ball.vy = -ball.vy;
}
if (ball.x + ball.vx + ball.radius > canvas.width
|| ball.x + ball.vx - ball.radius < 0) {
ball.vx = -ball.vx;
}
完整代码
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let raf;
const ball = {
x: 100,
y: 100,
vx: 5,
vy: 2,
radius: 25,
color: "blue",
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
},
};
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ball.draw();
ball.x += ball.vx;
ball.y += ball.vy;
ball.vy *= 0.99;
ball.vy += 0.25;
if (ball.y + ball.vy + ball.radius > canvas.height
|| ball.y + ball.vy - ball.radius < 0) {
ball.vy = -ball.vy;
}
if (ball.x + ball.vx + ball.radius > canvas.width
|| ball.x + ball.vx - ball.radius < 0) {
ball.vx = -ball.vx;
}
raf = window.requestAnimationFrame(draw);
}
canvas.addEventListener("mouseover", (e) => {
raf = window.requestAnimationFrame(draw);
});
canvas.addEventListener("mouseout", (e) => {
window.cancelAnimationFrame(raf);
});
ball.draw();
像素操作
ImageData 对象
ImageData
对象中存储这 canvas 对象真实的像素数据,包含以下几个只读属性
-
width
:图片宽度,单位是像素 -
height
:图片高度,单位是像素 -
data
:Unit8ClampedArray
类型的一维数组,包含 RGBA 格式的整型数据,范围在 0 到 255 之间
data
属性返回的数组,它存储了初始像素数据。每个像素使用 4 个 1 bytes 的值(分别是红、绿、蓝和透明)来表示。每个部分的值用 0 到 255 表示。数据第 0 项到第 3 项存图片左上角第一个像素点的 RGBA 数据。然后依次从左往右存每一行的像素数据。
创建 ImageData 对象
我们使用 createImageData
方法创建 ImageData
对象,它有两个重载
-
传入具体的宽高创建对象,所有像素被预设为透明黑
var myImageData = ctx.createImageData(width, height);
-
传入另一个
ImageData
对象,同样所有像素被与设定为透明黑。这个重载并非复制图片数据var myImageData = ctx.createImageData(anotherImageData);
获取像素数据
我们可以使用 getImageData()
方法获取一个矩形区域的 ImageData
对象:
var myImageData = ctx.getImageData(left, top, width, height);
(left
,top
)是矩形左上角的坐标
如果我们选取的区域在画布之外,那么会返回一个透明黑色的
ImageData
对象
写入像素数据
我们可以使用 putImageData()
方法将像素数据写入到画布中:
ctx.putImageData(myImageData, dx, dy);
dx
和 dy
参数表示写入的画布坐标
缩放和反锯齿
我们使用drawImage方法缩放图片,imageSmoothingEnabled
属性控制是否开启反锯齿(默认开启)。如果我们关闭反锯齿,将图形放大时,图片会出现锯齿(图 1)。开启反锯齿,图像会平滑缩放(图 2)
保存图片
我们可以使用以下方法保存图片:
canvas.toDataURL(type, encoderOptions);
-
type
(可选):图片格式,默认为image/png
-
encoderOptions
(可选):在指定图片格式为image/jpeg
或image/webp
的情况下,可以从 0 到 1 的区间内选择图片的质量。
它返回一个包含图片展示的 Data URL。
参考链接
基本的动画 - Web API 接口参考 | MDN (mozilla.org)