Canvas如何做个绚丽的万花筒画笔
前言
运动和变化在不断地更新着世界,
就像不间断的时间总在更新无穷无尽的岁月的持续一样。
——马尔库·奥勒利乌斯
介绍
本期我将给大家讲解一个新作品——万花筒画笔, 相信我们很多人小的时候都玩过万花筒。它通过长长的一个筒子,通过一个镜片,我们就可以看到里边那般五彩缤纷的世界。随着我们转动,它在我们的眼前呢就呈现出千奇百怪的图像,所以,今天就想能不能通过代码绘制让这个时间再度在计算机中呈现,就想到了这款画笔。
如上图所示,它会不断改变颜色,随着你输入设备的拖移也随即变化出相应的形态。而且它不借助任何第三方库,仅仅使用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最基础的绘制操作了,太过基础就不做过多说明了。
看,我们现在就可以在画布中绘制东西了,但是万花筒是变化的,我们不期望画过的线段一直保留下去,而且慢慢消失掉,接下来,我们就要不断的重绘他了。
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一层层的覆盖白色透明层,想达渐渐消失的效果,另外,这里我们可以让笔触颜色渐渐的发生变化。
但是问题来了,我们发现绘制之后的虽然颜色淡了,但是久久不能消失,他的路径轨迹依然会让人看到非常的丑。
所以,我们还要改进一下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去做了混合重叠对比直接消除了颜色轨迹的色值。
现在我们就实现了想要的结果,一款万花筒画笔——在线演示
结语
通过这个案例,一些绘制偏移技巧,色值变化,globalCompositeOperation的使用,相信大家也有了自己的新想法,现在仅仅是个万花筒画笔,我们也可以生成三角形圆形等等图形做个真实的万花筒也可以哦,或者是多重福字门帘也可以再此基础上去实现,还等什么赶紧发挥自己的创意吧
作者:jsmask
链接:https://juejin.cn/post/7017995850756390920