HTML5 Canvas 技术探究

概述

在前一篇 《Canvas 基础图形3D框架 Zdog》 中我们已经尝试了使用 Canvas 框架 Zdog 实现有趣的 3D 图形,而本篇我们来探究一下 Canvas 的使用和原理,还是先从这两个矩形开始吧。

Canvas绘制两个矩形

<canvas id="testcanvas" width="500" height="500"></canvas>
<script>
    var c = document.getElementById('testcanvas');
    var p = c.getContext("2d");
    p.fillStyle = "#FF0000";
    p.fillRect(0, 0, 300, 300);
    p.fillStyle = "rgba(0,0,255,0.5)";
    p.fillRect(200,200,500,500);
</script>

Canvas 标签提供了两个属性和三个方法:

width 属性: 默认值 300 , Canvas 画布的宽度。 height 属性: 默认值 300, Canvas 画布的高度。

getContext() 方法: 返回与该 Canvas 绘图相关的环境对象, 一般我们传入 “2d”, 建立一个 CanvasRenderingContext2D 二维渲染上下文。

var canvas = document.getElementById('testcanvas');

toDataURL(type, quality) 方法: 返回一个包含图片展示的 data URI 。可以使用 type 参数其类型,默认为 PNG 格式。图片的分辨率为96dpi。

var dataUrl = canvas.toDataURL("image/jpeg", 0.5);
console.log(dataUrl);

toBlob(callback, type, args) 方法: 创造Blob对象,用以展示canvas上的图片;这个图片文件可以被缓存或保存到本地,由用户代理端自行决定。如不特别指明,图片的类型默认为 image/png(第二个参数),分辨率为96dpi(图片质量,第三个参数从0到1)。

canvas.toBlob(function(blob) {
    var newImg = document.createElement("img"),
        url = URL.createObjectURL(blob);

    newImg.onload = function() {
        URL.revokeObjectURL(url);
    };
    
    newImg.src = url;
    document.body.appendChild(newImg);
});

基本绘制

矩形

上面我们绘制的矩形就是一个基本图形:

void ctx.fillRect(x, y, width, height); //绘制填充矩形 void ctx.strokeRect(x, y, width, height); //绘制描边矩形

  • x 矩形起始点的 x 轴坐标。
  • y 矩形起始点的 y 轴坐标。
  • width 矩形的宽度。
  • height 矩形的高度。

给画笔设置颜色和渐变:

ctx.fillStyle = color; //颜色字符串,设置颜色 ctx.fillStyle = gradient; //CanvasGradient 对象 (线性渐变或者放射性渐变) ctx.fillStyle = pattern; //CanvasPattern 对象 (可重复图像) CanvasGradient ctx.createLinearGradient(x0, y0, x1, y1); //线性渐变:起始和终点坐标 CanvasGradient ctx.createRadialGradient(x0, y0, r0, x1, y1, r1); //径向渐变: 开始坐标和半径,结束坐标和半径

线性渐变:

var canvas = document.getElementById('testcanvas');
var paint = canvas.getContext("2d");

var gradient = paint.createLinearGradient(0, 0, 200, 200);
gradient.addColorStop(0,"black");//开始颜色
gradient.addColorStop(1,"white");//结束颜色
paint.fillStyle = gradient;

paint.fillRect(0, 0, 125, 125);
paint.fillRect(75, 75, 200, 200);

径向渐变:

var canvas = document.getElementById('testcanvas');
var paint = canvas.getContext("2d");

var gradient = paint.createRadialGradient(100, 100, 100, 100, 100, 0);
gradient.addColorStop(0,"black");//开始颜色
gradient.addColorStop(1,"white");//结束颜色
paint.fillStyle = gradient;

paint.fillRect(0, 0, 200, 200);

注意上面的结束坐标和半径,如果结束坐标和起始坐标不同,则会出现如下现象:

var gradient = paint.createRadialGradient(100, 100, 100, 50, 50, 0);

如果结束的半径不是 0 ,则会出现一个纯色的区域。

var gradient = paint.createRadialGradient(100, 100, 100, 50, 50, 50);

不规则直边图形

void ctx.moveTo(x, y); //将一个新的子路径的起始点移动到(x,y)坐标 void ctx.lineTo(x, y); //绘制直线的锚点 void ctx.stroke(); //根据当前的画线样式,绘制当前或已经存在的路径的方法。

var canvas = document.getElementById('testcanvas');
var paint = canvas.getContext("2d");
paint.strokeStyle = 'green';
paint.moveTo(0, 0);
paint.lineTo(100, 50);
paint.lineTo(50, 100);
paint.lineTo(150, 50);
paint.lineTo(200, 200);
paint.stroke();

假设我们要绘制两个路径,一个绿色,一个红色,还需要用到一个 beginPath() 方法来开始新路径。

void ctx.beginPath();

var canvas = document.getElementById('testcanvas');
var paint = canvas.getContext("2d");
paint.strokeStyle = 'green';
paint.lineWidth = 5;
paint.moveTo(0, 0);
paint.lineTo(100, 50);
paint.lineTo(50, 100);
paint.lineTo(150, 50);
paint.lineTo(200, 200);
paint.stroke();
// TODO 这里需要添加一个 beginPath(); 清除上面的路径
paint.strokeStyle = 'red';
paint.lineWidth = 2;
paint.lineTo(150, 150);
paint.lineTo(80, 80);
paint.stroke();

接下来我们将上面的不规则直线路径变成填充的不规则图形,注意使用 fillStylefill 代替 strokeStylestroke

var canvas = document.getElementById('testcanvas');
var paint = canvas.getContext("2d");
paint.fillStyle = "green";
paint.moveTo(0, 0);
paint.lineTo(100, 50);
paint.lineTo(50, 100);
paint.lineTo(150, 50);
paint.lineTo(200, 200);
paint.lineTo(0, 0);
paint.fill();

圆弧路径

CanvasRenderingContext2D.arc() 是 Canvas 2D API 绘制圆弧路径的方法。 圆弧路径的圆心在 (x, y) 位置,半径为 r ,根据anticlockwise (默认为顺时针)指定的方向从 startAngle 开始绘制,到 endAngle 结束。

void ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);

var canvas = document.getElementById('testcanvas');
var paint = canvas.getContext("2d");
paint.strokeStyle = "green";
paint.arc(100, 100, 80, 0, 2 * Math.PI);
paint.stroke();

当然了,我们如果使用 fill 替代 stroke 将会是一个填充的圆形, 让我们填充一下试试:

paint.arc(100, 100, 80, 0, Math.PI / 2, true);

如果我们要绘制不规则的弧形(不规则弧度)路径怎么办?这个时候就要使用 arcTo 方法了,它允许我们使用控制点坐标来控制弧度大小。

void ctx.arcTo(x1, y1, x2, y2, radius); //第一个和第二个控制点坐标。 最后一个参数是圆弧半径。

如上图紫色标记的是起始点 moveTo() , 红色标记的是两个锚点 (150, 100), (50, 20)。

var canvas = document.getElementById('testcanvas');
var paint = canvas.getContext("2d");
paint.moveTo(150, 20);
paint.arcTo(150,100,50,20,30);
paint.stroke();

绘制图片

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);

上面 sx, sy, sWidth, sHeight 一组和 dx, dy, dWidth, dHeight 一组参数的区别在于前者的宽高是压缩形变,后者是裁切。

var canvas = document.getElementById('testcanvas');
var paint = canvas.getContext("2d");
var image = new Image();
image.src = "image1.png";
image.onload = function(e){
    paint.drawImage(image, 0, 0);
}

操作图像像素

ImageData ctx.getImageData(sx, sy, sw, sh); void ctx.putImageData(imagedata, dx, dy);

图像的反色(负片)和原图对比效果

var canvas = document.getElementById("testcanvas");
var cxt = canvas.getContext("2d");
canvas.width = 293;
canvas.height = 220;
cxt.drawImage(img, 0, 0, canvas.width, canvas.height);
var imageData = cxt.getImageData(0, 0, canvas.width / 2, canvas.height);
var imageData_length = imageData.data.length / 4;
// 解析之后进行算法运算
for (var i = 0; i < imageData_length; i++) {
    imageData.data[i * 4] = 255 - imageData.data[i * 4];
    imageData.data[i * 4 + 1] = 255 - imageData.data[i * 4 + 1];
    imageData.data[i * 4 + 2] = 255 - imageData.data[i * 4 + 2];
}
cxt.putImageData(imageData, 0, 0, 0, 0, canvas.width / 2, canvas.height);

图像的去色效果

Gray = (Red * 0.3 + Green * 0.59 + Blue * 0.11)

for (var i = 0; i < imageData_length; i++) {
    var red = imageData.data[i * 4];
    var green = imageData.data[i * 4 + 1];
    var blue = imageData.data[i * 4 + 2];
    var gray = 0.3 * red + 0.59 * green + 0.11 * blue;
    imageData.data[i * 4] = gray;
    imageData.data[i * 4 + 1] = gray;
    imageData.data[i * 4 + 2] = gray;
}

网站计时点阵

接下来我们做一个基于 Canvas 的计时器:

我们得先做一个绘制点阵的函数,把矩阵中的点通过状态显示出来:

function showNum( x, y, num, cxt ){
    cxt.fillStyle = '#09f';
    for( var i = 0; i < digital[num].length; i++ ){
        for( var j = 0; j < digital[num][i].length; j++ ){
            if ( digital[num][i][j] == 1 ){
                cxt.beginPath();
                cxt.arc( x + j * 2 * ( radius + 1 ) 
                    + ( radius + 1 ), y + i * 2 * ( radius + 1 ) 
                    + ( radius + 1 ), radius, 0, + 360 * Math.PI / 180, false );
                cxt.closePath();
                cxt.fill();
            }
        }
    }
}

例如我们的点阵是这样的: 能看出来这个是什么数字吗?它其实是由 1 组成的一个数字 9.

[0,1,1,1,1,1,0],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[0,1,1,1,0,1,1],
[0,0,0,0,0,1,1],
[0,0,0,0,0,1,1],
[0,0,0,0,1,1,0],
[0,0,0,1,1,0,0],
[0,1,1,0,0,0,0]

把我们要显示的所有文字的矩阵定义出来,然后我们来计算当前应该显示的时间:

now = new Date();
grt= new Date("01/11/2017 00:00:00");

now.setTime(now.getTime() + 250);

days = (now - grt ) / 1000 / 60 / 60 / 24;
dnum = Math.floor(days);

hours = (now - grt ) / 1000 / 60 / 60 - (24 * dnum);
hnum = Math.floor(hours);

minutes = (now - grt ) / 1000 /60 - (24 * 60 * dnum) - (60 * hnum);
mnum = Math.floor(minutes); 

seconds = (now - grt ) / 1000 - (24 * 60 * 60 * dnum) - (60 * 60 * hnum) - (60 * mnum);
snum = Math.round(seconds); 

最后计算位置来绘制到 Canvas 上面:

showNum( 0, 0, 12, cxt );  //运
showNum( 25 * ( radius + 1 ), 0, 13, cxt );  //行
showNum( 55 * ( radius + 1 ), 0, parseInt( dnum / 100 ), cxt );  //接下来3位日期
showNum( 70 * ( radius + 1 ), 0, parseInt( dnum % 100 / 10 ), cxt );
showNum( 85 * ( radius + 1 ), 0, parseInt( dnum % 10 ), cxt );
showNum( 100 * ( radius + 1 ), 0, 11, cxt ); // : 号
showNum( 130 * ( radius + 1 ), 0, parseInt( hnum / 10 ), cxt ); //接下来2位小时
showNum( 145 * ( radius + 1 ), 0, parseInt( hnum % 10 ), cxt );
showNum( 160 * ( radius + 1 ), 0, 10, cxt ); // : 号
showNum( 169 * ( radius + 1 ), 0, parseInt( mnum / 10 ), cxt ); //接下来2位分钟
showNum( 184 * ( radius + 1 ), 0, parseInt( mnum % 10 ), cxt );
showNum( 199 * ( radius + 1 ), 0, 10, cxt ); // : 号
showNum( 208 * ( radius + 1 ), 0, parseInt( snum / 10 ), cxt ); //接下来2位秒
showNum( 223 * ( radius + 1 ), 0, parseInt( snum % 10 ), cxt );