「✍ Canvas」这次我一定要学好!

438 阅读3分钟

每次想系统学习一下canvas,常规流程都是

  1. 看到某个炫酷效果
  2. 兴致勃勃打开mdn
  3. 漫无目的的翻阅5分钟后关掉

于是这次想改变一下战术,从小需求出发,从农村包围城市!

签名

这应该算是很普遍的一个需求了

思路

思路很直接,直接画,会用到下面这堆api :)

  • beginPath 创建一个新的路径
  • lineWidth 设置线段厚度的属性(即线段的宽度)
  • strokeStyle 描述画笔(绘制图形)颜色或者样式的属性,默认为黑色
  • moveTo(x, y) 将一个新的子路径的起始点移动到(x,y)坐标的方法(并不会真正地绘制)
  • lineTo(x, y) 使用直线连接子路径的终点到x,y坐标的方法(并不会真正地绘制)
  • closePath 从当前点到起始点绘制一条直线
  • stroke 实际地绘制出通过 moveTo() 和 lineTo() 方法定义的路径,默认颜色是黑色

代码

const canvas=document.getElementById("canvas")
const ctx=canvas.getContext("2d")
canvas.width = 800
canvas.height = 800

// 样式
ctx.lineWidth = 2;
ctx.strokeStyle = "#fff";

// 获取移动坐标
const getRealCoordinate = (event) => {
  const canvasOffsetLeft = canvas.offsetLeft
  const canvasOffsetTop = canvas.offsetTop
  return {
    x: event.clientX - canvasOffsetLeft,
    y: event.clientY - canvasOffsetTop
  }
}

const handleMouseMove = (event) =>{
  const { x, y } = getRealCoordinate(event)
  ctx.lineTo(x, y);
  ctx.stroke();
}

const handleMouseDown = (event) => {
  ctx.beginPath();
  const { x, y } = getRealCoordinate(event)
  ctx.moveTo(x, y);

  canvas.addEventListener('mousemove', handleMouseMove)
}

canvas.addEventListener('mousedown', handleMouseDown)

 canvas.addEventListener('mouseup', (e)=>{
  canvas.removeEventListener('mousemove', handleMouseMove)
})

刮刮乐

思路

  1. 将canvas绝对定位覆盖再获奖底图上
  2. 监听滑动动作,使用clearRect清除滑动部分的一小块画布即可

代码

view

  <style>
  .box{
    position: relative;
    width: 300px;
    height: 300px;
  }
   /* 获奖底图 */
  .prize{
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    background: coral;
    font: 50px serif
  }
  /* canvas */
  #prize-mask{
   position: absolute;
   top: 0;
   left: 0;
   cursor: pointer;
   z-index: 10;
  }
</style>
<div class="box"">
  <div class="prize">1000万</div>
  <canvas id="prize-mask" />
</div>

js

const canvas = document.querySelector('#prize-mask')
const ctx = canvas.getContext('2d')

// 获取外层宽高并动态设置宽高
const { clientWidth: wrapWidth, clientHeight: wrapHeight} = document.querySelector('.box')
canvas.width = wrapWidth
canvas.height = wrapHeight

const { clientWidth: canvasWidth, clientHeight: canvasHeight } = canvas

const text = '大吉大利'

    // 设置一些canvas的属性
ctx.fillStyle='black'
ctx.fillRect(0,0,canvasWidth,canvasHeight)
ctx.font='50px serif'
ctx.strokeStyle='red'
ctx.strokeText(text, ( canvasWidth - ctx.measureText( text ).width ) / 2, canvasHeight / 2 )   //  文本居中
ctx.save()


const handleMouseMove = (event) =>{
ctx.clearRect(event.offsetX,event.offsetY,20,20);
}

const handleMouseDown = () => {
  canvas.addEventListener('mousemove', handleMouseMove)
}

canvas.addEventListener('mousedown', handleMouseDown)

 canvas.addEventListener('mouseup', (e)=>{
  canvas.removeEventListener('mousemove', handleMouseMove)
})

效果

guaguale.gif

注意点

给canvas设置宽高,不要在class中设置,会造成拉伸/错乱等乱七八糟的问题

画个坐标网格

代码

const canvas=document.getElementById("canvas")
const context=canvas.getContext("2d")
const canvasWidth=800
const canvasHeight=800
const cap=50 // 间隔
const colNum=Math.ceil(canvasWidth/cap)-1
const rowNum=Math.ceil(canvasHeight/cap)-1
let Grids=[]
const lineWidth=2
canvas.width=800
canvas.height=800

//建立网格
function initGrid(cap,width,height,lineWidth){
  const colNum=Math.ceil(width/cap)-1
  const rowNum=Math.ceil(height/cap)-1
  for(let i=1;i<=colNum;i++){
    Grids.push([[cap*i-1, 0,lineWidth,colNum*cap],[i*cap,cap*i-1,colNum*cap+5,"top","center"]]) 
  }
  for(let i=1;i<=rowNum;i++){
    Grids.push([[ 0,cap*i-1,rowNum*cap,lineWidth],[i*cap,rowNum*cap+5,cap*i-1,"middle","left"]])
  }
}

initGrid(cap,canvasWidth,canvasHeight,lineWidth);

function createGrid(){
  context.fillStyle = 'green';
  context.font="24px Arial"
  Grids.forEach((grid)=>{
    context.textAlign=grid[1][4]
     context.textBaseline=grid[1][3]
     context.fillRect(grid[0][0],grid[0][1], grid[0][2], grid[0][3])
     context.fillText(grid[1][0],grid[1][1], grid[1][2]);   
  })
}
createGrid()

模拟下雪

思路

思路比较清晰,就是生成若干个雪花,然后一帧一帧遍历、移动

代码

定义好变量

let W = window.innerWidth;
let H = window.innerHeight;
canvas.width = W;
canvas.height = H;

let flakesCount = 100; // 雪花个数
let flakes = []; // 雪花集合

let angle = 0

初始化雪花数组,定义好每个雪花的初始位置

// 定义每个雪花的位置
for (let i = 0; i < flakesCount; i++) {
    flakes.push({
      x: Math.random() * W, // 雪花x轴位置
      y: Math.random() * H, // 雪花y轴位置
      r: Math.random() * 5 , // 雪花的半径
      d: Math.random() + 1 // 雪花密度,用于控制下落速度
    });
}

某一帧的移动

 function moveFlakes() {
    angle += 0.01;
    for (let i = 0; i < flakesCount; i++) {
      let flake = flakes[i];
      flake.y += Math.pow(flake.d, 2) ; // 速度和密度实际上不是平方的关系,这么些是为了效果更加错落有致
      flake.x += Math.sin(angle) * 2;

      // 超出界限了,重新初始化该雪花
      if (flake.y > H) {
        flakes[i] = { x: Math.random() * W, y: 0, r: flake.r, d: flake.d };
      }
    }
  }

定义绘制某一帧的function, 用requestAnimationFrame代替setInterval

function drawFlakes () {
 // 画出静态的雪花 
 ctx.clearRect(0, 0, W, H);
 ctx.fillStyle = '#fff';
 // ctx.strokeStyle = '#fff';
 ctx.beginPath();
 for (let i = 0; i < flakesCount; i++) {
   let flake = flakes[i];
   ctx.moveTo(flake.x, flake.y);
   ctx.arc(flake.x, flake.y, flake.r, 0, Math.PI * 2, true);
 }

 ctx.fill();
 // ctx.stroke();

  moveFlakes()
  window.requestAnimationFrame(drawFlakes);
}

最后执行绘制

drawFlakes()

一些API注意点

beginPath 和 closePath

beginPath - 开始一条新的路径,或者说重置一条新的路径

e.g

var ctx = document. getElementById ( 'canvas' ) . getContext ( '2d' ) ; 
ctx. beginPath ( ) ; 
ctx. moveTo ( 100 , 20 ) ; 
ctx. lineTo ( 200 , 20 ) ; 
ctx. strokeStyle = 'white' ; 
ctx. stroke ( ) ; 
ctx.beginPath() // important
ctx.moveTo(100, 40)
ctx. lineTo ( 200 , 40 ) 
ctx. strokeStyle = 'red' ; 
ctx. stroke ( ) ;

上面这段代码会绘制出一条白线和一条红线,如果没有第二个beginPath, 最终绘制出来的就会是两条红线

closePath - 跟beginPath没有关系,这个api只是为了闭合一段路径,即从当前点绘制一条线段到路径起点