基础篇: Html5中Canvas绘制、样式详解(不包含动画部分)
此篇为后续
目录
1.状态的保存和恢复
save()
保存画布(canvas)的所有状态
restore()
save 和 restore 方法是用来保存和恢复 canvas 状态的,都没有参数。Canvas 的状态就是当前画面应用的所有样式和变形的一个快照。
Canvas状态存储在栈中,每当save()方法被调用后,当前的状态就被推送到栈中保存。一个绘画状态包括:
当前应用的变形(即移动,旋转和缩放,见下)
以及下面这些属性:strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled
当前的裁切路径(clipping path)
你可以调用任意多次 save方法。每一次调用 restore 方法,上一个保存的状态就从栈中弹出,所有设定都恢复。
save
和 restore
的应用例子
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.fillRect(0,0,150,150); // 使用默认设置绘制一个矩形
ctx.save(); // 保存默认状态
ctx.fillStyle = '#09F' // 在原有配置基础上对颜色做改变
ctx.fillRect(15,15,120,120); // 使用新的设置绘制一个矩形
ctx.save(); // 保存当前状态
ctx.fillStyle = '#FFF' // 再次改变颜色配置
ctx.globalAlpha = 0.5;
ctx.fillRect(30,30,90,90); // 使用新的配置绘制一个矩形
ctx.restore(); // 重新加载之前的颜色状态
ctx.fillRect(45,45,60,60); // 使用上一次的配置绘制一个矩形
ctx.restore(); // 加载默认颜色配置
ctx.fillRect(60,60,30,30); // 使用加载的配置绘制一个矩形
}
第一步是用默认设置画一个大四方形,然后保存一下状态。改变填充颜色画第二个小一点的蓝色四方形,然后再保存一下状态。再次改变填充颜色绘制更小一点的半透明的白色四方形。
一旦我们调用 restore
,状态栈中最后的状态会弹出,并恢复所有设置。如果不是之前用 save
保存了状态,那么我们就需要手动改变设置来回到前一个状态,这个对于两三个属性的时候还是适用的,一旦多了,我们的代码将会猛涨。
当第二次调用 restore
时,已经恢复到最初的状态,因此最后是再一次绘制出一个黑色的四方形。
2. translate移动
translate
方法,它用来移动 canvas 和它的原点到一个不同的位置。
translate(x, y)
translate
方法接受两个参数。x 是左右偏移量,y 是上下偏移量
例子 平移之后画个圆,然后恢复状态,再平移再画个圆,再恢复状态,每次都画在原点上,每次原点都不在一个位置上
function draw3() {
var ctx = document.getElementById('canvas').getContext('2d');
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
ctx.save();
ctx.fillStyle = 'rgb(' + (51 * i) + ', ' + (255 - 51 * i) + ', 255)';
ctx.translate(10 + j * 50, 10 + i * 50);
ctx.beginPath();
ctx.arc(0, 0, 10, 0, Math.PI*2, true);
ctx.fill();
ctx.restore();
}
}
}
效果
3. 旋转Rotating
rotate
方法,它用于以原点为中心旋转 canvas
rotate(angle)
这个方法只接受一个参数:旋转的角度(angle),它是顺时针方向的,以弧度为单位的值。
旋转的中心点始终是 canvas 的原点,如果要改变它,我们需要用到 translate
方法。
来个例子,记住是坐标系旋转,想问题的时候要想清楚 此案例先把中心移到了圆的中心
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.translate(75,75);
for (var i=1;i<6;i++){ // Loop through rings (from inside to out)
ctx.save();
ctx.fillStyle = 'rgb('+(51*i)+','+(255-51*i)+',255)';
for (var j=0;j<i*6;j++){ // draw individual dots
ctx.rotate(Math.PI*2/(i*6));
ctx.beginPath();
ctx.arc(0,i*12.5,5,0,Math.PI*2,true);
ctx.fill();
}
ctx.restore();
}
}
效果:
4. 缩放Scaling
我们用它来增减图形在 canvas 中的像素数目,对形状,位图进行缩小或者放大
scale(x, y)
scale
方法可以缩放画布的水平和垂直的单位。两个参数都是实数,可以为负数,x 为水平缩放因子,y 为垂直缩放因子,如果比1小,会比缩放图形, 如果比1大会放大图形。默认值为1, 为实际大小。
画布初始情况下, 是以左上角坐标为原点的第一象限。如果参数为负实数, 相当于以x 或 y轴作为对称轴镜像反转(例如, 使用translate(0,canvas.height); scale(1,-1);
以y轴作为对称轴镜像反转, 就可得到著名的笛卡尔坐标系,左下角为原点)。
默认情况下,canvas 的 1 个单位为 1 个像素。举例说,如果我们设置缩放因子是 0.5,1 个单位就变成对应 0.5 个像素,这样绘制出来的形状就会是原先的一半。同理,设置为 2.0 时,1 个单位就对应变成了 2 像素,绘制的结果就是图形放大了 2 倍。
scale
的例子
function draw6() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.save();
ctx.scale(10, 3);
ctx.fillRect(1, 10, 10, 10);
ctx.restore();
// 镜像x轴
ctx.scale(-1, 1);
ctx.font = '48px serif';
ctx.fillText('海军', -135, 120);
}
效果图
5. 图形相互交叉显示规则
ctx.globalCompositeOperation = type
这个属性设定了在画新图形时采用的遮盖策略,其值是一个标识12种遮盖方式的字符串。
source-over
这是默认设置,并在现有画布上下文之上绘制新图形。
source-in
新图形只在新图形和目标画布重叠的地方绘制。其他的都是透明的。
source-out
在不与现有画布内容重叠的地方绘制新图形。
source-atop
新图形只在与现有画布内容重叠的地方绘制。
destination-over
在现有的画布内容后面绘制新的图形。
destination-in
现有的画布内容保持在新图形和现有画布内容重叠的位置。其他的都是透明的。
destination-out
现有内容保持在新图形不重叠的地方。
destination-atop
现有的画布只保留与新图形重叠的部分,新的图形是在画布内容后面绘制的。
lighter
两个重叠图形的颜色是通过颜色值相加来确定的。
copy
只显示新图形。
xor
图像中,那些重叠和正常绘制之外的其他地方是透明的。
multiply
将顶层像素与底层相应像素相乘,结果是一幅更黑暗的图片。
screen
像素被倒转,相乘,再倒转,结果是一幅更明亮的图片。
overlay
multiply和screen的结合,原本暗的地方更暗,原本亮的地方更亮。
darken
保留两个图层中最暗的像素。
lighten
保留两个图层中最亮的像素。
color-dodge
将底层除以顶层的反置。
color-burn
将反置的底层除以顶层,然后将结果反过来。
hard-light
屏幕相乘(A combination of multiply and screen)类似于叠加,但上下图层互换了。
soft-light
用顶层减去底层或者相反来得到一个正值。
difference
一个柔和版本的强光(hard-light)。纯黑或纯白不会导致纯黑或纯白。
exclusion
和difference相似,但对比度较低。
hue
保留了底层的亮度(luma)和色度(chroma),同时采用了顶层的色调(hue)。
saturation
保留底层的亮度(luma)和色调(hue),同时采用顶层的色度(chroma)。
color
保留了底层的亮度(luma),同时采用了顶层的色调(hue)和色度(chroma)。
luminosity
保持底层的色调(hue)和色度(chroma),同时采用顶层的亮度(luma)。
6. 裁切路径
裁切路径和普通的 canvas 图形差不多,不同的是它的作用是遮罩,用来隐藏不需要的部分。如上图所示。红边五角星就是裁切路径,所有在路径以外的部分都不会在 canvas 上绘制出来。
与globalCompositeOperation
属性作一比较,它可以实现与 source-in
和 source-atop
差不多的效果。最重要的区别是裁切路径不会在 canvas 上绘制东西,而且它永远不受新图形的影响。这些特性使得它在特定区域里绘制图形时相当好用。
clip()
方法
将当前正在构建的路径转换为当前的裁剪路径。
看案例
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.fillRect(0,0,150,150);
ctx.translate(75,75);
// Create a circular clipping path
ctx.beginPath();
ctx.arc(0,0,60,0,Math.PI*2,true);
ctx.clip();
// draw background
var lingrad = ctx.createLinearGradient(0,-75,0,75);
lingrad.addColorStop(0, '#232256');
lingrad.addColorStop(1, '#143778');
ctx.fillStyle = lingrad;
ctx.fillRect(-75,-75,150,150);
// draw stars
for (var j=1;j<50;j++){
ctx.save();
ctx.fillStyle = '#fff';
ctx.translate(75-Math.floor(Math.random()*150),
75-Math.floor(Math.random()*150));
drawStar(ctx,Math.floor(Math.random()*4)+2);
ctx.restore();
}
}
function drawStar(ctx,r){
ctx.save();
ctx.beginPath()
ctx.moveTo(r,0);
for (var i=0;i<9;i++){
ctx.rotate(Math.PI/5);
if(i%2 == 0) {
ctx.lineTo((r/0.525731)*0.200811,0);
} else {
ctx.lineTo(r,0);
}
}
ctx.closePath();
ctx.fill();
ctx.restore();
}
用了一个圆形的裁切路径来限制随机星星的绘制区域。
首先,画了一个与 canvas 一样大小的黑色方形作为背景,然后移动原点至中心点。然后用 clip
方法创建一个弧形的裁切路径。裁切路径也属于 canvas 状态的一部分,可以被保存起来。如果我们在创建新裁切路径时想保留原来的裁切路径,我们需要做的就是保存一下 canvas 的状态。
裁切路径创建之后所有出现在它里面的东西才会画出来。在画线性渐变时我们就会注意到这点。然后会绘制出50 颗随机位置分布(经过缩放)的星星,当然也只有在裁切路径里面的星星才会绘制出来。
效果图
7.动画基本步骤
你可以通过以下的步骤来画出一帧:
- 清空 canvas
除非接下来要画的内容会完全充满 canvas (例如背景图),否则你需要清空所有。最简单的做法就是用clearRect
方法。 - 保存 canvas 状态
如果你要改变一些会改变 canvas 状态的设置(样式,变形之类的),又要在每画一帧之时都是原始状态的话,你需要先保存一下。 - 绘制动画图形(animated shapes)
这一步才是重绘动画帧。(重绘是相当费时的,而且性能很依赖于电脑的速度。) - 恢复 canvas 状态
如果已经保存了 canvas 的状态,可以先恢复它,然后重绘下一帧。
为了实现动画,我们需要一些可以定时执行重绘的方法。有两种方法可以实现这样的动画操控。首先可以通过 setInterval
和 setTimeout
方法来控制在设定的时间点上执行重绘。
requestAnimationFrame(draw)
告诉浏览器你希望执行一个动画,并在重绘之前,请求浏览器执行一个特定的函数来更新动画。
如果你并不需要与用户互动,你可以使用setInterval()方法,它就可以定期执行指定代码。如果我们需要做一个游戏,我们可以使用键盘或者鼠标事件配合上setTimeout()方法来实现。通过设置事件监听,我们可以捕捉用户的交互,并执行相应的动作。
简答的案例
function draw9() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.globalCompositeOperation = 'destination-over';
ctx.clearRect(0,0,500,500); // clear canvas
ctx.fillStyle = 'green';
ctx.strokeStyle = 'blueviolet';
ctx.save();
ctx.translate(180,180);
ctx.beginPath();
ctx.arc(0,0,150,0,Math.PI*2,false);
ctx.stroke();
var time = new Date();
ctx.rotate( ((2*Math.PI)/60)*time.getSeconds() + ((2*Math.PI)/60000)*time.getMilliseconds() )
ctx.beginPath();
ctx.arc(0,150,20,0,Math.PI*2,false);
ctx.fill();
ctx.closePath();
ctx.restore();
window.requestAnimationFrame(draw9)
}
window.requestAnimationFrame(draw9);
效果就是一个小圆绕着大圆旋转
案例2模拟太阳系
var sun = new Image();
var moon = new Image();
var earth = new Image();
function init(){
sun.src = 'https://mdn.mozillademos.org/files/1456/Canvas_sun.png';
moon.src = 'https://mdn.mozillademos.org/files/1443/Canvas_moon.png';
earth.src = 'https://mdn.mozillademos.org/files/1429/Canvas_earth.png';
window.requestAnimationFrame(draw);
}
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.globalCompositeOperation = 'destination-over';
ctx.clearRect(0,0,300,300); // clear canvas
ctx.fillStyle = 'rgba(0,0,0,0.4)';
ctx.strokeStyle = 'rgba(0,153,255,0.4)';
ctx.save();
ctx.translate(150,150);
// Earth
var 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,50,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();
案例3动画时钟
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";
// Hour marks
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();
// Minute marks
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";
// write Hours
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();
// 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 = "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);
}
window.requestAnimationFrame(clock);
关于动画要了解的可不止这些,包括速率,加速度,边界,拖尾效果,鼠标控制等等
举个栗子
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var raf;
var running = false;
var ball = {
x: 100,
y: 100,
vx: 5,
vy: 1,
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();
}
};
function clear() {
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(0,0,canvas.width,canvas.height);
}
function draw() {
clear();
ball.draw();
ball.x += ball.vx;
ball.y += ball.vy;
if (ball.y + ball.vy > canvas.height || ball.y + ball.vy < 0) {
ball.vy = -ball.vy;
}
if (ball.x + ball.vx > canvas.width || ball.x + ball.vx < 0) {
ball.vx = -ball.vx;
}
raf = window.requestAnimationFrame(draw);
}
canvas.addEventListener('mousemove', function(e){
if (!running) {
clear();
ball.x = e.offsetX;
ball.y = e.offsetY;
ball.draw();
}
});
canvas.addEventListener('click',function(e){
if (!running) {
raf = window.requestAnimationFrame(draw);
running = true;
}
});
canvas.addEventListener('mouseout', function(e){
window.cancelAnimationFrame(raf);
running = false;
});
ball.draw();
用你的鼠标移动小球,点击可以释放。
8. canvas相关的动画js框架
Three.js
这个流行的库提供了非常多的3D显示功能,以一种直观的方式使用 WebGL。这个库提供了<canvas>、 <svg>、CSS3D 和 WebGL渲染器,让我们在设备和浏览器之间创建丰富的交互体验。该库于2010年4月首次推出
pixi.js
虽然只是个渲染框架而非游戏引擎,但是pixi.js挺适合写游戏的,你可以看下netent上的博彩游戏,上万款游戏全是pixi.js写的。pixi.js刚接触的时候觉得很简陋,文档都是英文的,有时候还需要看源码(源码写的非常的优秀)才能理解怎么回事,这对很多人是个很高的门槛,但是如果你能克服这些,会发现用着真的很舒服。
周边插件也挺丰富的,动画(spine/dragonbones),粒子系统,物理引擎等等,想用什么就引入什么,和unix设计哲学一样,每个工具只干一件事并把这件事做好,你可以组合很多工具完成复杂的功能。
来源:oschina
链接:https://my.oschina.net/u/4364008/blog/4331492