【Canvas】基础
文章目录
- 1. Canvas 基础
- 1.1 Canvas 简介
- 1.2 坐标系
- 2. 绘制图形
- 2.1 绘制矩形
- 2.2 绘制路径
- 2.3 圆形和弧形
- 2.4 贝塞尔曲线
- 3. 样式和颜色
- 3.1 填充和描边
- 3.2 透明度
- 3.3 线型
- 3.4 渐变
- 3.5 图案样式
- 3.6 阴影
- 4. 文本
- 4.1 绘制文本
- 4.2 文本样式
- 4.3 预测量文本宽度
- 5. 图像
- 5.1 绘制图像
- 5.2 像素操作
- 6. 状态的保存和恢复
- 7. 变换
- 7.1 基本变换
- 6.2 矩阵变换
- 8. 裁切路径
- 9. 动画
- 9.1 基本动画
1. Canvas 基础
参考文档:
- MDN 文档
- w3schools
1.1 Canvas 简介
- Canvas 是什么:Canvas 是 HTML5 提供的一个元素,用于通过 JavaScript 绘制图形、动画和图像。
- 使用场景:游戏开发、数据可视化、图像处理、交互式 UI 等。
- 基本用法:
<canvas id="canvas" width="200" height="200"></canvas> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // 获取 2D 渲染上下文 ctx.fillStyle = "green"; ctx.fillRect(10, 10, 150, 100); </script>
1.2 坐标系
- Canvas 使用二维坐标系,原点 (0, 0) 在左上角,x 轴向右,y 轴向下。
2. 绘制图形
2.1 绘制矩形
-
绘制矩形:
fillRect(x, y, width, height)
:绘制一个填充的矩形strokeRect(x, y, width, height)
:绘制一个矩形的边框clearRect(x, y, width, height)
:清除指定矩形区域,让清除部分完全透明。
-
示例:
const canvas = document.getElementById("myCanvas"); if (canvas.getContext) { const ctx = canvas.getContext("2d"); ctx.fillRect(25, 25, 100, 100); ctx.clearRect(45, 45, 60, 60); ctx.strokeRect(50, 50, 50, 50); }
-
矩形:
rect(x, y, width, height)
:将一个矩形路径增加到当前路径上
2.2 绘制路径
- 路径操作:
beginPath()
:开始新路径。moveTo(x, y)
:移动画笔到指定点。lineTo(x, y)
:从当前点画线到指定点。closePath()
:闭合路径。stroke()
:绘制路径边框。fill()
:填充路径。
- 示例:
绘制一个三角形:const canvas = document.getElementById("canvas"); if (canvas.getContext) { const ctx = canvas.getContext("2d"); ctx.beginPath(); ctx.moveTo(50, 50); ctx.lineTo(150, 50); ctx.lineTo(100, 150); ctx.closePath(); ctx.stroke(); }
2.3 圆形和弧形
- 绘制圆形:
arc(x, y, radius, startAngle, endAngle, anticlockwise)
:绘制圆弧。- arc() 函数中表示角的单位是弧度,不是角度。角度与弧度的 js 表达式:
弧度=(Math.PI/180)*角度
- arc() 函数中表示角的单位是弧度,不是角度。角度与弧度的 js 表达式:
arcTo(x1, y1, x2, y2, radius)
:通过控制点绘制圆弧。
- 示例:
const canvas = document.getElementById("canvas"); if (canvas.getContext) { const ctx = canvas.getContext("2d"); ctx.beginPath(); ctx.arc(100, 100, 50, 0, Math.PI * 2, false); ctx.stroke(); }
2.4 贝塞尔曲线
-
二次贝塞尔曲线
quadraticCurveTo(cp1x, cp1y, x, y)
:绘制二次贝塞尔曲线,cp1x,cp1y 为一个控制点,x,y 为结束点。
-
三次贝塞尔曲线
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
:绘制三次贝塞尔曲线,cp1x,cp1y为控制点一,cp2x,cp2y为控制点二,x,y为结束点。
-
示例:
二次贝塞尔曲线:const canvas = document.getElementById("canvas"); if (canvas.getContext) { const ctx = canvas.getContext("2d"); ctx.beginPath(); ctx.moveTo(75, 25); ctx.quadraticCurveTo(25, 25, 25, 62.5); ctx.stroke(); }
三次贝塞尔曲线:
const canvas = document.getElementById("canvas"); if (canvas.getContext) { const ctx = canvas.getContext("2d"); ctx.beginPath(); ctx.moveTo(75, 40); ctx.bezierCurveTo(75, 37, 70, 25, 50, 25); ctx.bezierCurveTo(20, 25, 20, 62.5, 20, 62.5); ctx.bezierCurveTo(20, 80, 40, 102, 75, 120); ctx.stroke(); }
3. 样式和颜色
3.1 填充和描边
fillStyle
:设置填充颜色或渐变。strokeStyle
:设置描边颜色或渐变。- 示例:
// 这些 fillStyle 的值均为“橙色” ctx.fillStyle = "orange"; ctx.fillStyle = "#FFA500"; ctx.fillStyle = "rgb(255,165,0)"; ctx.fillStyle = "rgba(255,165,0,1)";
3.2 透明度
- rgba() 透明度颜色:
ctx.strokeStyle = "rgba(255,0,0,0.5)";
globalAlpha
属性// 设置透明度值 ctx.globalAlpha = 0.2;
3.3 线型
-
lineWidth = value
:设置线条宽度。默认值1.0 -
lineCap = type
:设置线条末端样式。值:butt 、round 、square -
lineJoin = type
:设定线条与线条间接合处的样式。 值:round、bevel、miter -
miterLimit = value
:限制当两条线相交时交接处最大长度 -
getLineDash()
:返回一个包含当前虚线样式,长度为非负偶数的数组 -
setLineDash(segments)
:设置当前虚线样式。 -
lineDashOffset = value
:设置虚线样式的起始偏移量 -
示例:
const canvas = document.getElementById("canvas"); if (canvas.getContext) { const ctx = canvas.getContext("2d"); ctx.lineWidth = 10; ctx.lineCap = "round"; ["round", "bevel", "miter"].forEach((join, i) => { ctx.lineJoin = join; ctx.beginPath(); ctx.moveTo(10, 5 + i * 40); ctx.lineTo(55, 45 + i * 40); ctx.lineTo(95, 5 + i * 40); ctx.lineTo(135, 45 + i * 40); ctx.lineTo(175, 5 + i * 40); ctx.stroke(); }); }
虚线示例:
const canvas = document.getElementById("canvas"); if (canvas.getContext) { const ctx = canvas.getContext("2d"); // 虚线 ctx.beginPath(); ctx.setLineDash([10, 15]); ctx.moveTo(0, 50); ctx.lineTo(300, 50); ctx.stroke(); }
虚线动画示例:
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); let offset = 0; function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.setLineDash([4, 2]); ctx.lineDashOffset = offset; ctx.strokeRect(20, 20, 100, 100); } setInterval(() => { offset++; if (offset > 5) { offset = 0; } draw(); }, 20);
lineCap 值详细说明:
“butt”:线条末端呈正方形。这是默认值。
“round”:线条末端呈圆形的。
“square”:线条末端呈方形,通过添加一个宽度与线条粗细相同且高度粗细的一半的盒子来形成lineJoin 值详细说明:
“round”:通过填充一个额外的,圆心在相连部分末端的扇形,绘制拐角的形状。圆角的半径是线段的宽度。
“bevel”:在相连部分的末端填充一个额外的以三角形为底的区域,每个部分都有各自独立的矩形拐角。
“miter”:通过延伸相连部分的外边缘,使其相交于一点,形成一个额外的菱形区域。这个设置受到 miterLimit 属性的影响。默认值。
3.4 渐变
-
线性渐变:
createLinearGradient(x1, y1, x2, y2)
:
接受 4 个参数,表示渐变的起点 (x1,y1) 与终点 (x2,y2) -
径向渐变:
createRadialGradient(x1, y1, r1, x2, y2, r2)
:
接受 6 个参数,前三个定义一个以 (x1,y1) 为原点,半径为 r1 的圆,后三个参数则定义另一个以 (x2,y2) 为原点,半径为 r2 的圆 -
gradient.addColorStop(position, color)
:- position 参数必须是一个 0.0 与 1.0 之间的数值,表示渐变中颜色所在的相对位置
- color 参数必须是一个有效的 CSS 颜色值(如 #FFF、rgba(0,0,0,1),等等)
示例:
var lineargradient = ctx.createLinearGradient(0, 0, 150, 150); lineargradient.addColorStop(0, "white"); lineargradient.addColorStop(1, "black");
-
示例1:线性渐变
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const gradient = ctx.createLinearGradient(0, 0, 200, 0); gradient.addColorStop(0, 'red'); gradient.addColorStop(1, 'blue'); ctx.fillStyle = gradient; ctx.fillRect(10, 10, 200, 100);
-
示例2:径向渐变
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const gradient = ctx.createRadialGradient(100, 100, 10, 100, 100, 100); gradient.addColorStop(0, 'red'); gradient.addColorStop(1, 'blue'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, 200, 200);
3.5 图案样式
-
createPattern(image, type)
- Image 可以是一个 Image 对象的引用,或者另一个 canvas 对象
- Type 必须是下面的字符串值之一:repeat,repeat-x,repeat-y 和 no-repeat
const img = new Image(); img.src = "some-image.png"; const pattern = ctx.createPattern(img, "repeat");
-
示例:
function draw() { const ctx = document.getElementById("canvas").getContext("2d"); // 创建新 image 对象,用作图案 const img = new Image(); img.src = "canvas_create_pattern.png"; img.onload = () => { // 创建图案 const pattern = ctx.createPattern(img, "repeat"); ctx.fillStyle = pattern; ctx.fillRect(0, 0, 150, 150); }; }
3.6 阴影
-
shadowOffsetX = float
:设定阴影在 X 的延伸距离 -
shadowOffsetY = float
:设定阴影在 Y 的延伸距离 -
shadowBlur = float
:用于设定阴影的模糊程度,默认为 0 -
shadowColor = color
:用于设定阴影颜色效果,默认是全透明的黑色 -
示例:
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); ctx.shadowOffsetX = 2; ctx.shadowOffsetY = 2; ctx.shadowBlur = 2; ctx.shadowColor = "rgb(0 0 0 / 50%)"; ctx.font = "20px Times New Roman"; ctx.fillStyle = "Black"; ctx.fillText("Sample String", 5, 30);
4. 文本
4.1 绘制文本
fillText(text, x, y [, maxWidth])
:填充文本。strokeText(text, x, y [, maxWidth])
:绘制文本边框- 示例:
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); ctx.font = '30px Arial'; ctx.fillText('Hello Canvas', 10, 50); ctx.strokeText("Hello world", 10, 100);
4.2 文本样式
font
:设置字体样式。textAlign
:文本对齐选项。可选的值包括:start, end, left, right or center. 默认值是 start。textBaseline
:基线对齐选项。可选的值包括:top, hanging, middle, alphabetic, ideographic, bottom。默认值是 alphabetic
4.3 预测量文本宽度
measureText()
将返回一个 TextMetrics对象的宽度、所在像素,这些体现文本特性的属TextMetrics
属性:width
:文本的宽度(以像素为单位)actualBoundingBoxLeft
:平行于基线,从 textAlign 属性确定的对齐点到文本矩形边界左侧的距离;正值表示文本矩形边界左侧在该对齐点的左侧actualBoundingBoxRight
:平行于基线,从 textAlign 属性确定的对齐点到文本矩形边界右侧的距离fontBoundingBoxAscent
:从 textBaseline 属性标明的水平线到渲染文本的所有字体的矩形最高边界顶部的距离fontBoundingBoxDescent
:从 textBaseline 属性标明的水平线到渲染文本的所有字体的矩形边界最底部的距离actualBoundingBoxAscent
:从 textBaseline 属性标明的水平线到渲染文本的矩形边界顶部的距离actualBoundingBoxDescent
:从 textBaseline 属性标明的水平线到渲染文本的矩形边界底部的距离emHeightAscent
:从 textBaseline 属性标明的水平线到线框中 em 方块顶部的距离emHeightDescent
:从 textBaseline 属性标明的水平线到线框中 em 方块底部的距离hangingBaseline
: 从 textBaseline 属性标明的水平线到线框的 hanging 基线的距离alphabeticBaseline
:从 textBaseline 属性标明的水平线到线框的 alphabetic 基线的距离ideographicBaseline
:从 textBaseline 属性标明的水平线到线框的 ideographic 基线的距离
- 示例:
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const baselinesAboveAlphabetic = [ "fontBoundingBoxAscent", "actualBoundingBoxAscent", "emHeightAscent", "hangingBaseline", ]; const baselinesBelowAlphabetic = [ "ideographicBaseline", "emHeightDescent", "actualBoundingBoxDescent", "fontBoundingBoxDescent", ]; const baselines = [...baselinesAboveAlphabetic, ...baselinesBelowAlphabetic]; ctx.font = "25px serif"; ctx.strokeStyle = "red"; baselines.forEach((baseline, index) => { const text = `Abcdefghijklmnop (${baseline})`; const textMetrics = ctx.measureText(text); const y = 50 + index * 50; ctx.beginPath(); ctx.fillText(text, 0, y); let lineY = y - Math.abs(textMetrics[baseline]); if (baselinesBelowAlphabetic.includes(baseline)) { lineY = y + Math.abs(textMetrics[baseline]); } ctx.moveTo(0, lineY); ctx.lineTo(550, lineY); ctx.stroke(); });
5. 图像
5.1 绘制图像
-
drawImage(image, x, y)
: 绘制图像。 -
drawImage(image, x, y, width, height)
:缩放绘制图像。- width 和 height,这两个参数用来控制 当向 canvas 画入时应该缩放的大小
-
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
:切片。- image 一个图像或者另一个 canvas 的引用
- sx, sy, sWidth, sHeight 是定义图像源的切片位置和大小
- dx, dy, dWidth, dHeight 是定义切片的目标显示位置和大小
-
示例1:
const img = new Image(); img.src = 'image.png'; img.onload = () => { ctx.drawImage(img, 50, 50); };
-
示例2:(缩放)
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const img = new Image(); img.onload = () => { for (let i = 0; i < 4; i++) { for (let j = 0; j < 3; j++) { ctx.drawImage(img, j * 50, i * 38, 50, 38); } } }; img.src = "https://mdn.github.io/shared-assets/images/examples/rhino.jpg";
-
示例3:(切片)
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const img = new Image(); img.onload = () => { ctx.drawImage(img, 0, 0, 250, 250, 10, 10, 200, 200); }; img.src = "https://mdn.github.io/shared-assets/images/examples/rhino.jpg";
5.2 像素操作
相关文档:MDN文档-像素操作
-
5.2.1 创建 ImageData 对象:
getImageData(x, y, width, height)
:获取图像像素数据。createImageData(width, height)
:创建 ImageData 对象
-
5.2.2 写入像素数据
putImageData(imageData, x, y)
:将像素数据绘制到 Canvas 上
-
5.2.3 ImageData 对象属性:
width
: 图片宽度,单位是像素height
: 图片高度,单位是像素data
: 一个 Uint8ClampedArray 类型的数组,表示图像的像素数据。每个像素由 4 个字节(RGBA)表示,分别是红色、绿色、蓝色和透明度。数组的长度为 width * height * 4
-
示例1:(createImageData() 方法)
const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // 创建一个 100x100 的空白 ImageData 对象 const imageData = ctx.createImageData(100, 100);
-
示例2:(getImageData() 方法)
const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // 在 canvas 上绘制一些内容 ctx.fillStyle = 'red'; ctx.fillRect(0, 0, 100, 100); // 获取 canvas 上指定区域的像素数据 const imageData = ctx.getImageData(0, 0, 100, 100);
-
5.2.4 操作 ImageData 对象
你可以通过直接操作 data 数组来修改图像的像素数据。每个像素由 4 个连续的元素表示,分别对应 RGBA 值。访问和修改像素数据:
const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // 创建一个 2x2 的 ImageData 对象 const imageData = ctx.createImageData(2, 2); // 获取像素数据数组 const data = imageData.data; // 设置第一个像素为红色 (255, 0, 0, 255) data[0] = 255; // R data[1] = 0; // G data[2] = 0; // B data[3] = 255; // A // 设置第二个像素为绿色 (0, 255, 0, 255) data[4] = 0; // R data[5] = 255; // G data[6] = 0; // B data[7] = 255; // A // 设置第三个像素为蓝色 (0, 0, 255, 255) data[8] = 0; // R data[9] = 0; // G data[10] = 255; // B data[11] = 255; // A // 设置第四个像素为半透明黑色 (0, 0, 0, 128) data[12] = 0; // R data[13] = 0; // G data[14] = 0; // B data[15] = 128; // A // 将修改后的 ImageData 绘制到 canvas 上 ctx.putImageData(imageData, 0, 0);
遍历和修改所有像素:
你可以遍历整个 data 数组来修改所有像素。例如,将图像转换为灰度:const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); ctx.fillStyle = "red"; ctx.fillRect(0, 0, 300, 300); // 获取 canvas 上指定区域的像素数据 const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; // 计算灰度值 const gray = 0.299 * r + 0.587 * g + 0.114 * b; // 设置灰度值 data[i] = gray; // R data[i + 1] = gray; // G data[i + 2] = gray; // B // Alpha 通道保持不变 } // 将修改后的 ImageData 绘制到 canvas 上 ctx.putImageData(imageData, 0, 0);
6. 状态的保存和恢复
save()
:保存画布 (canvas) 的所有状态restore()
:恢复 canvas 状态- 每当save()方法被调用后,当前的状态就被推送到栈中保存;每一次调用 restore 方法,上一个保存的状态就从栈中弹出,所有设定都恢复。
7. 变换
7.1 基本变换
translate(x, y)
:平移坐标系。rotate(angle)
:旋转坐标系,以弧度为单位。scale(x, y)
:缩放坐标系。- 示例:
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); ctx.save() ctx.fillStyle = "red"; ctx.fillRect(0, 10, 100, 100); ctx.restore(); ctx.save() ctx.beginPath(); ctx.translate(20, 0); // 平移 ctx.moveTo(0, 10); ctx.lineTo(100, 10); ctx.stroke(); ctx.restore(); ctx.save() ctx.rotate(30 * Math.PI / 180); // 旋转 ctx.beginPath(); ctx.moveTo(0, 10); ctx.lineTo(100, 10); ctx.stroke(); ctx.restore(); ctx.save() ctx.scale(2, 2); // 缩放 ctx.beginPath(); ctx.moveTo(0, 10); ctx.lineTo(100, 10); ctx.stroke(); ctx.restore();
6.2 矩阵变换
-
transform(a, b, c, d, e, f)
:自定义变换矩阵。- a (m11) 水平方向的缩放
- b(m12) 竖直方向的倾斜偏移
- c(m21) 水平方向的倾斜偏移
- d(m22) 竖直方向的缩放
- e(dx) 水平方向的移动
- f(dy) 竖直方向的移动
-
常见变换的实现
- 平移(Translate)
// 等同于 ctx.translate(tx, ty) ctx.transform(1, 0, 0, 1, tx, ty);
- 缩放(Scale)
// 等同于 ctx.scale(sx, sy) ctx.transform(sx, 0, 0, sy, 0, 0);
- 旋转(Rotate)
// 等同于 ctx.rotate(θ) const cos = Math.cos(θ); const sin = Math.sin(θ); ctx.transform(cos, sin, -sin, cos, 0, 0);
- 倾斜(Skew)
// X 轴倾斜(水平倾斜) ctx.transform(1, 0, Math.tan(skewY), 1, 0, 0); // Y 轴倾斜(垂直倾斜) ctx.transform(1, Math.tan(skewX), 0, 1, 0, 0);
- 平移(Translate)
-
示例 1:transform 组合平移和缩放
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); // 初始绘制一个矩形 ctx.fillStyle = "blue"; ctx.fillRect(10, 10, 50, 50); // 应用变换:先缩放,再平移 ctx.transform(2, 0, 0, 2, 50, 50); // 水平缩放 2 倍,垂直缩放 2 倍,平移 (50,50) // 绘制变换后的矩形(位置和大小变化) ctx.fillStyle = "red"; ctx.fillRect(10, 10, 50, 50);
-
示例2:transform 旋转
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); // 初始绘制一个矩形 ctx.fillStyle = "blue"; ctx.fillRect(10, 10, 50, 10); // 旋转 const cos = Math.cos(Math.PI / 6); const sin = Math.sin(Math.PI / 6); ctx.transform(cos, sin, -sin, cos, 0, 0); // 绘制变换后的矩形(旋转) ctx.fillStyle = "red"; ctx.fillRect(10, 10, 50, 10);
-
setTransform(a, b, c, d, e, f)
:重置并设置变换矩阵。const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); // 旋转 const cos = Math.cos(Math.PI / 6); const sin = Math.sin(Math.PI / 6); ctx.transform(cos, sin, -sin, cos, 0, 0); // 绘制变换后的矩形(旋转) ctx.fillStyle = "red"; ctx.fillRect(10, 10, 50, 10); // 重置矩阵并应用新变换 ctx.setTransform(2, 0, 0, 2, 0, 0); // 直接设置缩放为 2x // 绘制变换后的矩形 ctx.fillStyle = "green"; ctx.fillRect(10, 10, 50, 10);
-
transform() vs setTransform()
transform()
: 叠加变换,当前变换矩阵会与新的矩阵相乘。setTransform()
: 直接重置变换矩阵,忽略之前的变换。
8. 裁切路径
clip()
将当前正在构建的路径转换为当前的裁剪路径。- 示例:
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); ctx.arc(75, 75, 60, 0, Math.PI * 2, true); ctx.clip(); //裁剪 ctx.fillStyle = "red"; ctx.fillRect(0, 0, 200, 200);
9. 动画
9.1 基本动画
- 使用
requestAnimationFrame
实现动画 - 示例:
function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制动画内容 requestAnimationFrame(draw); } draw();
- 太阳系的动画:
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const sun = new Image(); const moon = new Image(); const earth = new Image(); function init() { sun.src = "./assets/canvas_sun.png"; moon.src = "./assets/canvas_moon.png"; earth.src = "./assets/canvas_earth.png"; requestAnimationFrame(draw); } function draw() { ctx.globalCompositeOperation = "destination-over"; ctx.clearRect(0, 0, 300, 300); ctx.strokeStyle = "rgb(0 153 255 / 40%)"; ctx.save(); ctx.translate(150, 150); // 地球 const time = new Date(); ctx.rotate(((2 * Math.PI) / 60) * time.getSeconds() + ((2 * Math.PI) / 60000) * time.getMilliseconds()); ctx.translate(0, -105); ctx.drawImage(earth, -12, -12); // 月亮 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, 2 * Math.PI); ctx.stroke(); // 太阳 ctx.drawImage(sun, 0, 0, 300, 300); requestAnimationFrame(draw); } init();