Canvas使用
canvas是一个可以使用脚本(通常为JavaScript)来绘制图形的 HTML 元素.例如,它可以用于绘制图表、制作图片构图或者制作简单的(以及不那么简单的)。像echarts、antv就是使用canvas来绘制图表的
标签
首先要有一个canvas标签存在于html中
<canvas id="charts" width="150" height="150"></canvas>
然后在js中获取它的上下文,然后来绘制和处理展示内容
var canvas = document.getElementById('charts');
// 使用 getContext() 方法来检测是否支持canvas
if (canvas.getContext){
var ctx = canvas.getContext('2d');
// drawing code here
} else {
// canvas-unsupported code here
}
绘制形状
使用canvas能轻松的绘制矩形,三角形,直线,圆弧和曲线,还能自己指定一个点来构成一个路径来绘制,使用fillStyle属性修改填充色,使用strokeStyle修改线的颜色
- 绘制矩形
fillRect(x, y, width, height) // 绘制一个填充的矩形
// x 矩形起始点的 x 轴坐标
// y 矩形起始点的 y 轴坐标
// width 矩形的宽度
// height 矩形的高度
strokeRect(x, y, width, height) // 绘制一个填充的矩形
clearRect(x, y, width, height) // 清除指定矩形区域,让清除部分完全透明
// 绘制矩形
ctx.fillRect(25, 25, 100, 100);
ctx.clearRect(45, 45, 60, 60);
ctx.strokeRect(50, 50, 50, 50);
- 绘制路径
(1)首先,你需要创建路径起始点。(2)然后你使用画图命令去画出路径。(3)之后你把路径封闭。(4)一旦路径生成,你就能通过描边或填充路径区域来渲染图形。
// beginPath() 新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径
// closePath() 闭合路径之后图形绘制命令又重新指向到上下文中
// stroke() 通过线条来绘制图形轮廓
// fill() 通过填充路径的内容区域生成实心的图形
// 绘制三角形
ctx.beginPath()
ctx.moveTo(75, 50);
ctx.lineTo(100, 75);
ctx.lineTo(100, 25);
ctx.lineTo(75, 50);
ctx.stroke()
ctx.closePath()
- 使用路径绘制笑脸
canvas就是一块画布,我们可以移动画笔会绘制图案,和ps的路径相似,完成路径后可以选择是填充还是描边,使用moveTo()来移动画笔,使用lineTo()来连线
arc(x,y,r,start,stop,bloo) // 绘制圆
// x 相当于margin-left
// y 相当于margin-top
// r 圆的半径
// start 从圆上的起点
// stop 圆的终点
// bloo 为false顺时针,为true逆时针
// 绘制笑脸
ctx.beginPath()
ctx.arc(75, 75, 50, 0, Math.PI*2, true)
ctx.moveTo(110, 75)
ctx.arc(75, 75, 35, 0, Math.PI, false) // false为顺时针
ctx.moveTo(65, 65)
ctx.arc(60, 65, 5, 0, Math.PI*2, true)
ctx.moveTo(95,65)
ctx.arc(90, 65, 5, 0, Math.PI*2, true)
ctx.stroke()
- 绘制太极
// 绘制整个的圆
ctx.beginPath()
ctx.arc(100, 100, 60, 0, Math.PI*2, true)
ctx.stroke()
ctx.closePath()
// 绘制下半圆
ctx.beginPath()
ctx.arc(100, 100, 60, 0, Math.PI, false)
ctx.closePath()
ctx.fillStyle = '#000'
ctx.fill()
// 绘制上半圆右侧
ctx.beginPath()
ctx.arc(130, 100, 30, 0, Math.PI, true)
ctx.closePath()
ctx.fill()
// 绘制小圆点
ctx.beginPath()
ctx.arc(130, 100, 5, 0, Math.PI*2, true)
ctx.closePath()
ctx.fillStyle = '#fff'
ctx.fill()
// 绘制下半圆左侧
ctx.beginPath()
ctx.arc(70, 100, 30, 0, Math.PI, false)
ctx.closePath()
ctx.fill()
// 绘制小圆点
ctx.beginPath()
ctx.arc(70, 100, 5, 0, Math.PI*2, true)
ctx.closePath()
ctx.fillStyle = '#000'
ctx.fill()
- 二次贝塞尔曲线
quadraticCurveTo(cp1x, cp1y, x, y)绘制二次贝塞尔曲线,cp1x,cp1y为一个控制点,x,y为结束点。bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)绘制三次贝塞尔曲线,cp1x,cp1y为控制点一,cp2x,cp2y为控制点二,x,y为结束点。
// 二次贝塞尔曲线
ctx.beginPath();
ctx.moveTo(75,25);
ctx.quadraticCurveTo(25,25,25,62.5);
ctx.quadraticCurveTo(25,100,50,100);
ctx.quadraticCurveTo(50,120,30,125);
ctx.quadraticCurveTo(60,120,65,100);
ctx.quadraticCurveTo(125,100,125,62.5);
ctx.quadraticCurveTo(125,25,75,25);
ctx.stroke();
//三次贝塞尔曲线
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.bezierCurveTo(110,102,130,80,130,62.5);
ctx.bezierCurveTo(130,62.5,130,25,100,25);
ctx.bezierCurveTo(85,25,75,37,75,40);
ctx.fill();
添加样式和颜色
- 颜色
ctx.fillStyle = "orange";
ctx.fillStyle = "#FFA500";
ctx.fillStyle = "rgb(255,165,0)";
ctx.fillStyle = "rgba(255,165,0,1)";
// 利用rgba来绘制
for(var i=0;i<6;i++){
for(var j=0;j<6;j++){
ctx.fillStyle = `rgba(0,${Math.floor(255-42.5*i)},${Math.floor(255-42.5*j)},${0.2+Math.random()*0.8})`
ctx.fillRect(i*25,j*25,25,25)
}
}
- 透明度 Transparency
通过设置 globalAlpha 属性或者使用一个半透明颜色作为轮廓或填充的样式
ctx.fillStyle = '#FD0';
ctx.fillRect(0,0,75,75);
ctx.fillStyle = '#6C0';
ctx.fillRect(75,0,75,75);
ctx.fillStyle = '#09F';
ctx.fillRect(0,75,75,75);
ctx.fillStyle = '#F30';
ctx.fillRect(75,75,75,75);
ctx.fillStyle = '#FFF';
// 设置透明度值
ctx.globalAlpha = 0.2;
// 画半透明圆
for (var i=0;i<7;i++){
ctx.beginPath();
ctx.arc(75,75,10+10*i,0,Math.PI*2,true);
ctx.fill();
}
- 线型 Line styles
/*
lineWidth = value
设置线条宽度。
lineCap = type 'butt','round','square'
设置线条末端样式。
lineJoin = type 'round', 'bevel', 'miter'
设定线条与线条间接合处的样式。
miterLimit = value
限制当两条线相交时交接处最大长度;所谓交接处长度(斜接长度)是指线条交接处内角顶点到外角顶点的长度。
getLineDash()
返回一个包含当前虚线样式,长度为非负偶数的数组。
setLineDash(segments)
设置当前虚线样式。
lineDashOffset = value
设置虚线样式的起始偏移量
*/
- 渐变
使用 createLinearGradient(x1, y1, x2, y2)(线性渐变)、createRadialGradient(x1, y1, r1, x2, y2, r2)(径向渐变) 方法创建渐变。createLinearGradient 方法接受 4 个参数,表示渐变的起点 (x1,y1) 与终点 (x2,y2)。createRadialGradient 方法接受 6 个参数,前三个定义一个以 (x1,y1) 为原点,半径为 r1 的圆,后三个参数则定义另一个以 (x2,y2) 为原点,半径为 r2 的圆。
创建出 canvasGradient 对象后,我们就可以用 addColorStop 方法给它上色了,addColorStop 方法接受 2 个参数,position 参数必须是一个 0.0 与 1.0 之间的数值,表示渐变中颜色所在的相对位置。例如,0.5 表示颜色会出现在正中间。color 参数必须是一个有效的 CSS 颜色值(如 #FFF, rgba(0,0,0,1),等等)。strokeStyle 和 fillStyle 属性都可以接受 canvasGradient 对象。
var lineargradient = ctx.createLinearGradient(0,0,150,150);
var radialgradient = ctx.createRadialGradient(75,75,0,75,75,100);
lineargradient.addColorStop(0,'white');
lineargradient.addColorStop(1,'black');
ctx.fillStyle = lineargradient
ctx.fillRect(0,0,80,60)
ctx.beginPath()
ctx.arc(40,30,26,0,Math.PI*2,true)
lineargradient.addColorStop(0.3, '#00bcd4')
lineargradient.addColorStop(0.2, '#dd5866')
ctx.strokeStyle = lineargradient
ctx.stroke()
ctx.closePath()
使用径向渐变
var radialGradient = ctx.createRadialGradient(75,75,0,75,75,100)
ctx.arc(75,75,75,0,Math.PI*2)
radialGradient.addColorStop(0, '#00C9FF')
radialGradient.addColorStop(0.3, '#E4C700')
radialGradient.addColorStop(0.5, '#00bcd4')
radialGradient.addColorStop(0.8, '#00B5E2')
ctx.fillStyle = radialGradient
ctx.fill()
- 图案样式 Patterns
createPattern(image, type)该方法接受两个参数。Image 可以是一个 Image 对象的引用,或者另一个 canvas 对象。Type 必须是下面的字符串值之一:repeat,repeat-x,repeat-y 和 no-repeat
var img = new Image();
img.src = 'https://mdn.mozillademos.org/files/222/Canvas_createpattern.png';
img.onload = function() {
// 创建图案
var ptrn = ctx.createPattern(img, 'repeat');
ctx.fillStyle = ptrn;
ctx.fillRect(0, 0, 150, 150);
}
- 阴影
这个例子绘制了带阴影效果的文字
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.shadowBlur = 2;
ctx.shadowColor = "rgba(0, 0, 0, 0.5)";
ctx.font = "20px Times New Roman";
ctx.fillStyle = "Black";
ctx.fillText("Sample String", 5, 30);
- Canvas 填充规则
默认nonzero,会以fillStyle填充,evenodd会以fillStyle与#fff交替填充,从这个例子就能看出来
ctx.beginPath();
ctx.fillStyle = '#00bcd4'
ctx.arc(50, 50, 30, 0, Math.PI*2, true);
ctx.arc(50, 50, 15, 0, Math.PI*2, true);
ctx.arc(50, 50, 10, 0, Math.PI*2, true);
ctx.fill("evenodd");
绘制文本
两种绘制文本的方式fillText和strokeText
ctx.font = '40px 微软雅黑'
ctx.fillText('Hello world', 10, 40)
ctx.strokeText('Hello world', 10, 80)
- 文本的样式
/*
font = value
当前我们用来绘制文本的样式. 这个字符串使用和 CSS font 属性相同的语法. 默认的字体是 10px sans-serif。
textAlign = value
文本对齐选项. 可选的值包括:start, end, left, right or center. 默认值是 start。
textBaseline = value
基线对齐选项. 可选的值包括:top, hanging, middle, alphabetic, ideographic, bottom。默认值是 alphabetic。
direction = value
文本方向。可能的值包括:ltr, rtl, inherit。默认值是 inherit。
*/
- 预测量文本宽度 measureText
var text = ctx.measureText("foo");
console.log(text.width); // 20;
使用图片
canvas更有意思的一项特性就是图像操作能力。可以用于动态的图像合成或者作为图形的背景,以及游戏界面(Sprites)等等。浏览器支持的任意格式的外部图片都可以使用,比如PNG、GIF或者JPEG。 你甚至可以将同一个页面中其他canvas元素生成的图片作为图片源。
-
HTMLImageElement这些图片是由Image()函数构造出来的,或者任何的 img 元素
-
HTMLVideoElement用一个HTML的 video 元素作为你的图片源,可以从视频中抓取当前帧作为一个图像
-
HTMLCanvasElement可以使用另一个 canvas 元素作为你的图片源。
-
ImageBitmap这是一个高性能的位图,可以低延迟地绘制,它可以从上述的所有源以及其它几种源中生成
-
使用地址
若调用 drawImage 时,图片没装载完,那什么都不会发生(在一些旧的浏览器中可能会抛出异常)。因此你应该用load事件来保证不会在加载完毕之前使用这个图片
var img = new Image(); // 创建img元素
img.src = 'http://img.22family.com/mySKey/favicon.png'; // 设置图片源地址
img.onload = function(){
// 执行drawImage语句
ctx.drawImage(img, 0, 0)
}
- 使用data-url
其优点就是图片内容即时可用,无须再到服务器兜一圈。(还有一个优点是,可以将 CSS,JavaScript,HTML 和 图片全部封装在一起,迁移起来十分方便。)缺点就是图像没法缓存,图片大的话内嵌的 url 数据会相当的长
img.src = 'data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw==';
- 使用视频帧
var videoDom = document.getElementById('video');
video.addEventListener('loadeddata',function(){
ctx.drawImage(video, 0, 0, 500, 300)
})
- 切片 Slicing
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)后面四个参数分别就是最终的图margin-left,margin-top,与之前的比例,就是原图的宽高比例
var img = new Image(); // 创建img元素
img.src = 'http://img.22family.com/mySKey/favicon.png'; // 设置图片源地址
img.onload = function(){
// 执行drawImage语句
ctx.drawImage(img, 0, 0, 400, 400, 100, 100, 800, 800)
}
变形
变形是一种更强大的方法,可以将原点移动到另一点、对网格进行旋转和缩放。
- save 和 restore
save 和 restore 方法是用来保存和恢复 canvas 状态的,都没有参数。Canvas 的状态就是当前画面应用的所有样式和变形的一个快照。包括当前应用的变形(即移动,旋转和缩放,见下)
strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation 的值
当前的裁切路径(clipping path)。每一次调用 restore 方法,上一个保存的状态就从栈中弹出,所有设定都恢复。
ctx.fillRect(0,0,200,200)
ctx.save()
ctx.fillStyle = '#00bcd4'
ctx.fillRect(20,20,160,160)
ctx.save()
ctx.fillStyle = '#fff'
ctx.globalAlpha = 0.6
ctx.fillRect(40,40,120,120)
ctx.restore()
ctx.fillRect(60,60,80,80)
ctx.restore()
ctx.fillRect(80,80,40,40)
- 移动translate
translate(x, y)相当于设置想x,y的偏移量
for(var i=0;i<4;i++){
for(var j=0;j<4;j++){
ctx.save()
ctx.fillStyle = `rgb(0,${235-40*i},${255-40*j})`
ctx.translate(10+40*i, 10+40*j)
ctx.fillRect(0,0,30,30)
ctx.restore()
}
}
- 旋转rotate
rotate(angle)这个方法只接受一个参数:旋转的角度(angle),它是顺时针方向的,以弧度为单位的值
ctx.translate(75,75)
for(var i=1;i<6;i++){
ctx.save()
ctx.fillStyle = `rgb(0,${235-40*i},${255-40*i})`
for(var j=0;j<i*6;j++){
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()
}
- 缩放
scale(x, y),scale 方法接受两个参数。x,y 分别是横轴和纵轴的缩放因子,它们都必须是正值。值比 1.0 小表示缩小,比 1.0 大则表示放大,值为 1.0 时什么效果都没有。
ctx.strokeStyle = "#fc0";
ctx.fillRect(0,0,300,300)
ctx.save()
ctx.translate(50,50);
drawSpirograph(ctx,22,6,5);
ctx.translate(100,0)
ctx.scale(0.8,0.8)
drawSpirograph(ctx,22,6,5);
ctx.translate(100,0)
ctx.scale(0.5,0.5)
drawSpirograph(ctx,22,6,5);
ctx.restore()
ctx.strokeStyle = "#00bcd4"
ctx.save()
ctx.translate(50,150)
ctx.scale(0.8,1)
drawSpirograph(ctx,22,6,5);
ctx.translate(120,0)
ctx.scale(0.5,1)
drawSpirograph(ctx,22,6,5);
ctx.translate(200,0)
ctx.scale(0.3,1)
drawSpirograph(ctx,22,6,5);
ctx.restore()
ctx.strokeStyle = "#dd5866"
ctx.save()
ctx.translate(50,250)
ctx.scale(1,0.8)
drawSpirograph(ctx,22,6,5);
ctx.translate(100,0)
ctx.scale(1,0.5)
drawSpirograph(ctx,22,6,5);
ctx.translate(100,0)
ctx.scale(1,0.3)
drawSpirograph(ctx,22,6,5);
function drawSpirograph(ctx,R,r,O){
var x1 = R-O;
var y1 = 0;
var i = 1;
ctx.beginPath();
ctx.moveTo(x1,y1);
do {
if (i>2000) break;
var x2 = (R+r)*Math.cos(i*Math.PI/72) - (r+O)*Math.cos(((R+r)/r)*(i*Math.PI/72))
var y2 = (R+r)*Math.sin(i*Math.PI/72) - (r+O)*Math.sin(((R+r)/r)*(i*Math.PI/72))
ctx.lineTo(x2,y2);
x1 = x2;
y1 = y2;
i++;
} while (x2 != R-O && y2 != 0 );
ctx.stroke();
}
- 变形
transform(m11, m12, m21, m22, dx, dy)这个方法是将当前的变形矩阵乘上一个基于自身参数的矩阵,m11:水平方向的缩放。m12:水平方向的倾斜偏移。m21:竖直方向的倾斜偏移。m22:竖直方向的缩放。dx:水平方向的移动。dy:竖直方向的移动
ctx.fillRect(0,0,200,200)
var sin = Math.sin(Math.PI/6);
var cos = Math.cos(Math.PI/6);
ctx.translate(100, 100);
var c = 0;
for (var i=0; i <= 12; i++) {
c = Math.floor(255 / 12 * i);
ctx.fillStyle = `rgb(${c},${c},${c})`
ctx.fillRect(0, 0, 100, 10);
ctx.transform(cos, sin, -sin, cos, 0, 0);
}
合成与裁剪
globalCompositeOperation = type这个属性设定了在画新图形时采用的遮盖策略,其值是一个标识12种遮盖方式的字符串
- 裁切路径
clip()将当前正在构建的路径转换为当前的裁剪路径
ctx.fillRect(0,0,200,200)
ctx.fillStyle = '#00bcd4'
ctx.translate(100,100)
drawStar(ctx, 100)
ctx.clip()
ctx.beginPath()
ctx.fillStyle = '#fff'
ctx.arc(0,0,50,0,2*Math.PI,true)
ctx.fill()
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();
}
基本动画
- 1、清空 canvas
除非接下来要画的内容会完全充满 canvas (例如背景图),否则你需要清空所有。最简单的做法就是用 clearRect 方法。
- 2、保存 canvas 状态
如果你要改变一些会改变 canvas 状态的设置(样式,变形之类的),又要在每画一帧之时都是原始状态的话,你需要先保存一下。
- 3、绘制动画图形(animated shapes)
这一步才是重绘动画帧。
- 4、恢复 canvas 状态
如果已经保存了 canvas 的状态,可以先恢复它,然后重绘下一帧
- 使用canvas动画绘制时钟
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>canvas</title>
</head>
<body>
<canvas id="charts" width="600" height="400"></canvas>
<script type="text/javascript">
var canvas = document.getElementById('charts')
var ctx = canvas.getContext('2d')
var img = new Image()
img.src = 'http://img.22family.com/blog/article/1548667872184744835'
function clock(){
var now = new Date();
var s = now.getSeconds();
var m = now.getMinutes();
var h = now.getHours() % 12;
ctx.save()
ctx.clearRect(0,0,300,300)
ctx.fillStyle = '#000'
ctx.fillRect(0,0,300,300)
// 时钟的外部圆
ctx.strokeStyle = '#fff'
ctx.lineWidth = 5
ctx.beginPath()
ctx.arc(150,150,100,0,Math.PI*2,true)
ctx.stroke()
ctx.clip()
ctx.translate(150,150)
ctx.rotate(-Math.PI/2)
// 给表添加背景图
ctx.save()
ctx.rotate(Math.PI/2)
ctx.drawImage(img, -100, -100);
ctx.restore()
// 时钟的每5分钟的刻度
ctx.save()
for(var i=0;i<12;i++){
ctx.beginPath();
ctx.moveTo(85,0);
ctx.lineTo(95,0);
ctx.stroke();
ctx.rotate(Math.PI/6)
}
ctx.restore()
// 绘制分钟刻度
ctx.lineWidth = 3
ctx.save()
for(var i=0;i<60;i++){
if(i%5!=0){
ctx.beginPath();
ctx.moveTo(90,0);
ctx.lineTo(95,0);
ctx.stroke();
}
ctx.rotate(Math.PI/30)
}
ctx.restore()
// 绘制时针
ctx.save();
ctx.rotate( h*(Math.PI/6) + (Math.PI/360)*m + (Math.PI/21600)*s )
ctx.lineWidth = 8;
ctx.beginPath();
ctx.moveTo(-10,0);
ctx.lineTo(60,0);
ctx.stroke();
ctx.restore();
// 绘制分针
ctx.save();
ctx.rotate( (Math.PI/30)*m + (Math.PI/1800)*s )
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(-15,0);
ctx.lineTo(80,0);
ctx.stroke();
ctx.restore();
// 绘制秒针
ctx.save();
ctx.rotate( (Math.PI/30)*s )
ctx.lineWidth = 2;
ctx.strokeStyle = '#dd5866'
ctx.beginPath();
ctx.moveTo(-20,0);
ctx.lineTo(90,0);
ctx.stroke();
ctx.restore();
// 绘制连接时针分针秒针的纽扣
var radialGradient = ctx.createRadialGradient(0,0,0,0,0,5)
radialGradient.addColorStop(0.2, '#000')
radialGradient.addColorStop(1, '#00B5E2')
ctx.save()
ctx.beginPath()
ctx.arc(0,0,5,0,2*Math.PI,true)
ctx.fillStyle = radialGradient
ctx.fill()
ctx.restore()
ctx.restore()
window.requestAnimationFrame(clock)
}
window.requestAnimationFrame(clock)
</script>
</body>
</html>
高级动画
符合物理的运动来让我们的动画更加高级
- 实现一个小球在屏幕区域,遵循物理规律的运动
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style type="text/css">
*{
margin: 0;
padding: 0;
}
.container{
width:100vw;
height: 100vh;
overflow: hidden;
position: relative;
}
#ball{
width: 50px;
height: 50px;
border-radius: 50%;
background: #00bc38;
position: absolute;
left: 0;
top: 0;
}
</style>
</head>
<body>
<div class="container">
<div id="ball"></div>
</div>
<script type="text/javascript">
function init(){
var containerDom = document.getElementsByClassName('container')[0]
var ballDom = document.getElementById('ball')
var x = 0,
y = 0,
vx = 10,
vy = 10;
function updateBall(){
checkPosition()
x += vx
y += vy
ballDom.style.left = x + 'px'
ballDom.style.top = y + 'px'
}
function checkPosition(){
if(ballDom.offsetLeft>=containerDom.offsetWidth - ballDom.offsetWidth) vx = -10
if(ballDom.offsetLeft==0) vx = 10
if(ballDom.offsetTop==0) vy = 10
if(ballDom.offsetTop>=containerDom.offsetHeight - ballDom.offsetHeight) vy = -10
}
function animate(){
updateBall()
requestAnimationFrame(animate)
}
animate()
}
window.addEventListener('DOMContentLoaded', init)
</script>
</body>
</html>
- 使用canvas来实现
一般使用ctx.clearRect(0,0, canvas.width, canvas.height)来清除画布,但是要制作出长尾效果的话,使用透明背景色(rgba(255,255,255,0.3))填充
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>canvas-ball</title>
</head>
<body>
<canvas id="canvas" width="800" height="400"></canvas>
<script type="text/javascript">
var canvas = document.getElementById('canvas')
var ctx = canvas.getContext('2d')
var raf;
var running = false;
canvas.width = window.innerWidth
canvas.height = window.innerHeight
var ball = {
x: 50,
y: 50,
r: 50,
vx: 9,
vy: 3,
bg: 'rgba(255,255,255,0.3)',
color: '#00bcd4',
draw(){
// 使用透明度白色可以做出长尾效果
ctx.fillStyle = this.bg
ctx.fillRect(0,0,canvas.width, canvas.height)
ctx.beginPath()
ctx.arc(this.x,this.y,this.r,0,Math.PI*2,true)
ctx.fillStyle = this.color
ctx.fill()
}
}
function render(){
//ctx.clearRect(0,0, canvas.width, canvas.height);
ball.draw()
checkPosition()
ball.x += ball.vx
ball.y += ball.vy
// ball.vy *= .99;
// ball.vy += .25;
raf = window.requestAnimationFrame(render)
}
function checkPosition(){
if(ball.x + ball.r >= canvas.width || ball.x<ball.r) ball.vx = -ball.vx
if(ball.y + ball.r >= canvas.height || ball.y<ball.r) ball.vy = -ball.vy
}
//window.requestAnimationFrame(render)
ball.draw()
// 鼠标控制
canvas.addEventListener('mousemove', function(e){
if(running) return
ball.x = e.clientX
ball.y = e.clientY
ball.draw()
})
canvas.addEventListener('mouseout', function(){
window.cancelAnimationFrame(raf)
running = false
})
canvas.addEventListener('click', function(){
raf = window.requestAnimationFrame(render)
running = true
})
</script>
</body>
</html>
像素操作
可以直接通过ImageData对象操纵像素数据,直接读取或将数据数组写入该对象中
- 例如,要读取图片中位于第50行,第200列的像素的蓝色部份,你会写以下代码:
blueComponent = imageData.data[((50-1)*imageData.width + (200-1))*4 - 1 + 3];
- 根据行、列读取某像素点的R/G/B/A值的公式:
imageData.data[((行数-1)*imageData.width + (列数-1))*4 - 1 + 1/2/3/4];
- 你可能用会使用Uint8ClampedArray.length属性来读取像素数组的大小(以bytes为单位):
var numBytes = imageData.data.length;
- 创建ImageData
去创建一个新的,空白的ImageData对象,你应该会使用createImageData() 方法
var imageData = ctx.createImageData(300, 300);
- 得到场景像素数据
ctx.getImageData(left, top, width, height);
- 取色器的实现
根据鼠标的坐标获取当前的颜色值
var img = new Image()
img.src = 'http://img.22family.com/blog/article/1548667872184744835'
img.crossOrigin = '' // 要使用getImageData(),不添加crossOrigin属性会跨域
img.onload = function(){
ctx.drawImage(img, 0, 0)
var imageData = ctx.createImageData(300, 300);
var numBytes = imageData.data.length;
var blueComponent = imageData.data[((80-1)*imageData.width + (80-1))*4 - 1 + 1/2/3/4]
console.log(blueComponent)
}
canvas.addEventListener('mousemove', function(e){
var x = e.layerX
var y = e.layerY
var pixel = ctx.getImageData(x, y, 1, 1);
var data = pixel.data
var rgba = `rgba(${data[0]},${data[1]},${data[2]},${data[3] / 255})`
console.log(rgba)
})
- 在场景中写入像素数据
ctx.putImageData(myImageData, dx, dy)
- 缩放和防锯齿 imageSmoothingEnabled
在drawImage() 方法, 第二个画布和imageSmoothingEnabled 属性的帮助下,我们可以放大显示我们的图片及看到详情内容
var img = new Image()
img.src = 'http://img.22family.com/blog/article/1548667872184744835'
img.crossOrigin = ''
img.onload = function(){
ctx.drawImage(img, 0, 0)
ctx.imageSmoothingEnabled = 10
}
canvas.addEventListener('mousemove', function(e){
var x = e.layerX
var y = e.layerY
ctx.drawImage(canvas, x, y, 50, 50, 300, 0, 300, 300)
})
- 保存图片
canvas.toDataURL(‘image/png’),默认设定。创建一个PNG图片。
canvas.toDataURL(‘image/jpeg’, quality),创建一个JPG图片。你可以有选择地提供从0到1的品质量,1表示最好品质,0基本不被辨析但有比较小的文件大小
canvas.toBlob(callback, type, encoderOptions),这个创建了一个在画布中的代表图片的Blob对像
点击区域和无障碍访问
hit region API让你可以在canvas上定义一个区域,这让无障碍工具获取canvas上的交互内容成为可能。它能让你更容易地进行点击检测并把事件转发到DOM元素去。
/*
CanvasRenderingContext2D.addHitRegion()
在canvas上添加一个点击区域。
CanvasRenderingContext2D.removeHitRegion()
从canvas上移除指定id的点击区域。
CanvasRenderingContext2D.clearHitRegions()
移除canvas上的所有点击区域。
*/
- 焦点圈
当用键盘控制时,焦点圈是一个能帮我们在页面上快速导航的标记。要在canvas上绘制焦点圈,可以使用drawFocusIfNeeded 属性
/*
CanvasRenderingContext2D.drawFocusIfNeeded()
如果给定的元素获得了焦点,这个方法会沿着在当前的路径画个焦点圈
CanvasRenderingContext2D.scrollPathIntoView()
把当前的路径或者一个给定的路径滚动到显示区域内。
*/
性能贴士
- 1、在离屏canvas上预渲染相似的图形或重复的对象
如果你发现你的在每一帧里有好多复杂的画图运算,请考虑创建一个离屏canvas,将图像在这个画布上画一次(或者每当图像改变的时候画一次),然后在每帧上画出视线以外的这个画布
- 2、避免浮点数的坐标点,用整数取而代之
ctx.fillRect(0.3,0.4,40,50)
- 3、不要在用drawImage时缩放图像
在离屏canvas中缓存图片的不同尺寸,而不要用drawImage()去缩放它们。
- 4、使用多层画布去画一个复杂的场景
你可能会发现,你有些元素不断地改变或者移动,而其它的元素,例如外观,永远不变。这种情况的一种优化是去用多个画布元素去创建不同层次
- 5、用CSS设置大的背景图
如果像大多数游戏那样,你有一张静态的背景图,用一个静态的
- 6、用CSS transforms特性缩放画布
CSS transforms 特性由于调用GPU,因此更快捷。最好的情况是,不要将小画布放大,而是去将大画布缩小。例如Firefox系统,目标分辨率480 x 320 px
- 7、关闭透明度
如果你的游戏使用画布而且不需要透明,当使用 HTMLCanvasElement.getContext() 创建一个绘图上下文时把 alpha 选项设置为 false 。这个选项可以帮助浏览器进行内部优化
var ctx = canvas.getContext('2d', { alpha: false });
/*
将画布的函数调用集合到一起(例如,画一条折线,而不要画多条分开的直线)
避免不必要的画布状态改变
渲染画布中的不同点,而非整个新状态
尽可能避免 shadowBlur特性
尽可能避免text rendering
使用不同的办法去清除画布(clearRect() vs. fillRect() vs. 调整canvas大小)
有动画,请使用window.requestAnimationFrame() 而非window.setInterval()
请谨慎使用大型物理库
*/
Demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>canvas</title>
</head>
<body>
<video style="display:none;" src="http://img.22family.com/test/test-video.mp4" id="video"></video>
<canvas id="charts" width="600" height="400"></canvas>
<script type="text/javascript">
var canvas = document.getElementById('charts')
var ctx = canvas.getContext('2d')
// 绘制矩形
// ctx.fillRect(25, 25, 100, 100);
// ctx.clearRect(45, 45, 60, 60);
// ctx.strokeRect(50, 50, 50, 50);
// 绘制三角形
// ctx.beginPath()
// ctx.moveTo(75, 50);
// ctx.lineTo(100, 75);
// ctx.lineTo(100, 25);
// ctx.lineTo(75, 50);
// ctx.stroke()
// ctx.closePath()
// 绘制笑脸
// ctx.beginPath()
// ctx.arc(75, 75, 50, 0, Math.PI*2, true)
// ctx.moveTo(110, 75)
// ctx.arc(75, 75, 35, 0, Math.PI, false) // false为顺时针
// ctx.moveTo(65, 65)
// ctx.arc(60, 65, 5, 0, Math.PI*2, true)
// ctx.moveTo(95,65)
// ctx.arc(90, 65, 5, 0, Math.PI*2, true)
// ctx.stroke()
// 绘制太极
// // 绘制整个的圆
// ctx.beginPath()
// ctx.arc(100, 100, 60, 0, Math.PI*2, true)
// ctx.stroke()
// ctx.closePath()
// // 绘制下半圆
// ctx.beginPath()
// ctx.arc(100, 100, 60, 0, Math.PI, false)
// ctx.closePath()
// ctx.fillStyle = '#000'
// ctx.fill()
// // 绘制上半圆右侧
// ctx.beginPath()
// ctx.arc(130, 100, 30, 0, Math.PI, true)
// ctx.closePath()
// ctx.fill()
// // 绘制小圆点
// ctx.beginPath()
// ctx.arc(130, 100, 5, 0, Math.PI*2, true)
// ctx.closePath()
// ctx.fillStyle = '#fff'
// ctx.fill()
// // 绘制下半圆左侧
// ctx.beginPath()
// ctx.arc(70, 100, 30, 0, Math.PI, false)
// ctx.closePath()
// ctx.fill()
// // 绘制小圆点
// ctx.beginPath()
// ctx.arc(70, 100, 5, 0, Math.PI*2, true)
// ctx.closePath()
// ctx.fillStyle = '#000'
// ctx.fill()
// 二次贝塞尔曲线
// ctx.beginPath();
// ctx.moveTo(75,25);
// ctx.quadraticCurveTo(25,25,25,62.5);
// ctx.quadraticCurveTo(25,100,50,100);
// ctx.quadraticCurveTo(50,120,30,125);
// ctx.quadraticCurveTo(60,120,65,100);
// ctx.quadraticCurveTo(125,100,125,62.5);
// ctx.quadraticCurveTo(125,25,75,25);
// ctx.stroke();
//三次贝塞尔曲线
// 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.bezierCurveTo(110,102,130,80,130,62.5);
// ctx.bezierCurveTo(130,62.5,130,25,100,25);
// ctx.bezierCurveTo(85,25,75,37,75,40);
// ctx.fill();
// 利用rgba来绘制
// for(var i=0;i<6;i++){
// for(var j=0;j<6;j++){
// ctx.fillStyle = `rgba(0,${Math.floor(255-42.5*i)},${Math.floor(255-42.5*j)},${0.2+Math.random()*0.8})`
// ctx.fillRect(i*25,j*25,25,25)
// }
// }
// 透明度 Transparency
// ctx.fillStyle = '#FD0';
// ctx.fillRect(0,0,75,75);
// ctx.fillStyle = '#6C0';
// ctx.fillRect(75,0,75,75);
// ctx.fillStyle = '#09F';
// ctx.fillRect(0,75,75,75);
// ctx.fillStyle = '#F30';
// ctx.fillRect(75,75,75,75);
// ctx.fillStyle = '#FFF';
// ctx.globalAlpha = 0.2;
// for(var i=0;i<7;i++){
// ctx.beginPath()
// ctx.arc(75, 75, 10+10*i, 0, Math.PI*2, true)
// ctx.fill()
// }
// 线性渐变
// var lineargradient = ctx.createLinearGradient(0,0,150,150);
// var radialgradient = ctx.createRadialGradient(75,75,0,75,75,100);
// lineargradient.addColorStop(0,'white');
// lineargradient.addColorStop(1,'black');
// ctx.fillStyle = lineargradient
// ctx.fillRect(0,0,80,60)
// ctx.beginPath()
// ctx.arc(40,30,26,0,Math.PI*2,true)
// lineargradient.addColorStop(0.3, '#00bcd4')
// lineargradient.addColorStop(0.2, '#dd5866')
// ctx.strokeStyle = lineargradient
// ctx.stroke()
// ctx.closePath()
// 径向渐变
// var radialGradient = ctx.createRadialGradient(75,75,0,75,75,100)
// ctx.arc(75,75,75,0,Math.PI*2)
// radialGradient.addColorStop(0, '#00C9FF')
// radialGradient.addColorStop(0.3, '#E4C700')
// radialGradient.addColorStop(0.5, '#00bcd4')
// radialGradient.addColorStop(0.8, '#00B5E2')
// ctx.fillStyle = radialGradient
// ctx.fill()
// 图案
// 创建新 image 对象,用作图案
// var img = new Image();
// img.src = 'https://mdn.mozillademos.org/files/222/Canvas_createpattern.png';
// img.onload = function() {
// // 创建图案
// var ptrn = ctx.createPattern(img, 'repeat');
// ctx.fillStyle = ptrn;
// ctx.fillRect(0, 0, 150, 150);
// }
// 阴影
// ctx.shadowOffsetX = 2;
// ctx.shadowOffsetY = 2;
// ctx.shadowBlur = 2;
// ctx.shadowColor = "rgba(0, 0, 0, 0.5)";
// ctx.font = "20px Times New Roman";
// ctx.fillStyle = "Black";
// ctx.fillText("Sample String", 5, 30);
// 填充规则
// ctx.beginPath();
// ctx.fillStyle = '#00bcd4'
// ctx.arc(50, 50, 30, 0, Math.PI*2, true);
// ctx.arc(50, 50, 15, 0, Math.PI*2, true);
// ctx.arc(50, 50, 10, 0, Math.PI*2, true);
// ctx.fill("evenodd");
// 文本
// ctx.font = '40px 微软雅黑'
// ctx.fillText('Hello world', 10, 40)
// ctx.strokeText('Hello world', 10, 80)
// 预测量文本宽度
// var text = ctx.measureText("foo");
// console.log(text.width); // 20;
// 图片
// var img = new Image(); // 创建img元素
// img.src = 'http://img.22family.com/mySKey/favicon.png'; // 设置图片源地址
// img.onload = function(){
// // 执行drawImage语句
// ctx.drawImage(img, 0, 0)
// }
// 获取视频帧图片
// var videoDom = document.getElementById('video');
// video.addEventListener('loadeddata',function(){
// ctx.drawImage(video, 0, 0, 500, 300)
// })
// 切片
// var img = new Image(); // 创建img元素
// img.src = 'http://img.22family.com/mySKey/favicon.png'; // 设置图片源地址
// img.onload = function(){
// // 执行drawImage语句
// ctx.drawImage(img, 0, 0, 400, 400, 100, 100, 800, 800)
// }
// save与restore
// ctx.fillRect(0,0,200,200)
// ctx.save()
// ctx.fillStyle = '#00bcd4'
// ctx.fillRect(20,20,160,160)
// ctx.save()
// ctx.fillStyle = '#fff'
// ctx.globalAlpha = 0.6
// ctx.fillRect(40,40,120,120)
// ctx.restore()
// ctx.fillRect(60,60,80,80)
// ctx.restore()
// ctx.fillRect(80,80,40,40)
// 移动translate
// for(var i=0;i<4;i++){
// for(var j=0;j<4;j++){
// ctx.save()
// ctx.fillStyle = `rgb(0,${235-40*i},${255-40*j})`
// ctx.translate(10+40*i, 10+40*j)
// ctx.fillRect(0,0,30,30)
// ctx.restore()
// }
// }
// 旋转
// ctx.translate(75,75)
// for(var i=1;i<6;i++){
// ctx.save()
// ctx.fillStyle = `rgb(0,${235-40*i},${255-40*i})`
// for(var j=0;j<i*6;j++){
// 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()
// }
// 缩放
// ctx.strokeStyle = "#fc0";
// ctx.fillRect(0,0,300,300)
// ctx.save()
// ctx.translate(50,50);
// drawSpirograph(ctx,22,6,5);
// ctx.translate(100,0)
// ctx.scale(0.8,0.8)
// drawSpirograph(ctx,22,6,5);
// ctx.translate(100,0)
// ctx.scale(0.5,0.5)
// drawSpirograph(ctx,22,6,5);
// ctx.restore()
// ctx.strokeStyle = "#00bcd4"
// ctx.save()
// ctx.translate(50,150)
// ctx.scale(0.8,1)
// drawSpirograph(ctx,22,6,5);
// ctx.translate(120,0)
// ctx.scale(0.5,1)
// drawSpirograph(ctx,22,6,5);
// ctx.translate(200,0)
// ctx.scale(0.3,1)
// drawSpirograph(ctx,22,6,5);
// ctx.restore()
// ctx.strokeStyle = "#dd5866"
// ctx.save()
// ctx.translate(50,250)
// ctx.scale(1,0.8)
// drawSpirograph(ctx,22,6,5);
// ctx.translate(100,0)
// ctx.scale(1,0.5)
// drawSpirograph(ctx,22,6,5);
// ctx.translate(100,0)
// ctx.scale(1,0.3)
// drawSpirograph(ctx,22,6,5);
// function drawSpirograph(ctx,R,r,O){
// var x1 = R-O;
// var y1 = 0;
// var i = 1;
// ctx.beginPath();
// ctx.moveTo(x1,y1);
// do {
// if (i>2000) break;
// var x2 = (R+r)*Math.cos(i*Math.PI/72) - (r+O)*Math.cos(((R+r)/r)*(i*Math.PI/72))
// var y2 = (R+r)*Math.sin(i*Math.PI/72) - (r+O)*Math.sin(((R+r)/r)*(i*Math.PI/72))
// ctx.lineTo(x2,y2);
// x1 = x2;
// y1 = y2;
// i++;
// } while (x2 != R-O && y2 != 0 );
// ctx.stroke();
// }
// 变形
// ctx.fillRect(0,0,200,200)
// var sin = Math.sin(Math.PI/6);
// var cos = Math.cos(Math.PI/6);
// ctx.translate(100, 100);
// var c = 0;
// for (var i=0; i <= 12; i++) {
// c = Math.floor(255 / 12 * i);
// ctx.fillStyle = `rgb(${c},${c},${c})`
// ctx.fillRect(0, 0, 100, 10);
// ctx.transform(cos, sin, -sin, cos, 0, 0);
// }
// ctx.fillRect(0,0,200,200)
// ctx.fillStyle = '#00bcd4'
// ctx.translate(100,100)
// drawStar(ctx, 100)
// ctx.clip()
// ctx.beginPath()
// ctx.fillStyle = '#fff'
// ctx.arc(0,0,50,0,2*Math.PI,true)
// ctx.fill()
// 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();
// }
// 绘制时钟
// var img = new Image()
// img.src = 'http://img.22family.com/blog/article/1548667872184744835'
// function clock(){
// var now = new Date();
// var s = now.getSeconds();
// var m = now.getMinutes();
// var h = now.getHours() % 12;
// ctx.save()
// ctx.clearRect(0,0,300,300)
// ctx.fillStyle = '#000'
// ctx.fillRect(0,0,300,300)
// // 时钟的外部圆
// ctx.strokeStyle = '#fff'
// ctx.lineWidth = 5
// ctx.beginPath()
// ctx.arc(150,150,100,0,Math.PI*2,true)
// ctx.stroke()
// ctx.clip()
// ctx.translate(150,150)
// ctx.rotate(-Math.PI/2)
// // 给表添加背景图
// ctx.save()
// ctx.rotate(Math.PI/2)
// ctx.drawImage(img, -100, -100);
// ctx.restore()
// // 时钟的每5分钟的刻度
// ctx.save()
// for(var i=0;i<12;i++){
// ctx.beginPath();
// ctx.moveTo(85,0);
// ctx.lineTo(95,0);
// ctx.stroke();
// ctx.rotate(Math.PI/6)
// }
// ctx.restore()
// // 绘制分钟刻度
// ctx.lineWidth = 3
// ctx.save()
// for(var i=0;i<60;i++){
// if(i%5!=0){
// ctx.beginPath();
// ctx.moveTo(90,0);
// ctx.lineTo(95,0);
// ctx.stroke();
// }
// ctx.rotate(Math.PI/30)
// }
// ctx.restore()
// // 绘制时针
// ctx.save();
// ctx.rotate( h*(Math.PI/6) + (Math.PI/360)*m + (Math.PI/21600)*s )
// ctx.lineWidth = 8;
// ctx.beginPath();
// ctx.moveTo(-10,0);
// ctx.lineTo(60,0);
// ctx.stroke();
// ctx.restore();
// // 绘制分针
// ctx.save();
// ctx.rotate( (Math.PI/30)*m + (Math.PI/1800)*s )
// ctx.lineWidth = 5;
// ctx.beginPath();
// ctx.moveTo(-15,0);
// ctx.lineTo(80,0);
// ctx.stroke();
// ctx.restore();
// // 绘制秒针
// ctx.save();
// ctx.rotate( (Math.PI/30)*s )
// ctx.lineWidth = 2;
// ctx.strokeStyle = '#dd5866'
// ctx.beginPath();
// ctx.moveTo(-20,0);
// ctx.lineTo(90,0);
// ctx.stroke();
// ctx.restore();
// // 绘制连接时针分针秒针的纽扣
// var radialGradient = ctx.createRadialGradient(0,0,0,0,0,5)
// radialGradient.addColorStop(0.2, '#000')
// radialGradient.addColorStop(1, '#00B5E2')
// ctx.save()
// ctx.beginPath()
// ctx.arc(0,0,5,0,2*Math.PI,true)
// ctx.fillStyle = radialGradient
// ctx.fill()
// ctx.restore()
// ctx.restore()
// window.requestAnimationFrame(clock)
// }
// window.requestAnimationFrame(clock)
// 像素操作
// var img = new Image()
// img.src = 'http://img.22family.com/blog/article/1548667872184744835'
// img.crossOrigin = ''
// img.onload = function(){
// ctx.drawImage(img, 0, 0)
// var imageData = ctx.createImageData(300, 300);
// var numBytes = imageData.data.length;
// var blueComponent = imageData.data[((80-1)*imageData.width + (80-1))*4 - 1 + 1/2/3/4]
// console.log(blueComponent)
// }
// canvas.addEventListener('mousemove', function(e){
// var x = e.layerX
// var y = e.layerY
// var pixel = ctx.getImageData(x, y, 1, 1);
// var data = pixel.data
// var rgba = `rgba(${data[0]},${data[1]},${data[2]},${data[3] / 255})`
// console.log(rgba)
// })
// 缩放和防锯齿 imageSmoothingEnabled
var img = new Image()
img.src = 'http://img.22family.com/blog/article/1548667872184744835'
img.crossOrigin = ''
img.onload = function(){
ctx.drawImage(img, 0, 0)
ctx.imageSmoothingEnabled = 10
}
canvas.addEventListener('mousemove', function(e){
var x = e.layerX
var y = e.layerY
ctx.drawImage(canvas, x, y, 50, 50, 300, 0, 300, 300)
var img = canvas.toDataURL('image/png')
console.log(img)
})
</script>
</body>
</html>