手把手教你玩转Canvas

之前用过echarts等图表插件,首次尝试自己写原生canvas,还以为多难多复杂的东西,深入了解之后发现就是数学中的几何图形嘛~ 欢迎大家和我一起来入坑!

Canvas基本使用

<canvas> 元素

<canvas> 看起来和 <img> 标签一样,只是 <canvas> 只有两个可选的属性 width、heigth 属性,而没有 src、alt 属性。
如果不给 <canvas> 设置 widht、height 属性时,则默认 width为300、height 为 150,单位都是 px。也可以使用 css 属性来设置宽高,但是如宽高属性和初始比例不一致,他会出现扭曲。所以,建议永远不要使用 css 属性来设置 <canvas> 的宽高。
替换内容
由于某些较老的浏览器(尤其是 IE9 之前的 IE 浏览器)或者浏览器不支持 HTML 元素 <canvas>,在这些浏览器上你应该总是能展示替代内容。
支持 <canvas> 的浏览器会只渲染 <canvas> 标签,而忽略其中的替代内容。不支持 <canvas> 的浏览器则 会直接渲染替代内容。

用文本替换:

<canvas>
    你的浏览器不支持 canvas,请升级你的浏览器。
</canvas>

<img> 替换:

<canvas>
    <img src="./美女.jpg" alt=""> 
</canvas>

结束标签 </canvas> 不可省略。

<img> 元素不同,<canvas> 元素需要结束标签(</canvas>)。如果结束标签不存在,则文档的其余部分会被认为是替代内容,将不会显示出来。

2d渲染上下文

var canvas = document.getElementById('tutorial');
//获得 2d 上下文对象
var ctx = canvas.getContext('2d');

检测支持性

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

if (canvas.getContext){
  var ctx = canvas.getContext('2d');
  // drawing code here
} else {
  // canvas-unsupported code here
}

代码模板

<canvas id="tutorial" width="300" height="300"></canvas>
<script type="text/javascript">
function draw(){
    var canvas = document.getElementById('tutorial');
    if(!canvas.getContext) return;
      var ctx = canvas.getContext("2d");
      //开始代码
    
}
draw();
</script>

绘制形状

栅格 (grid) 和坐标空间

如下图所示,canvas 元素默认被网格所覆盖。通常来说网格中的一个单元相当于 canvas 元素中的一像素。栅格的起点为左上角,坐标为 (0,0) 。所有元素的位置都相对于原点来定位。所以图中蓝色方形左上角的坐标为距离左边(X 轴)x 像素,距离上边(Y 轴)y 像素,坐标为 (x,y)。

后面我们会涉及到坐标原点的平移、网格的旋转以及缩放等。

绘制矩形

<canvas> 只支持一种原生的图形绘制:矩形。所有其他图形都至少需要生成一种路径 (path)。不过,我们拥有众多路径生成的方法让复杂图形的绘制成为了可能。

canvas 提供了三种方法绘制矩形:

  1. fillRect(x, y, width, height):绘制一个填充的矩形。
  2. strokeRect(x, y, width, height):绘制一个矩形的边框。
  3. clearRect(x, y, widh, height):清除指定的矩形区域,然后这块区域会变的完全透明。

说明:这 3 个方法具有相同的参数。

  • x, y:指的是矩形的左上角的坐标。(相对于canvas的坐标原点)
  • width, height:指的是绘制的矩形的宽和高。
function draw(){
  var canvas = document.getElementById('tutorial');
  if(!canvas.getContext) return;
  var ctx = canvas.getContext("2d");
  ctx.fillRect(10, 10, 100, 50);     // 绘制矩形,填充的默认颜色为黑色
  ctx.strokeRect(10, 70, 100, 50);   // 绘制矩形边框
}
draw();
ctx.clearRect(15, 15, 50, 25);

绘制路径 (path)

图形的基本元素是路径。
路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。
一个路径,甚至一个子路径,都是闭合的。

使用路径绘制图形需要一些额外的步骤:

  1. 创建路径起始点
  2. 调用绘制方法去绘制出路径
  3. 把路径封闭
  4. 一旦路径生成,通过描边或填充路径区域来渲染图形。

下面是需要用到的方法:

  1. beginPath()
    新建一条路径,路径一旦创建成功,图形绘制命令被指向到路径上生成路径

  2. moveTo(x, y)
    把画笔移动到指定的坐标(x, y)。相当于设置路径的起始点坐标。

  3. closePath()
    闭合路径之后,图形绘制命令又重新指向到上下文中

  4. stroke()
    通过线条来绘制图形轮廓

  5. fill()
    通过填充路径的内容区域生成实心的图形

绘制线段

function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.beginPath(); //新建一条path
    ctx.moveTo(50, 50); //把画笔移动到指定的坐标
    ctx.lineTo(200, 50);  //绘制一条从当前位置到指定坐标(200, 50)的直线.
    //闭合路径。会拉一条从当前点到path起始点的直线。如果当前点与起始点重合,则什么都不做
    ctx.closePath();
    ctx.stroke(); //绘制路径。
}
draw();

绘制三角形边框

function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.beginPath();
    ctx.moveTo(50, 50);
    ctx.lineTo(200, 50);
    ctx.lineTo(200, 200);
      ctx.closePath(); //虽然我们只绘制了两条线段,但是closePath会closePath,仍然是一个3角形
    ctx.stroke(); //描边。stroke不会自动closePath()
}
draw();

填充三角形

function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.beginPath();
    ctx.moveTo(50, 50);
    ctx.lineTo(200, 50);
    ctx.lineTo(200, 200);
   
    ctx.fill(); //填充闭合区域。如果path没有闭合,则fill()会自动闭合路径。
}
draw();

绘制圆弧

有两个方法可以绘制圆弧:

  1. arc(x, y, r, startAngle, endAngle, anticlockwise): 以(x, y) 为圆心,以r 为半径,从 startAngle 弧度开始到endAngle弧度结束。anticlosewise 是布尔值,true 表示逆时针,false 表示顺时针(默认是顺时针)。

注意:

    1. 这里的度数都是弧度。
    1. 0 弧度是指的 x 轴正方向。
radians=(Math.PI/180)*degrees   //角度转换成弧度
  1. arcTo(x1, y1, x2, y2, radius): 根据给定的控制点和半径画一段圆弧,最后再以直线连接两个控制点。
function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.beginPath();
    ctx.arc(50, 50, 40, 0, Math.PI / 2, false);
    ctx.stroke();
}
draw();
function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.beginPath();
    ctx.arc(50, 50, 40, 0, Math.PI / 2, false);
    ctx.stroke();
 
    ctx.beginPath();
    ctx.arc(150, 50, 40, 0, -Math.PI / 2, true);
    ctx.closePath();
    ctx.stroke();
 
    ctx.beginPath();
    ctx.arc(50, 150, 40, -Math.PI / 2, Math.PI / 2, false);
    ctx.fill();
 
    ctx.beginPath();
    ctx.arc(150, 150, 40, 0, Math.PI, false);
    ctx.fill();
 
}
draw();
function draw(){
    var canvas = document.getElementById('tutorial');
    if (!canvas.getContext) return;
    var ctx = canvas.getContext("2d");
    ctx.beginPath();
    ctx.moveTo(50, 50);
      //参数1、2:控制点1坐标   参数3、4:控制点2坐标  参数4:圆弧半径
    ctx.arcTo(200, 50, 200, 200, 100);
    ctx.lineTo(200, 200)
    ctx.stroke();
    
    ctx.beginPath();
    ctx.rect(50, 50, 10, 10);
    ctx.rect(200, 50, 10, 10)
    ctx.rect(200, 200, 10, 10)
    ctx.fill()
}
draw();

arcTo 方法的说明:

这个方法可以这样理解。绘制的弧形是由两条切线所决定。

第 1 条切线:起始点和控制点1决定的直线。

第 2 条切线:控制点1 和控制点2决定的直线。

其实绘制的圆弧就是与这两条直线相切的圆弧。

canvas实战

环形饼图的实现

环形饼图的实例图
//html
<canvas id="circle-pei-chart"></canvas>
//调用
let chartDatas=[ {color: "rgb(253, 122, 79)",title: "后端开发",percent: 0.2}, **];
let defalutIndex=0
let circlePeiChart = new multiCircleChart("circle-pei-chart",chartDatas, defalutIndex,(i)=>{defalutIndex=i});
circlePeiChart.draw();
//重绘
circlePeiChart.defaultIndex=2;
circlePeiChart.draw();
/*
chartDatas [ {color: "rgb(253, 122, 79)",title: "后端开发",percent: 0.2}, **];
*/
class multiCircleChart {
    constructor(id, chartDatas, defalutIndex,callback) {
        this.canvas = document.getElementById(id);
        this.size = this.canvas.parentNode.clientWidth * 4;
        this.canvas.style.width = this.size / 4 + "px";
        this.canvas.style.height = this.size / 4 + "px";
        this.canvas.width = this.size;
        this.canvas.height = this.size;
        this.ctx = this.canvas.getContext("2d");
        this.defalutIndex = defalutIndex;
        this.chartDatas = chartDatas;
        this.lineWidth = this.size/5;
        this.startAngle = -0.5;
        this.callback=callback;
        this.canvas.addEventListener('click',this.mouseDownEvent.bind(this));
        this.AngleList=[];
    }
    draw() {
        this.ctx.clearRect(0,0,this.size,this.size);
        if (this.chartDatas.length == 0) return;
        this.ctx.lineWidth = this.lineWidth;
        this.ctx.lineCap="butt";
        let startAngle = Math.PI * -0.5;
        let endAngle = startAngle;
        this.AngleList=[];
        this.chartDatas.map((item, i) => {
            this.ctx.beginPath();
            this.ctx.strokeStyle = item.color;
            if (i > 0) {
                startAngle = endAngle;
            }
            endAngle = startAngle + item.percent * Math.PI * 2;
            this.AngleList.push(endAngle);
            //选中当前项,需要向外偏移
            if (i == this.defalutIndex) {
                //选中当前项,需要向外偏移
                let centerAngle=(startAngle+endAngle)/2;
                let x=this.lineWidth*0.2*Math.cos(centerAngle);
                let y=this.lineWidth*0.2*Math.sin(centerAngle);
                this.ctx.arc(this.size / 2+x, this.size / 2+y, this.size / 2 - this.lineWidth / 2 - this.lineWidth * 0.2, startAngle, endAngle);
            } else {
                this.ctx.arc(this.size / 2, this.size / 2, this.size / 2 - this.lineWidth / 2 - this.lineWidth * 0.2, startAngle, endAngle);
            }
            this.ctx.stroke();
        });
    }
    mouseDownEvent(e){
        const [x1,y1]=[e.offsetX,e.offsetY];
        const [x0,y0]=[this.size/2/4,this.size/2/4];
        let angle=0;
        if(x1>x0){
            y1<y0?angle=-0.5*Math.PI+Math.atan((x1-x0)/(y0-y1)):angle=Math.atan((y1-y0)/(x1-x0));
        }else{
            y1<y0?angle=Math.PI+Math.atan((y0-y1)/(x0-x1)):angle=Math.atan((x0-x1)/(y1-y0))+Math.PI*0.5;
        }
        for(let i=0;i<this.AngleList.length;i++){
            if(angle<this.AngleList[i]){
                this.defalutIndex=i;
                this.draw();
                this.callback?this.callback(i):'';
                break;
            }
        }
    }
}

时钟的实现

init();
 
function init(){
    let canvas = document.querySelector("#solar");
    let ctx = canvas.getContext("2d");
    draw(ctx);
}
 
function draw(ctx){
    requestAnimationFrame(function step(){
        drawDial(ctx); //绘制表盘
        drawAllHands(ctx); //绘制时分秒针
        requestAnimationFrame(step);
    });
}
/*绘制时分秒针*/
function drawAllHands(ctx){
    let time = new Date();
 
    let s = time.getSeconds();
    let m = time.getMinutes();
    let h = time.getHours();
 
    let pi = Math.PI;
    let secondAngle = pi / 180 * 6 * s;  //计算出来s针的弧度
    let minuteAngle = pi / 180 * 6 * m + secondAngle / 60;  //计算出来分针的弧度
    let hourAngle = pi / 180 * 30 * h + minuteAngle / 12;  //计算出来时针的弧度
 
    drawHand(hourAngle, 60, 6, "red", ctx);  //绘制时针
    drawHand(minuteAngle, 106, 4, "green", ctx);  //绘制分针
    drawHand(secondAngle, 129, 2, "blue", ctx);  //绘制秒针
}
/*绘制时针、或分针、或秒针
 * 参数1:要绘制的针的角度
 * 参数2:要绘制的针的长度
 * 参数3:要绘制的针的宽度
 * 参数4:要绘制的针的颜色
 * 参数4:ctx
 * */
function drawHand(angle, len, width, color, ctx){
    ctx.save();
    ctx.translate(150, 150); //把坐标轴的远点平移到原来的中心
    ctx.rotate(-Math.PI / 2 + angle);  //旋转坐标轴。 x轴就是针的角度
    ctx.beginPath();
    ctx.moveTo(-4, 0);
    ctx.lineTo(len, 0);  // 沿着x轴绘制针
    ctx.lineWidth = width;
    ctx.strokeStyle = color;
    ctx.lineCap = "round";
    ctx.stroke();
    ctx.closePath();
    ctx.restore();
}
 
/*绘制表盘*/
function drawDial(ctx){
    let pi = Math.PI;
 
    ctx.clearRect(0, 0, 300, 300); //清除所有内容
    ctx.save();
 
    ctx.translate(150, 150); //一定坐标原点到原来的中心
    ctx.beginPath();
    ctx.arc(0, 0, 148, 0, 2 * pi); //绘制圆周
    ctx.stroke();
    ctx.closePath();
 
    for (let i = 0; i < 60; i++){//绘制刻度。
        ctx.save();
        ctx.rotate(-pi / 2 + i * pi / 30);  //旋转坐标轴。坐标轴x的正方形从 向上开始算起
        ctx.beginPath();
        ctx.moveTo(110, 0);
        ctx.lineTo(140, 0);
        ctx.lineWidth = i % 5 ? 2 : 4;
        ctx.strokeStyle = i % 5 ? "blue" : "red";
        ctx.stroke();
        ctx.closePath();
        ctx.restore();
    }
    ctx.restore();
}

学习canvas基础点它 -》 学习 HTML5 Canvas 这一篇文章就够了
做canvas实例点它 -》Canvas环形饼图与手势控制的实现代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容