用于将目标转换成像素点,该方法返回一个像素点数组[{x, y, rgba}]
,xy 为像素点的位置,rgba 为对应颜色。实现逻辑主要通过 canvas getImageData 方法获取像素数据然后遍历找出有颜色的像素点。
// 获取目标对象的像素点,space 用于稀释像素点,值越大返回的像素点越少
function getPixels(target, space = 5) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const viewWidth = window.innerWidth
const viewHeight = window.innerHeight
canvas.width = viewWidth
canvas.height = viewHeight
if (typeof target === 'string') {
// 绘制文字
ctx.font = '150px bold'
ctx.fillStyle = '#fff'
ctx.textBaseline = 'middle'
ctx.textAlign = 'center'
ctx.fillText(target, viewWidth / 2, viewHeight / 2)
} else {
// 绘制图片
ctx.drawImage(
target,
(viewWidth - target.width) / 2,
(viewHeight - target.height) / 2,
target.width,
target.height
)
}
// 获取像素数据
const { data, width, height } = ctx.getImageData(0, 0, viewWidth, viewHeight)
const pixeles = []
// 遍历像素数据,用space减少取到的像素数据
for (let x = 0; x < width; x += space) {
for (let y = 0; y < height; y += space) {
const pos = (y * width + x) * 4 // 每个像素点由 rgba 四个值组成,所以需要乘以4才能得到正确的位置
// 只提取 rgba 中透明度大于0.5的像素,imageData 里 aplha 128等于 rgba 中 alpha 的 0.5
if (data[pos + 3] > 128) {
pixeles.push({
x,
y,
rgba: [data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]
})
}
}
}
return pixeles
}
用于将像素点数据转换成粒子对象,在后面动画帧里会操作粒子对象移动完成动画成像,粒子对象包含 x, y, tx, ty, color, radius 等属性,xy 为粒子当前位置,tx ty 为成像的目标位置。
class Particle {
constructor({ x = 0, y = 0, tx = 0, ty = 0, radius = 2, color = '#F00000' }) {
// 当前坐标
this.x = x
this.y = y
// 目标点坐标
this.tx = tx
this.ty = ty
this.radius = radius
this.color = color
}
draw(ctx) {
ctx.save()
ctx.translate(this.x, this.y)
ctx.fillStyle = this.color
// ctx.fillRect(0, 0, this.radius * 2, this.radius * 2)
ctx.beginPath()
ctx.arc(0, 0, this.radius, 0, Math.PI * 2, true)
ctx.closePath()
ctx.fill()
ctx.restore()
return this
}
通过 requestAnimationFrame 循环绘制画布,遍历所有的粒子对象,通过缓动动画算法操控它们的位置,直到所有粒子移动到目标位置完成成像才取消绘制。实现一个 drawFrame 方法来完成动画的逻辑,该方法接收粒子对象集合以及一个完成动画的回调函数。
// 传入粒子对象绘制动画帧,并接受一个动画结束的回调
function drawFrame(particles, finished) {
const timer = window.requestAnimationFrame(() => {
drawFrame(particles, finished)
})
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 缓动系数
const easing = 0.06
const finishedParticles = particles.filter(particle => {
// 当前坐标和目标点之间的距离
const dx = particle.tx - particle.x
const dy = particle.ty - particle.y
// 速度
let vx = dx * easing
let vy = dy * easing
// 当距离小于0.1表示粒子已完成动画
if (Math.abs(dx) < 0.1 && Math.abs(dy) < 0.1) {
particle.finished = true
particle.x = particle.tx
particle.y = particle.ty
} else {
particle.x += vx
particle.y += vy
}
particle.draw(ctx)
return particle.finished
})
if (finishedParticles.length === particles.length) {
window.cancelAnimationFrame(timer)
finished && finished()
}
}
实现 createParticles 方法创建粒子,对传入的文字提取粒子,返回粒子集合。
// 创建粒子
function createParticles({ text, radius, space }) {
const pixeles = getPixels(text, space)
return pixeles.map(({ x, y, rgba: color }) => {
return new Particle({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
tx: x,
ty: y,
radius,
color: `rgba(${color})`
})
})
}
最后通过组合调用这些函数就能完成粒子动画效果:
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
canvas.width = window.innerWidth
canvas.height = window.innerHeight
const particles = createParticles({ text: 'JS', radius: 2, space: 5 }
drawFrame(particles, () => { console.log('动画已完成') })
在 createParticles 方法里初始粒子的随机位置使用的是基于视图的大小随机的,效果不是很好。可以改用基于圆形分布的随机位置,让效果更加自然。创建一个获取圆形随机位置的方法 getRandomPos,该方法接受一个圆形半径为参数,并返回随机 x,y 坐标。
// 获取圆形的随机分布位置
function getRandomPos(maxRadius = canvas.width / 1.3) {
const radius = Math.sqrt(Math.random()) * maxRadius
const angle = Math.PI * 2 * Math.random()
const x = canvas.width / 2 + Math.cos(angle) * radius
const y = canvas.height / 2 + Math.sin(angle) * radius
return { x,y }
}
// 修改 createParticles 方法中 xy 赋值逻辑
function createParticles (...) {
// ...
const randomPos = getRandomPos()
return { x: randomPos.x, y: randomPos.y }
}
基于圆形随机分布:http://jsrun.net/PkIKp
基于视图大小随机分布:http://jsrun.net/SAwKp