canvas入门案例

写在最前:文章转自掘金

1. canvas实现时钟转动

实现以下效果,分为几步:

  • 找到canvas的中心,画出表心,以及表框
  • 获取当前时间,并根据时间画出时针,分针,秒针,还有刻度
  • 使用定时器,每过一秒获取新的时间,并重新绘图,达到时钟转动的效果


1.1 表心,表框

画表心,表框有两个知识点:

  • 找到canvas的中心位置
  • 绘制圆形
// html 
<canvas id="canvas" width="600" height="600"></canvas>

// js
const c = document.querySelector("#canvas")
const ctx = c.getContext('2d')
ctx.translate(300,300) // 设置中心,此时300,300变成了坐标的0,0
ctx.beginPath() // 画大圆
ctx.arc(0,0,100,0,2*Math.PI) // 画圆线使用arc(中心x,中心y,半径,其实角度,结束角度)
ctx.stroke() // 执行画线段的操作stroke
ctx.closePath() 

ctx.beginPath() // 画小圆
ctx.arc(0,0,5,0,2*Math.PI)
ctx.stroke() // 执行画线段的操作stroke
ctx.closePath()

1.2 时针,分针,秒针

画这三个指针,有两个知识点:

  • 根据当前的时,分,秒去计算角度
  • 在计算好的角度上画出时针,分针,秒针

例如当前是3点,那么时针就应该以12点为起点,顺时针旋转 2*Math.PI/12*3=90°,分针和秒针也是同样的道理,只不过跟时针不同的是比例问题而已,因为时在表上有12份,而分针和秒针都是60份

我算出了90°后,那我们怎么画出时针呢?我们可以使用moveTo和lineTo去画线段。至于90°我们只需要将x轴顺时针旋转90°,然后再画出这条线段,那就得到了指定角度的指针了,但是上面说了,是要以12点为起始点,我们的默认轴x轴是水平的所以我们时分秒针算出角度后,每次都要减去90°。下面通过图演示一下。


当我画完时针,再想画分针时,x轴已经在我画分针的时候偏转了,这时候画出的分针是不准的。save和restore就派上用场了,save是把ctx当前的转台打包压入栈中,restore是取出栈顶部状态并赋值给ctx,sava可多次,但是restore取状态的次数必须等于save次数

懂得上面所说的,身下画刻度了,起始刻度的道理跟时分秒针道理一样,只不过刻度是死的,不需要计算规则画出60个小刻度,和12个大刻度就行了。

// 获取当前时分秒
let time =new Date()
let hour = time.getHours() % 12
let min = time.getMinutes()
let sec = time.getSeconds()

// 绘制时针
ctx.rotate(2*Math.PI / 12 * hour + 2*Math.PI/12*(min/60)-Math.PI/2) // 时针角度
ctx.beginPath()
ctx.moveTo(-10,0) // 线条起点
ctx.lineTo(40,0) // 线条终点
ctx.lineWidth = 3
ctx.stroke()
ctx.closePath() 
ctx.restore() //恢复上一次sava的状态
ctx.save() //恢复完再保存一次

// 分针
ctx.rotate(2*Math.PI/60*min-2*Math.PI/2)
ctx.beginPath()
ctx.moveTo(-10,0)
ctx.lineTo(60,0)
ctx.lineWidth = 2
ctx.strokeStyle = '#999'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

// 分针
ctx.rotate(2*Math.PI/60*sec-2*Math.PI/2)
ctx.beginPath()
ctx.moveTo(-10,0)
ctx.lineTo(80,0)
ctx.lineWidth = 1
ctx.strokeStyle = '#dc0000'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

// 绘制刻度
ctx.lineWidth = 1
for(let i = 0;i<60;i++){
  ctx.rotate(2*Math.PI/60)
  ctx.beginPath()
  ctx.moveTo(90,0)
  ctx.lineTo(100,0)
  ctx.stroke()
  ctx.closePath()
}
ctx.restore()
ctx.save()
ctx.lineWidth = 2
for(let i = 0;i<12;i++){
  ctx.rotate(2*Math.PI/12)
  ctx.beginPath()
  ctx.moveTo(86,0)
  ctx.lineTo(100,0)
  ctx.stroke()
  ctx.closePath()
}
ctx.restore()

最后一步就是更新视图,使时钟转起来,第一想到的肯定是定时器,但注意一个问题,每次更新视图的时候都要把上一次的画布清除,再重新画视图。
最终代码:

const c = document.querySelector("#canvas")
  const ctx = c.getContext('2d')
  setInterval(_ => {
    ctx.save() // 保存初始状态
    ctx.clearRect(0,0,600,600) //清空矩阵中的内容
    ctx.translate(300, 300) // 设置中心,此时300,300变成了坐标的0,0
    ctx.save() // 把状态保存起来

    ctx.beginPath() // 画大圆
    ctx.arc(0, 0, 100, 0, 2 * Math.PI) // 画圆线使用arc(中心x,中心y,半径,其实角度,结束角度)
    ctx.stroke() // 执行画线段的操作stroke
    ctx.closePath()

    ctx.beginPath() // 画小圆
    ctx.arc(0, 0, 5, 0, 2 * Math.PI)
    ctx.stroke() // 执行画线段的操作stroke
    ctx.closePath()

    // 获取当前时分秒
    let time = new Date()
    let hour = time.getHours() % 12
    let min = time.getMinutes()
    let sec = time.getSeconds()

    // 绘制时针
    ctx.rotate(2 * Math.PI / 12 * hour + 2 * Math.PI / 12 * (min / 60) - Math.PI / 2) // 时针角度
    ctx.beginPath()
    ctx.moveTo(-10, 0) // 线条起点
    ctx.lineTo(40, 0) // 线条终点
    ctx.lineWidth = 3
    ctx.stroke()
    ctx.closePath()
    ctx.restore() //恢复上一次sava的状态
    ctx.save() //恢复完再保存一次

    // 分针
    ctx.rotate(2 * Math.PI / 60 * min - 2 * Math.PI / 2)
    ctx.beginPath()
    ctx.moveTo(-10, 0)
    ctx.lineTo(60, 0)
    ctx.lineWidth = 2
    ctx.strokeStyle = '#999'
    ctx.stroke()
    ctx.closePath()
    ctx.restore()
    ctx.save()

    // 分针
    ctx.rotate(2 * Math.PI / 60 * sec - 2 * Math.PI / 2)
    ctx.beginPath()
    ctx.moveTo(-10, 0)
    ctx.lineTo(80, 0)
    ctx.lineWidth = 1
    ctx.strokeStyle = '#dc0000'
    ctx.stroke()
    ctx.closePath()
    ctx.restore()
    ctx.save()

    // 绘制刻度
    ctx.lineWidth = 1
    for (let i = 0; i < 60; i++) {
      ctx.rotate(2 * Math.PI / 60)
      ctx.beginPath()
      ctx.moveTo(90, 0)
      ctx.lineTo(100, 0)
      ctx.stroke()
      ctx.closePath()
    }
    ctx.restore()
    ctx.save()
    ctx.lineWidth = 2
    for (let i = 0; i < 12; i++) {
      ctx.rotate(2 * Math.PI / 12)
      ctx.beginPath()
      ctx.moveTo(86, 0)
      ctx.lineTo(100, 0)
      ctx.stroke()
      ctx.closePath()
    }
    ctx.restore()
    ctx.restore() // 恢复初始状态
  }, 1000)

2. canvas实现刮刮卡

思路是这样的:

  • 底下答案是一个div,顶部灰皮是一个canvas,一开始盖住div
  • 鼠标事件,点击并移动时,鼠标经过的路径都画圆形开路,并设置globalCompositeOperationdestination-out,使鼠标经过的路径变成透明,下方答案自然显示。

关于fill这个方法,其实是对标strokefill是把图形填充,stroke只是画出边框线。

const c = document.querySelector("#canvas")
  const ctx = c.getContext('2d')

  // 设置填充颜色
  ctx.fillStyle = 'darkgray'
  // 填充矩形(起始X,起始Y,终点X,终点Y)
  ctx.fillRect(0, 0, 400, 100)
  ctx.fillStyle = '#fff'

  //绘制填充文字
  ctx.fillText('刮刮卡', 180, 50)

  let isDraw = false
  canvas.onmousedown = function () { isDraw = true }
  canvas.onmousemove = function (e) {
    if (!isDraw) return
    // 计算鼠标在canvas里的位置
    const x = e.pageX - canvas.offsetLeft
    const y = e.pageY - canvas.offsetTop
    // 设置globalCompositeOperation
    ctx.globalCompositeOperation = 'destination-out'
    // 画图
    ctx.beginPath()
    ctx.arc(x, y, 10, 0, 2 * Math.PI)
    // 填充圆
    ctx.fill()
    ctx.closePath()
  }
  canvas.onmouseup = function () {
    isDraw = false
  }

3. canvas实现画板和保存

其实很简单,难点有以下几点:

  • 鼠标拖拽画正方形和圆形
  • 画完一个保存画布,下次再画的时候叠加
  • 保存图片

第一点,只需要计算出鼠标点击的点坐标,以及鼠标的当前坐标,就可以计算了,矩形长宽计算:x - beginX, y - beginY,圆形则要利用勾股定理:Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY))
第二点,则要利用canvasgetImageDataputImageData方法
第三点,思路是将canvas生成图片链接,并赋值给具有下载功能的a标签,并主动点击a标签进行图片下载

<template>
  <div>
    <div style="margin-bottom: 10px; display: flex; align-items: center">
      <el-button @click="changeType('huabi')" type="primary">画笔</el-button>
      <el-button @click="changeType('rect')" type="success">正方形</el-button>
      <el-button
        @click="changeType('arc')"
        type="warning"
        style="margin-right: 10px"
        >圆形</el-button
      >
      <div>颜色:</div>
      <el-color-picker v-model="color"></el-color-picker>
      <el-button @click="clear">清空</el-button>
      <el-button @click="saveImg">保存</el-button>
    </div>
    <canvas
      id="canvas"
      width="800"
      height="400"
      @mousedown="canvasDown"
      @mousemove="canvasMove"
      @mouseout="canvasUp"
      @mouseup="canvasUp"
    >
    </canvas>
  </div>
</template>

<script>
export default {
  data() {
    return {
      type: "huabi",
      isDraw: false,
      canvasDom: null,
      ctx: null,
      beginX: 0,
      beginY: 0,
      color: "#000",
      imageData: null,
    };
  },
  mounted() {
    this.canvasDom = document.getElementById("canvas");
    this.ctx = this.canvasDom.getContext("2d");
  },
  methods: {
    changeType(type) {
      this.type = type;
    },
    canvasDown(e) {
      this.isDraw = true;
      const canvas = this.canvasDom;
      this.beginX = e.pageX - canvas.offsetLeft;
      this.beginY = e.pageY - canvas.offsetTop;
    },
    canvasMove(e) {
      if (!this.isDraw) return;
      const canvas = this.canvasDom;
      const ctx = this.ctx;
      const x = e.pageX - canvas.offsetLeft;
      const y = e.pageY - canvas.offsetTop;
      this[`${this.type}Fn`](ctx, x, y);
    },
    canvasUp() {
      this.imageData = this.ctx.getImageData(0, 0, 800, 400);
      this.isDraw = false;
    },
    huabiFn(ctx, x, y) {
      ctx.beginPath();
      ctx.arc(x, y, 5, 0, 2 * Math.PI);
      ctx.fillStyle = this.color;
      ctx.fill();
      ctx.closePath();
    },
    rectFn(ctx, x, y) {
      const beginX = this.beginX;
      const beginY = this.beginY;
      ctx.clearRect(0, 0, 800, 400);
      this.imageData && ctx.putImageData(this.imageData, 0, 0, 0, 0, 800, 400);
      ctx.beginPath();
      ctx.strokeStyle = this.color;
      ctx.rect(beginX, beginY, x - beginX, y - beginY);
      ctx.stroke();
      ctx.closePath();
    },
    arcFn(ctx, x, y) {
      const beginX = this.beginX;
      const beginY = this.beginY;
      this.isDraw && ctx.clearRect(0, 0, 800, 400);
      this.imageData && ctx.putImageData(this.imageData, 0, 0, 0, 0, 800, 400);
      ctx.beginPath();
      ctx.strokeStyle = this.color;
      ctx.arc(
        beginX,
        beginY,
        Math.round(
          Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY))
        ),
        0,
        2 * Math.PI
      );
      ctx.stroke();
      ctx.closePath();
    },
    saveImg() {
      const url = this.canvasDom.toDataURL();
      const a = document.createElement("a");
      a.download = "sunshine";
      a.href = url;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
    },
    clear() {
        this.imageData = null
        this.ctx.clearRect(0, 0, 800, 400)
    }
  },
};
</script>

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

推荐阅读更多精彩内容