小知识,大挑战!本文正在参与“程序员必备小知识”创作活动
本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金
前言
运动和变化在不断地更新着世界,
就像不间断的时间总在更新无穷无尽的岁月的持续一样。
——马尔库·奥勒利乌斯
介绍
本期我将给大家讲解一个新作品——万花筒画笔, 相信我们很多人小的时候都玩过万花筒。它通过长长的一个筒子,通过一个镜片,我们就可以看到里边那般五彩缤纷的世界。随着我们转动,它在我们的眼前呢就呈现出千奇百怪的图像,所以,今天就想能不能通过代码绘制让这个时间再度在计算机中呈现,就想到了这款画笔。
如上图所示,它会不断改变颜色,随着你输入设备的拖移也随即变化出相应的形态。而且它不借助任何第三方库,仅仅使用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的使用,相信大家也有了自己的新想法,现在仅仅是个万花筒画笔,我们也可以生成三角形圆形等等图形做个真实的万花筒也可以哦,或者是多重福字门帘也可以再此基础上去实现,还等什么赶紧发挥自己的创意吧~