Canvas如何做个绚丽的万花筒画笔

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

前言

运动和变化在不断地更新着世界,

就像不间断的时间总在更新无穷无尽的岁月的持续一样。

——马尔库·奥勒利乌斯

介绍

本期我将给大家讲解一个新作品——万花筒画笔, 相信我们很多人小的时候都玩过万花筒。它通过长长的一个筒子,通过一个镜片,我们就可以看到里边那般五彩缤纷的世界。随着我们转动,它在我们的眼前呢就呈现出千奇百怪的图像,所以,今天就想能不能通过代码绘制让这个时间再度在计算机中呈现,就想到了这款画笔。

VID_20211012_084207.gif

如上图所示,它会不断改变颜色,随着你输入设备的拖移也随即变化出相应的形态。而且它不借助任何第三方库,仅仅使用canvas api去完成绘制,接下来,我们就要从基础结构,事件绑定,绘制渲染等三个方面去讲解它。

正文

1.基础结构

我们本次还是用vite去构建,先看下HTML结构吧

<head>
    <!-- .... -->
	<style>
        body{
            width: 100%;
            height: 100vh;
        }
        canvas{
            width: 100%;
            height: 100%;         
        }
    </style>
</head>
<body>
    <canvas id="canvas"></canvas>
    <script type="module" src="./app.js"></script>
</body>
复制代码

很简单,我们就放一个canvas标签和引入app.js作为主逻辑就行了。因为本次也是简短,就把这次的逻辑都写到了app.js里,不做单独拆分了。

/*app.js*/
class Application {
  constructor() {
    this.canvas = null;                         // 画布
    this.ctx = null;                            // 环境
    this.w = 0;                                 // 画布宽
    this.h = 0;                                 // 画布高
    this.n = 18;                                // 万花筒一次画出的线条数量
    this.bgColor = "rgba(255,255,255,1)";       // 背景颜色
    this.penColor = 0;                          // 线条颜色
    this.lineCap = "round"                      // 线帽类型
    this.state = false;                         // 当前按下状态,false是没按下,true是按下
    this.point = null;                          // 当前按下的坐标点
    this.rotate = 0;                            // 万花筒每条线的偏移角度
    this.init();
  }
  init() {
    this.canvas = document.getElementById("canvas");
    this.ctx = this.canvas.getContext("2d");
    window.addEventListener("resize", this.reset.bind(this));
    this.penColor = ~~(Math.random() * 360)
    this.rotate = 360 / this.n * Math.PI / 180;
    this.reset();
    this.bindEvent();
    this.step();
  }
  reset() {
    // 重置
    this.w = this.canvas.width = this.ctx.width = window.innerWidth;
    this.h = this.canvas.height = this.ctx.height = window.innerHeight;
    this.penColor = ~~(Math.random() * 360)
    this.clear();
  }
  clear() {
    // 清空画布
    this.ctx.clearRect(0, 0, this.w, this.h);
  }
  _mouseDown(e) {
     // 输入设备按下
  }
  _mouseUp() {
     // 输入设备抬起
  }
  _mouseMove(e) {
      // 输入设备移动
  }
  bindEvent() {
     // 绑定事件
  }
  drawLine(x, y) {
     // 绘制线条
  }
  step() {
     // 帧变化
  }
}

window.onload = new Application();
复制代码

基础结构跟之前的案例也大体相同,注释应该能看明白。

我们这里主要讲解一下这几点:

  • penColor:这是我们笔触的当前颜色,他是一个数值类型,因为每次生成要随机一个值给它,而且后面在step实时变化中,将会让它也不断增益变化,达到改变颜色的效果,所以,在绘制的时候,将通过hsl去做颜色控制,而penColor充当了色相这一栏的值,即hsl(色相,饱和度,亮度)。
  • rotate:初始化就会执行计算,算出后面笔触分裂出的每条线段偏移的基本角度。
  • reset:每次屏幕的宽高发生变化都期望重置一下整个环境。

2.绑定数据

我们先分析一下,我的场景可能在电脑上也可能在手机上,所以,分别做两套事件,即轻触和鼠标。

bindEvent() {
    const {canvas} = this;
    if (navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i)) {
        canvas.addEventListener("touchstart", this._mouseDown.bind(this), false);
        canvas.addEventListener("touchmove", this._mouseMove.bind(this), false);
        canvas.addEventListener("touchend", this._mouseUp.bind(this), false);
    } else {
        canvas.addEventListener("mousedown", this._mouseDown.bind(this), false);
        canvas.addEventListener("mousemove", this._mouseMove.bind(this), false)
        canvas.addEventListener("mouseup", this._mouseUp.bind(this), false)
        canvas.addEventListener("mouseout", this._mouseUp.bind(this), false)
    }
}
复制代码

他们都会分为按下,移动,抬起三个事件,然后我们来写对应的事件吧

_mouseDown(e) {
    let {clientX, clientY, touches} = e;
    let x = clientX,
        y = clientY;
    if (touches) {
        x = touches[0].clientX;
        y = touches[0].clientY;
    }
    this.state = true
    this.point = {x,y}
}
_mouseMove(e) {
    if (!this.state) return;
    let {clientX, clientY, touches} = e;
    let x = clientX,
        y = clientY;
    if (touches) {
        x = touches[0].clientX;
        y = touches[0].clientY;
    }
    this.drawLine(x, y)
    this.point = {x,y}
}
_mouseUp() {
    this.state = false
}
复制代码
  • 按下:将state状态变成true,并且记录一下初始点
  • 移动:只有state状态变为true时才会向下执行,拿到点先在drawLine去绘制线条,然后在用point去保存坐标,这样下次就能用结束的坐标开始绘制。
  • 抬起:将state状态变成false

3.绘制渲染

我们在刚刚的输入设备移动事件中,可以拿到要移动到x和y坐标值,接下来就用drawLine先去做绘制。

drawLine(x, y) {
    const {w, h, ctx, penColor, point, rotate, n, lineCap} = this;
    ctx.lineWidth = 10;
    ctx.strokeStyle = `hsl(${~~penColor}, 100% , 50%)`;
    ctx.lineCap = lineCap;
    ctx.lineJoin = "round";
    ctx.shadowColor = "rgba(255,255,255,.1)";
    ctx.shadowBlur = 1;
    for (let i = 0; i < n; i++) {
        ctx.save();
        ctx.translate(w / 2, h / 2);
        ctx.rotate(rotate * i);
        if ((n % 2 === 0) && i % 2 !== 0) {
            ctx.scale(1, -1);
        }
        ctx.beginPath();
        ctx.moveTo(point.x - w / 2, point.y - h / 2);
        ctx.lineTo(x - w / 2, y - h / 2);
        ctx.stroke();
        ctx.restore();
    }
}
复制代码

其实,说来也简单,最关键的是,找准坐标原点,一原点为中心去绘制你要绘制的线条。而且他是怎么分裂的呢,看了代码就会发现,我用了ctx.rotate方法做了旋转,因为最早我们在初始化中也有个rotate计算了他的偏移角度,每条先根据原点和当前下标i做计算应当偏移到那个角度上做绘制,而且当总线条数是偶数时,每隔一条让他在外周反正形成对称。剩下的就是canvas api最基础的绘制操作了,太过基础就不做过多说明了。

微信截图_20211012094300.png

看,我们现在就可以在画布中绘制东西了,但是万花筒是变化的,我们不期望画过的线段一直保留下去,而且慢慢消失掉,接下来,我们就要不断的重绘他了。

step() {
    window.requestAnimationFrame(this.step.bind(this))
    const {ctx, w, h} = this;
    ctx.fillStyle = "rgba(255,255,255,.008)"
    ctx.fillRect(0, 0, w, h);
    this.penColor += 0.05;
    this.penColor %= 360;
}
复制代码

我们通过fillRect一层层的覆盖白色透明层,想达渐渐消失的效果,另外,这里我们可以让笔触颜色渐渐的发生变化。

微信截图_20211012095003.png

但是问题来了,我们发现绘制之后的虽然颜色淡了,但是久久不能消失,他的路径轨迹依然会让人看到非常的丑。

所以,我们还要改进一下step方法。

step() {
    window.requestAnimationFrame(this.step.bind(this))
    const {ctx, w, h} = this;
    ctx.globalCompositeOperation = "lighter";
    ctx.fillStyle = "rgba(255,255,255,.008)"
    ctx.fillRect(0, 0, w, h);
    ctx.globalCompositeOperation = "source-over";
    this.penColor += 0.05;
    this.penColor %= 360;
}
复制代码

相信,大家已经明白了,这里直接用了globalCompositeOperation去做了混合重叠对比直接消除了颜色轨迹的色值。

现在我们就实现了想要的结果,一款万花筒画笔——在线演示

VID_20211012_084207.gif

结语

通过这个案例,一些绘制偏移技巧,色值变化,globalCompositeOperation的使用,相信大家也有了自己的新想法,现在仅仅是个万花筒画笔,我们也可以生成三角形圆形等等图形做个真实的万花筒也可以哦,或者是多重福字门帘也可以再此基础上去实现,还等什么赶紧发挥自己的创意吧~

猜你喜欢

转载自juejin.im/post/7017995850756390920