canvas实现粒子跟随鼠标动画
canvas是前端绘制图形的好帮手,不少大牛的博客也会精心的用canvas绘制炫酷的背景。图形与可视化界面就是前端最好的名片,泡妞之前尚且知道要梳个靓头精心打扮,而canvas就是你装点门面与大牛平齐,脱颖而出的绝佳神器。
文章目录
前言
- 本文意指通过面相对象的方式更优雅的组织canvas相关代码,而不是讲解canvas相关的api
一、从创建一个canvas元素开始
我们假设页面中有一个canvas元素,我们通过一个类的实例来让其产生黑色的幕布以及鼠标滑过粒子的能力。
第一步:初始化一个黑色幕布的canvas,定义类Draw
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
class Draw {
public ctx: CanvasRenderingContext2D
public r: number
constructor(public canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d')
this.ctx.fillRect(0, 0, 300, 150) // 黑色填充
}
}
new Draw(canvas)
第二步:创建绘制粒子的类,定义类Particle
class Particle {
public ctx: CanvasRenderingContext2D
constructor(public x: number, public y: number) {
this.x = x;
this.y = y;
}
draw(ctx: CanvasRenderingContext2D, r: number) {
if (r < 0) return
this.ctx = ctx
this.r = r
ctx.fillStyle = "rgba(255,255,255, 1)";
ctx.shadowBlur = r * 2 / 10;
ctx.beginPath();
ctx.arc(this.x, this.y, r, 0, 2 * Math.PI, false);
ctx.closePath();
ctx.fill();
return this
}
}
这里我们定义了Particle类,其中x,y,r用于绘制粒子的坐标点位以及圆的半径,draw是绘制方法的实现,其内部运用了canva绘制圆形的方法不做讲解。
第三步:从创建一个Particle元素开始
现在Draw结合Particle在黑色幕布中绘制一个粒子,给Draw添加一个createCanvas方法
// In the Draw
createCanvas(x: number, y: number) {
const particle = new Particle(x, y).draw(this.ctx, 5)
}
createCanvas方法内生产了一个粒子,这里我们把Context2D传给了Particle对象,在Particle内部的draw方法内做具体的实现。这么做的好处在于我们不单单可以有Particle,也可以有Square,Triangle,每一个类的内部去做具体的实现。
// In the Draw
constructor(public canvas: HTMLCanvasElement) {
/...和之前代码一样/
this.createCanvas(50, 50)
}
createCanvas(x: number, y: number) {
const particle = new Particle(x, y).draw(this.ctx, 5)
return this
}
简单看看效果,在幕布坐标50,50绘制一个半径为5粒子
二、事件绑定产生粒子
聪明如你应该可以想到,只要给canvas绑定鼠标事件,就能通过事件的坐标生产粒子
// In the Draw
constructor(public canvas: HTMLCanvasElement) {
/...和之前代码一样/
const throttleMousemove= throttle(this.handleMousemove.bind(this), 100)
this.canvas.addEventListener('mousemove', throttleMousemove)
}
handleMousemove(event: MouseEvent) {
const { offsetX, offsetY } = event;
this.createCanvas(offsetX, offsetY)
}
这里的throttle只是一个普通的节流函数
看看效果
事实上你也可以考虑通过判断鼠标的位置与上一次的坐标位置,达到一定的间距之后再产生粒子,这样就不用节流函数了,也不会因为鼠标滑动的速度滑动的快慢而产生间距不同粒子。
三、粒子渐变动效
何为渐变?
渐变是指一个值到另外一个值不断变化的过程,而这个值必然是数值。
回过头看第一张GIF,这里我们渐变的值是粒子的半径,以及颜色,其中颜色随半径的增大而变亮。
所以可以得出结论:Particle.draw()方法里的半径r是唯一需要变化的状态state,而颜色依赖这个state的变化计算即可。
// In the Particle
// 假设外部调用draw方法传入r值不断变化
// 新增public scaleFlag
public scaleFlag: boolean = false
draw(ctx: CanvasRenderingContext2D, r: number) {
/..其他和之前代码一样/
ctx.fillStyle = "rgba(255,255,255," + r / 10 + ")";
ctx.shadowBlur = r * 2 / 10;
ctx.arc(this.x, this.y, r, 0, 2 * Math.PI, false);
}
scaleFlag辅助确认放大还是缩小
定义方法vitalityParticle用于渐变Particle
// In the Particle
vitalityParticle() {
if (!this.scaleFlag) {
this.r += 2
} else {
this.r -= 2
}
this.draw(this.ctx, this.r)
if (this.r === 10) {
this.scaleFlag = true
}
return this
}
vitalityParticle方法不断修改r值以达到渐变的目的,让我们来演示一下
// In the Draw
constructor(public canvas: HTMLCanvasElement) {
/...上面和之前的代码一样/
const particle = this.createCanvas(50, 50).draw(this.ctx, 0)
let setp = 10;
const timer = setInterval(() => {
this.ctx.clearRect(0, 0, 300, 150)
this.ctx.fillStyle = '#000'
this.ctx.fillRect(0, 0, 300, 150)
particle.vitalityParticle()
setp--
if (setp === 0) {
clearInterval(timer)
}
}, 200)
}
在幕布坐标50,50创建了一个半径为0的粒子,通过定时器去改变它
四、粒子渐变与事件结合
现在通过定时器驱动以达到时间与运动的结合,上面的例子只是单个渐变,对于所有createCanvas产生的particle我们需要保存在数组中,通过迭代数组的方式运动其每个particle实例。
// In the Draw
public canvasArray: Particle[] = []
public setp: number = 10;
public timer: NodeJS.Timer | null = null;
createCanvas(x: number, y: number) {
const particle = new Particle(x, y)
.draw(this.ctx, 0);
this.generator(particle)
return particle
}
generator(particle: Particle) {
this.canvasArray.push(particle)
this.setp = 10
if (this.timer) return
this.timer = setInterval(() => {
this.ctx.clearRect(0, 0, 300, 150)
this.ctx.fillStyle = '#000'
this.ctx.fillRect(0, 0, 300, 150)
for (const particle of this.canvasArray) {
particle.vitalityParticle()
}
this.setp--
if (this.setp === 0) {
clearInterval(this.timer)
this.timer = null
}
}, 100)
}
新增了generator方法,并将所有创建的particle实例都push到数组中,在定时器的每一次调度中迭代这个数组,执行每一个particle的vitalityParticle方法。
手速快的话还有可以有这种效果
总结
以上就是今天要讲的内容,本文仅仅简单介绍了canvas的使用,而canvas的实际开发过程需要开发人员灵巧设计实现,通过class或function封装调用。
一个优秀的开发往往还需要考虑内存占用的问题,比如上面的数组不断的push particle,数组越来越大是不是要考虑清除。另外如果通过shift清除数组中已完成运动周期的particle是不是就等于释放了内存也是我们要考虑的问题。