本文基本上可以用来当做 canvas 的一个基本参考。基本涵盖了所有的 canvas 内容,当然,不包括使用 canvas 来处理的高级应用。
基本的有:
基本的架构为:
ctx.beginPath(); // 开始画线,里面没有任何参数 ctx.moveTo(x,y); // 定义起始点 ctx.lineTo(x,y); // 定义过程点 // 还可以定义线宽,线的颜色 ctx.stroke(); // 开始划线,里面没任何参数
// 简单就是 ctx.moveTo(x,y) ctx.lineTo(x,y)
控制线宽
ctx.lineWidth = 20; // 单位默认为 px
线颜色定义可以使用:
ctx.strokeStyle = "#fff";
定义线两端的格式为:
context.lineCap = 'butt';
该属性有 3 个取值:butt,round,square。分别为:
用来描述,多个路径之间的连接方式。基本取值有:round,bevel,miter。
context.beginPath(); context.moveTo(379, 150); context.lineTo(429, 50); context.lineTo(479, 150); context.lineJoin = 'bevel'; context.stroke();
详情为:
曲线和线段 基本区现有: 弧线,二次曲线,贝塞尔曲线。
基本格式为:
ctx.arcTo(cx1, cy1, x2, y2, radius);
同样,使用 moveTo 或者 lineTo 确定第一个起始点。
context.beginPath(); context.moveTo(100, 225); // P0 context.arcTo(228, 40, 530, 70, 89); // P1, P2 and the radius context.lineTo(530, 70); // P2 context.stroke();
上面,P2 点用到了 lineTo。 这有什么影响吗?有的。 如果没定义 lineTo,圆弧可能并不会过到 P2 点,因为圆弧实际的算法为:
它只会确定最终圆弧的范围的大小,并不会关注 P2 点是否连接。如果没定义 lineTo 的话,结果为:
该是用来画二次曲线的:
ctx.quadraticCurveTo(cpx, cpy, x, y);
他通常结合 moveTo
来找到 3 个点,确定二次函数。
ctx.beginPath(); ctx.moveTo(50,20); // x 轴上的点 (50,20) ctx.quadraticCurveTo(230, 30, 50, 100); // 控制点为 (230,30)。 // 另外 x 轴上的点为 (50,100) ctx.stroke();
该 tag 是用来画贝塞尔曲线的,即通过定义 4 个点,即可确定,线的形状,具体格式为:
// 这里定义了两个控制点,一个基准点 ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
上面说到 4 个点,还有一个点是通过 moveTo 来定义的。基本格式为:
ctx.beginPath(); ctx.moveTo(50,20); ctx.bezierCurveTo(230, 30, 150, 60, 50, 100); ctx.stroke();
如图:
实际计算方法是取中点,然后取过中点的切线。
基本的简单图形有:,圆,椭圆,自定义图形. 图形方面通常是结合,ctx.fill() 来进行触发渲染的操作。
该API 用来在 canvas 上画一个。
// 基本格式为 ctx.rect(x, y, width, height); ctx.fill(); // 或者使用两者的结合属性 ctx.fillRect(x, y, width, height);
看 API 应该很容易就知道,这个是用来干啥的了。 rect 默认颜色是 black
。当然,你也可以通过使用 fillStyle
来显示的改变颜色。
ctx.fillStyle = "green"; ctx.fillRect(10, 10, 100, 100);
上面那种形式,是用来画图形内容,接着,我们可以使用 strokeRect() 来画一个矩形边框使用。 基本格式为:
ctx.strokeRect(x, y, width, height);
实际画法。
ctx.strokeStyle = "green"; ctx.strokeRect(10, 10, 100, 100);
这里,直接使用 stroke 即可,不需要在显示触发渲染啥的了。画边框,当然可以使用 line 相关的属性,比如,定义线宽。
ctx.lineWidth = 5;
实际上,结合 rect 也可以来画一个矩形框:
context.beginPath(); context.rect(188, 50, 200, 100); context.fillStyle = 'yellow'; context.fill(); context.lineWidth = 7; context.strokeStyle = 'black'; context.stroke(); // 触发画边框的效果
基本格式为:
// 默认为逆时针 ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
startAngle 是从 x 轴正方向,顺时针计算角度。 详细demo:
ctx.arc(75, 75, 50, 0, 2 * Math.PI); // 顺时针画圆 ctx.arc(75, 75, 50, 0, 2 * Math.PI,false);
比如,另外画一个半圆。
context.beginPath(); context.arc(288, 75, 70, 0, Math.PI, false); context.closePath(); // 封闭图形 context.lineWidth = 5; context.fillStyle = 'red'; context.fill(); context.strokeStyle = '#550000'; context.stroke();
这个 API 是最近提出来的,比较新。所以,兼容性需要考虑。基本格式为:
// x,y 确定长轴,短轴的位置 // rotation 按照 x 轴正方向,按照 anticlockwise 的设置进行旋转,也是弧度制 // startAngle,endAngle 也是按照 x 轴正方向来的 ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise);
他的角度表示都是 弧度制(radians)。 看个实例:
ctx.beginPath(); ctx.ellipse(100, 100, 50, 75, 45 * Math.PI/180, 0, 2 * Math.PI); ctx.stroke();
图为:
当然,你也可以使用 fill 等,来填充相关颜色。
如果你想画一个自定义图形的话,需要结合 line 相关的标签。最后使用 closePath() 来显示封闭图形。 closePath API 的实际作用是: 当你当前的点已经和起始点重合,那么 do nothing。否则,将当前点和起始点用直线连接起来,构成封闭图形,这样才可能使用 fill 相关来进行填充。
var canvas = document.getElementById('myCanvas'); var context = canvas.getContext('2d'); // begin custom shape context.beginPath(); context.moveTo(170, 80); context.bezierCurveTo(130, 100, 130, 150, 230, 150); context.bezierCurveTo(250, 180, 320, 180, 340, 150); context.bezierCurveTo(420, 150, 420, 120, 390, 100); context.bezierCurveTo(430, 40, 370, 30, 340, 50); context.bezierCurveTo(320, 5, 250, 20, 250, 50); context.bezierCurveTo(200, 5, 150, 20, 170, 80); // complete custom shape context.closePath(); context.lineWidth = 5; context.strokeStyle = 'blue'; context.stroke();
最后一定要记得使用 closePath() 这样,才能达到完整图形的效果。
关于填充有:基本颜色填充,渐变填充,图片填充。基本的颜色填充会涉及到两个,一个是 fillStyle,还有一个是 strokeStyle。两个的基本形似是一模一样的:
// 填充基本颜色值,比如 #fff ctx.fillStyle = color; // 填充渐变值,比如 createLinear 创建的渐变等 ctx.fillStyle = gradient; // 通常用来贴图用的值 ctx.fillStyle = pattern;
这个就不过说了,就是填 RGB 值。
ctx.fillStyle = "blue";
渐变色有两种,一个是线性渐变:createLinearGradient(),一个是中心渐变:createRadialGradient()。 他们可以使用一个共同的 API : addColorStop()。来设置间隔色。基本格式为:
addColorStop(offset, color);
var gradient = ctx.createLinearGradient(0,0,200,0); gradient.addColorStop(0,"green"); gradient.addColorStop(1,"white"); ctx.fillStyle = gradient; ctx.fillRect(10,10,200,100);
线性渐变的内容是:
ctx.createLinearGradient(x0, y0, x1, y1);
两个点确定一条直线,然后将颜色按照这个线段进行渐变。
实例:
代码为:
gradient.addColorStop(0,'red'); gradient.addColorStop(1,'black'); ctx.fillStyle=gradient; ctx.fillRect(0,0,320,320);
中心渐变内容为:
ctx.createRadialGradient(x0, y0, r0, x1, y1, r1);
以及上就是两个圆,然后按照顺序,将颜色渐变,当然中间也可以添加多段。
实例:
代码为:
let gradient = ctx.createRadialGradient(200,200,0,200,200,160); gradient.addColorStop(0,'red'); gradient.addColorStop(1,'white'); ctx.fillStyle=gradient; ctx.arc(200,200,160,0,2*Math.PI); ctx.fill();
这一块应该算是 canvas 牛逼的地方,能够和图形相关的元素结合起来的关键点。格式为:
ctx.createPattern(image, repetition);
这里就举一个贴图的列子:
// FROM MDN var img = new Image(); img.src = 'https://mdn.mozillademos.org/files/222/Canvas_createpattern.png'; img.onload = function() { var pattern = ctx.createPattern(img, 'repeat'); ctx.fillStyle = pattern; ctx.fillRect(0,0,400,400); };
图片处理相关的 API 有很多,这里先说最基本的,drawImage()。它有 3 种基本形式:
void ctx.drawImage(image, dx, dy); void ctx.drawImage(image, dx, dy, dWidth, dHeight); void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
分别说一下:
void ctx.drawImage(image, dx, dy);
简单的 3 个参数,就用来确定图片在 canvas 上的位置,不进行任何缩放。
基本格式为:
void ctx.drawImage(image, dx, dy, dWidth, dHeight);
通过, dWidth 和 dHeight 来确定在 canvas 中绘制的大小,可以放大和缩小。
代码为:
let img = new Image(); img.onload = function(){ ctx.drawImage(img,20,20,150,100); } img.src = "...";
基本格式为:
void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
其中,sx,sy,sWidth,sHeight 用来确定在原来图片上,截取图像的区域。
具体代码为:
let img = new Image(); img.onload = function(){ ctx.drawImage(img,20,20,60,95,60,60,100,200); } img.src = "...";
在 canvas 里,还有显示文字的一个 trick 。文字方面的话,没什么特别的就是基本的 style, szie ,color 等。 一般结合 fillText/stokeText API 一起使用,进行绘制。这里两个 API 的区别也很普通,就是简单的 填充实心字体 和 边界字体。
context.font = 'italic 40pt Calibri'; context.fillText('Hello World!', 150, 100);
先简单说一下,这两个 API。他们的格式,基本上是一样的:
ctx.strokeText(text, x, y [, maxWidth]);
代码为:
ctx.font="20px serif"; ctx.strokeText("I dont Know how to Do",20,20,200);
定义一个最简单的字体,使用的是:
ctx.font=value;
value 的值,就是一般的 css font 属性的值。默认为: 10px sans-serif
ctx.font = "48px serif";
这里,还可以使用比较新的 API FontFace() 来使用在线字体:
var f = new FontFace("font-name", "url(x)"); f.load().then(function() { ctx.font="20px font-name"; ctx.fillText("ABC",100,100); });
字体的颜色相关和上面图形一样,同样使用的是 fillStyle
和 strokeStyle
这里就不赘述了。
定义粗细的话,同样使用 lineWidth 即可。
基本格式为:
// 默认为 start ctx.textAlign = "left" || "right" || "center" || "start" || "end";
这里,并不是用来定义字体在 canvas 中的排列位置,而是用来定义,基准点相对于字体的位置。 常用的就是居中布局:
代码为:
ctx.font="20px serif"; ctx.textAlign='center'; ctx.strokeText("I dont Know how to Do",200,200);
其余的取值,比如 left,right 都是相对于该点进行绘制的。 比如,取 right/end:
基本格式为:
// 默认值为: ideographic ctx.textBaseline = "top" || "hanging" || "middle" || "alphabetic" || "ideographic" || "bottom";
该属性和 baseline 差不多,是用来确定基线在字体的哪一个位置。 在定位的时候,线是不动的,动的是字。 详情参考:
如果想知道当前字体的宽度,可以使用 measureText
API 来完成。基本使用也很简单:
var text = ctx.measureText("foo"); // TextMetrics object text.width; // 16;
当然,这个属性返回的对象上面,还挂载了很多其他测量值,不过兼容性比较差。关键点在于,可以结合该 API 来画出分行的字体内容。简单的说来就是通过将字符串拆分,判断渲染字符串是否超过本行的宽度,进而决定是否将标识点 y 轴值加 lineheight。 简单看个算法:
function wrapText(context, text, x, y, maxWidth, lineHeight) { // 通过使用 ' ' 进行字符的拆分 (这里就不针对中文了) var words = text.split(' '); var line = ''; for(var n = 0; n < words.length; n++) { // 判断行 var testLine = line + words[n] + ' '; // 测量渲染的宽度 var metrics = context.measureText(testLine); var testWidth = metrics.width; if (testWidth > maxWidth && n > 0) { // 超过,则下一该行 context.fillText(line, x, y); line = words[n] + ' '; y += lineHeight; } else { line = testLine; } } context.fillText(line, x, y); } var canvas = document.getElementById('myCanvas'); var context = canvas.getContext('2d'); var maxWidth = 400; var lineHeight = 25; var x = (canvas.width - maxWidth) / 2; var y = 60; var text = 'All the world /'s a stage, and all the men and women merely players. They have their exits and their entrances; And one man in his time plays many parts.'; context.font = '16pt Calibri'; context.fillStyle = '#333'; wrapText(context, text, x, y, maxWidth, lineHeight);
关于变化,最基本的就是 translate, scale,skew 等。在 canvas 中,这些就是针对于 canvas 坐标系来的。
这是用来进行原点平移的变化。基本格式为:
ctx.translate(x, y);
当然,还有对应的 2D 变换矩阵,这和 transform 属性一样。可以使用下列的变换:
ctx.setTransform(1, 0, 0, 1, x, y);
这是用来旋转坐标系的。基本格式为:
// 里面的参数是弧度,可以使用 Math.PI 来进行转换 ctx.rotate(angle);
例如:
ctx.rotate(45 * Math.PI / 180); ctx.fillRect(70,0,100,30);
当在旋转时,在矩阵中是根据 sin 函数来表示的旋转角度的值。
ctx.setTransform(cosθ,sinθ,-sinθ,cosθ,0,0) // 就是 cs-sc
缩放坐标系,基本格式为:
ctx.scale(x, y);
它表达的意思是:
x
值. y
值. 看个 demo:
ctx.scale(10, 3); ctx.fillRect(10,10,10,10); // 最后的结果就是,在 (100,30) 点,画出 width: 100,height: 30 的矩形。
另外,你还可以利用这个属性作颠倒:
ctx.scale(-1, 1); // x 轴对称 ctx.font = "48px serif"; ctx.fillText("Hello world!", -320, 120); // x 轴的值需要设为负数 ctx.setTransform(1, 0, 0, 1, 0, 0); // 还原坐标
它对应于矩阵的表达就是:
setTransform(A, 0, 0, B, 0,0); // X 轴放大 A 倍 // Y 轴放大 B 倍
矩阵变换的 API 和 css3 动画中的没啥区别:
ctx.setTransform(a, b, c, d, e, f);
最常使用的是用来进行坐标还原。因为,它每一次变换都是覆盖掉上一次变化,所以,还原坐标常使用:
ctx.setTransform(1,0,0,1,0,0);
不过,除了这个方法外,其他变换都是基于已经变化后的坐标来变换的。
该 API 和 setTransform 有些不同。setTransform 相当于重置,而 transform 会基于前一个变换结果,接着进行变换。它的使用方式和 setTransform 差不多。
ctx.transform(a, b, c, d, e, f);
相当于就是 setTransform(1,0,0,1,0,0)
的封装。
ctx.resetTransform();
在 canvas 里面,因为有时候操作比较多,可能会造成来回变换坐标。这时候,就可以使用 canvas 里面的状态管理。 save
& restore
,这两个方法相当于 stack 的 push
& pop
方法。一个入栈,一个出栈。那么,这些状态会保存什么呢?
不过,它仅仅只是作为一个状态进行保存的。当结合 restore 一起,才会发挥它应该有的效果。
ctx.save(); ctx.fillStyle = "green"; ctx.fillRect(10, 10, 100, 100); ctx.restore(); // 恢复原始的 fillStyle 内容 ctx.fillRect(150, 75, 100, 100);
在 canvas 中的层合并,涉及到阴影,裁剪等效果。
关于 canvas 中的阴影涉及到 4 个API。
分别介绍一下:
这是用来添加阴影的,基本格式为:
ctx.shadowBlur = level;
level 的值默认为 0,表示不存在阴影。并且,不能取 Negative, Infinity or NaN。level 表示的意义只在于,规定阴影模糊的范围而已。需要注意,一旦你设置了 shadoeBlur,那么接下来,在 canvas 上画的所有元素,都会带上阴影的效果,即使是透明元素。所以,一般情况下可以手动回撤:
ctx.shadowBlur = 0;
或者,结合 save 和 restore 来进行状态回撤。
ctx.save(); ctx.shadowBlur = 5; ctx.fillRect(0,0,400,400); ctx.restore();
用来设置阴影的颜色值,默认是不透明的黑色。
基本格式为:
// color 默认为 “black” ctx.shadowColor = color;
这个实际上和 box-shadow 设置的阴影效果是一样的,用来定义阴影相对于原始图形的偏移量。基本格式为:
// offset 默认值为 0,相当于取 canvas 上的像素值 // 可以为负值,但不能取 Infinity or NaN ctx.shadowOffsetX = offset;
实例为:
// 在 x 轴上移动阴影 ctx.shadowOffsetX = 10; // 在 Y 轴上移动阴影 ctx.shadowOffsetY = 10;
在 canvas 里面,定义颜色一般只支持 RGB 的格式,如果想要设置透明颜色的话,则需要使用 globalAlpha
属性值。基本格式为:
// value 为 [0.0,1.0] // 默认值为 0.0,表示不透明 ctx.globalAlpha = value;
看个 demo:
ctx.globalAlpha = 0.5; ctx.fillStyle = "blue"; ctx.fillRect(10, 10, 100, 100); ctx.fillStyle = "red"; ctx.fillRect(50, 50, 100, 100);
在 canvas 里面,一般使用的是 clip
API 来进行对屏幕的裁剪。基本格式为:
// 最常用 void ctx.clip(); // fillRule 主要有两种,下面再解释 void ctx.clip(fillRule); // 这里就是将需要画的路径,当参数传到 clip 里 void ctx.clip(path, fillRule);
先解释一下 fillRule: 常用的 fillRule 有两种,一种是 nonzero,一种是 even-odd 。主要作用就是用来判断重叠区域是否属于裁剪区域。ok,什么叫重叠区域呢?就是:
这种情况下,canvas 怎么判断这样的区域是否重叠呢? 默认的算法是 nonzero。
它具体的过程是,在重叠区域中,选择一个 P 点,然后随机的按照一个方向,做无限长的射线,检测该线和边界的交叉点,判断接触位置是 顺时针还是逆时针。如果为顺时针则 -1,如果为逆时针则 +1。统计最终的结果,如果为 0 则说明,该区域在外部,否则在内部。 所以,上面的结果是 -2,不是 0,则表示在内部。
该算法主要约定的是,统计射线和边界相交的次数,如果为偶数,则表示不在内部,如果为奇数,则表示在内部。
所以,根据该算法,上面的结果为 2 (偶数),则不在内部。
不过,在大多数情况下,这你都不需要过多关心。接下来,我们来实践一下,如何绘制裁剪区域。这里,我们就可以将 clip
方法想象为 stroke 方法。在 clip 前,先手动绘制路径,绘制完成后,便可以触发 clip 进行裁剪就 ok 了。
ctx.beginPath(); ctx.ellipse(100,100,30,50,0,0,2*Math.PI); ctx.clip(); ctx.fillRect(100,100,30,50);
实际样式为:
图层重叠用到的 API 为: globalCompositeOperation
。它的作用主要是用来规定,两个重叠图层绘制的效果。基本格式为:
globalCompositeOperation = type
基本的取值,可以参考: globalCompositeOperation - 取值内容
canvas 中的图像处理内容不多,基本上有 3 个API:createImageData(),putImageData(),getImageData()。第一个就是用来截屏用的,第二/三个就是用来获取图片的基本信息,在关于 image 有个比较重要的概念就是 imagedata。
Uint8ClampedArray
格式,实际上一个一维数组,按顺序,每个像素点占 4 位,里面包含了 RGBA 的内容,每一位都是 0-255 大小的数。最后透明度有点特殊,取值为 [0-255] 表示不透明。如果需要用到 css 的 rgba 中,需要 /255
。例如: [0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255]
,4个点。 createImageData
。注意,通过 image 是不能获取 imagedata 的,imagedata 只能通过 createImageData 来获取和创建。 ctx.rect(10, 10, 100, 100); ctx.fill(); let imagedata = ctx.createImageData(100, 100);
用来手动创建一个空图片内容,默认的像素都是透明的黑色,基本格式为:
ImageData ctx.createImageData(width, height); ImageData ctx.createImageData(imagedata);
目前来说,它的用途,还不知道在哪里~
该 API 常常用来作为图片分析,因为它可以直接获取 canvas 上像素的内容,然后进行相关操作。基本格式为:
ctx.getImageData(x, y, width, height);
例如:
ctx.fillRect(0,0,200,200); let new_one = ctx.getImageData(0,0,2,2);
该属性最大的用处在于,用来作为取色器。通过,传入 canvas 里的坐标,来获取指定位置的颜色值。
ctx.fillRect(0,0,200,200); let new_one = ctx.getImageData(0,0,1,1); let backColor = function(data){ return `(${data[0]},${data[1]},${data[2]},${data[3]/255})`; }
该属性常常用来作为,像素的填写。基本格式为:
void ctx.putImageData(imagedata, dx, dy); void ctx.putImageData(imagedata, dx, dy, startX, startY, width, height);
主要所一下第二种形式吧:
具体的含义为:
ctx.fillRect(0,0,100,100); var imagedata = ctx.getImageData(0,0,100,100); // 获取部分图像信息 ctx.putImageData(imagedata, 150, 0, 20, 20, 25, 25); ctx.strokeRect(150,0,100,100);
该常常用来作为放大预览,灰度处理,导出图片等。详情可以参考: 图片像素处理 。总而言之,putImageData 常作为处理图片本身,而 drawImage 常用于创建另外一新的 canvas。
这应该算一种快速生成图片格式的方法吧。该是通过算法,将图像中的像素点转化为序列值(也就是文本),我们可以直接将文本放入 img.src 中,即可显示图片。 在 canvas 中生成 URI 需要使用:
canvas.toDataURL(type, encoderOptions);
该 API 是直接挂在到 canvas 下的。
DataURI 的基本格式为:
data:[<mediatype>][;base64],<data> // 例如: data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ9oADAMBAAIRAxEAPwD/AD/6AP/Z
生成的 DataURI 是根据 canvas 的大小来确定的。
var canvas = document.getElementById("canvas"); // canvas size: 5x5 var dataURL = canvas.toDataURL(); // 生成的大小为 5x5
不过并不是,任何图片都能生成 DataURI 的,有些浏览器由于内存的限制,对 DataURI 的长度也会有所限制。例如:Opera 限制为 65000 characters。
使用 Canvas 来做动画,我们需要了解幕布的盖帘–擦除,绘制。基本的 API 有: clearRect, requestAnimation
该 API 用来清空一块幕布,基本格式为:
ctx.clearRect(x, y, width, height);
用来清除指定区域的内容。
一般来说,通常是用来清除整个 canvas 内容。
ctx.clearRect(0, 0, canvas.width, canvas.height);
主要还是依靠循环调用 RAF,来实现流畅动画。看一个 demo
var sun = new Image(); var moon = new Image(); var earth = new Image(); function init(){ // online earth.src = 'https://mdn.mozillademos.org/files/1429/Canvas_earth.png'; sun.src = 'https://mdn.mozillademos.org/files/1456/Canvas_sun.png'; moon.src = 'https://mdn.mozillademos.org/files/1443/Canvas_moon.png'; // local earth.src = 'Canvas_earth.png'; sun.src='Canvas_sun.png'; window.requestAnimationFrame(draw); } function draw(){ let canvas = document.getElementsByTagName('canvas')[0], ctx = canvas.getContext('2d'), width = canvas.width, height = canvas.height; // ctx.globalCompositeOperation = 'destination-over'; ctx.clearRect(0,0,width,height); ctx.drawImage(sun,0,0,width,height); ctx.fillStyle= 'rgba(0,0,0,0.4)'; ctx.strokeStyle = 'rgba(0,50,50,0.5)'; ctx.arc(width/2,height/2,width/3,0,2*Math.PI); ctx.stroke(); // ctx.fill(); ctx.save(); //earth let date = new Date(); ctx.translate(width/2,height/2); // 公转 ctx.rotate ((2*Math.PI)*1*(date.getSeconds()/60 + date.getMilliseconds()/60000) ); ctx.translate(width/3,0); // 自转 ctx.rotate ((2*Math.PI)*25*(date.getSeconds()/60 + date.getMilliseconds()/60000) ); ctx.drawImage(earth,-earth.width/2,-earth.height/2); ctx.save(); ctx.translate(20,0); ctx.drawImage(moon,-moon.width/2,-moon.height/2); ctx.restore(); ctx.restore(); window.requestAnimationFrame(draw); } init();