大概<妇联 4>上映的时候, google 也上线了一个无限手套的特效(点此链接再点屏幕右侧的手套). 搜索结果中的条目, 随机的一半机会随风消逝, 同时附加一些屏幕滚动以及搜索总数目的变化. 除了敬佩还是敬佩! 从那时就有了复刻一下这个效果的想法.
简单考察了一下, 控制台打出了html2canvas
的 log, 也指明了主要技术方向. 其他方面, 由于有随机性加持, 也没看出什么端倪, 只好自己想办法.
html2canvas
转为canvas
或至少为图像数据canvas
完成于是主要问题出现...
canvas
的 api 极其底层, 这里也不打算使用第三方库来省事. 于是最容易想到的方案就是: 暴力渲染. 经查, getImageData
和putImageData
两个 API 可以实现对图像的截取以及填充. 那么剩下的步骤就简单了
这里有一个问题: 所有动画效果都无法超出 canvas 的范围. 不过, 先来实现它吧.
particalize()
切割图片为粒子的集合function particalize(ctx, width = 2, height = 2) {
let canvas = ctx.canvas
let particals = []
let cols = (rows = 0)
let wholeImage = ctx.getImageData(0, 0, canvas.width, canvas.height)
for (let x = 0; x < canvas.width; x += width) {
cols += 1
for (let y = 0; y < canvas.height; y += height) {
rows += 1
// using getImageData() for every partical, slower
// let imgData = ctx.getImageData(x, y, width, height)
// if (imgData.data[3] === 0) {
// continue // ignore transparent particals
// }
let data = clapData(x, y, width, height, canvas.width, canvas.height, wholeImage.data)
if (data[3] === 0) {
continue // ignore transparent particals
}
let imgData = new ImageData(data, width, height)
particals.push([x, y, imgData, rows, cols])
}
}
return [particals, cols, rows]
}
animate()
计算位置并回填粒子实现动画let currentFrame = 0
let endFrame = 15
function animate(ctx, particals, rows) {
clearRect(ctx)
currentFrame += 1
let stripHeight = rows / 8
let alpha = (1 - currentFrame / endFrame) * 255
particals.forEach(p => {
let [x, y, imgData, row, col] = p
for (var i = 3; i < imgData.data.length; i += 4) {
imgData.data[i] = alpha
}
let dx = 0,
dy = randomInt(-15, -5)
switch (Math.floor(rows / stripHeight)) {
case 0:
case 2:
case 4:
case 6:
dx = randomInt(-2, 15)
break
case 1:
case 3:
case 5:
case 7:
dx = randomInt(-15, 2)
break
}
p[0] = x + dx
p[1] = y + dy
ctx.putImageData(imgData, p[0], p[1])
})
if (currentFrame > endFrame) {
currentFrame = 0
return
}
requestAnimationFrame(() => animate(ctx, particals, rows))
}
就把我的 F22 变消失吧. 这是一张 500*300 的扣掉背景的图, 点击蒸发!
从代码不难看出, 复杂度为 O(粒子数量)的线性关系, 粒子数量又为粒子宽度的平方, 所以这个方法的效率有显而易见的问题. 这里已经经过了几方面的优化:
getImageData
, 然后手动剪切Uint8ClampedArray
生成粒子的ImageData
不怕死的点下面粒子为 1*1, 开 alpha 渐变的效果, ☠️
很明显, 这样的方案虽然能达到基本效果, 但是效率没法让人满意, 后续优化的空间也基本没有, 基本是个死胡同. 于是又仔细观察了一下 Google 的效果, 发现:
于是猜想另一种实现: 把目标按像素打印在多张层叠的 canvas 上, 然后 css 控制 canvas 的动画
代码中略去了一些不重要的细节.
// get layered canvases
function partition(ctx, layer) {
let canvas = ctx.canvas
let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height)
let layers = new Array(layer)
for (let i = 0; i < layer; i++) {
layers[i] = new ImageData(canvas.width, canvas.height)
}
let data = imgData.data
for (let i = 0; i < data.length; i += 4) {
let copy = layers[randomInt(0, layer)]
copy.data.set(data.subarray(i, i + 4), i)
}
return layers
}
// animation: append layers to dom, set css target state
function animate() {
let layers = 20
let overlays = getCanvasNodes(layers)
canvas.parentNode.style = 'position:relative;'
overlays.forEach(n => {
canvas.parentNode.insertBefore(n, canvas.nextSibling)
})
setTimeout(() => {
canvas.style = 'visibility:hidden;'
// shared css props, set elsewhere..
// position: absolute;
// left: 0;
// transition: all 2s;
let style = () =>
`user-select: none; pointer-events: none;transition: transform 1.5s ease-out 0s, opacity 1.5s ease-out; transform: rotate(${random() *
10}deg) translate(${random() * 100}px, ${random() * 50}px) rotate(${random() * 5}deg); opacity: 0;`
overlays.forEach(l => (l.style = style()))
}, 500)
}
Snap...
很顺滑有没有. 经测, 分个百十来层都不会有卡顿问题, 分到 300 层有明显卡顿但也可接受, 远超上一个做法. 实际效果并不是层数越多越好, 而是应该有层次的飘散. 由于我采用了随机数分层所以再层数少的时候可能会有些点聚集的状况, 可以用均匀分配的方式来进一步减少层数. 不过, 比这些细节更重要的是, 对技术运用的想象力
. 感谢 Google 工程师带来的启发!