怎样把 Logo 变成粒子?
- 采样。把 Logo 画到屏幕外的画布上,读出像素,每隔 N 个像素取一个属于 Logo 的像素。每个取到的像素就是一个目标点。
- 分配。为每个目标点创建一个粒子,给它一个随机的起始位置。
- 运动。每一帧让每个粒子朝目标走完剩余距离的一小部分。决定路径的规则不同,就是不同的效果。
- 绘制。清空同一块画布,把每个粒子画成小方块或小圆点。
第 1 步:点是怎么采样的?
- 图片被画到屏幕外的画布上,用
getImageData读出像素。如果超过 5% 的像素是透明的,就把不透明的部分(透明度高于 32)当作 Logo;否则取四个角的颜色作为背景色,保留与背景有差别的像素。工坊里的“提取阈值”和“反相”用来在浅色或深色背景下调整这个判断。 - 按网格逐点检查,间隔就是点阵步长(工坊里可选 2 到 12,默认 4)。每个点会随机偏移不到半个像素,让网格看起来不那么死板。
- 每个点保留自己的 RGB 颜色;工坊里也可以用配色方案给点重新上色。
- 所有点按相对中心的角度排序,这样依赖点序的效果(比如绕着轮廓传播的脉冲)才会运动得平滑。
- 细节丰富的插画会用更高的分辨率采样,所以点数比简单 Logo 多。
默认步长 4 时,我们的三张测试图分别采样出 589 个点(白色四字母字标)、4,139 个点(五种颜色的几何图形)和 7,082 个点(Particle Logo 自己的标志)。工坊会在预览旁边显示点数。代码导出最多保留 1,600 个均匀分布的点,JSON 导出保留全部。
第 2 步:粒子从哪里出发、到哪里去?
每个粒子有一个画布内的随机起点和一个目标点。效果的代码每一帧拿到一个 0 到 100 的进度值,据此算出粒子此刻应该去哪里,再让粒子朝那里走一部分,系数在 0.09 到 0.28 之间,越大越干脆。
第 3 步:9 种效果各自怎么运动?
下表对应 src/engines/morphParticle.ts 里每种效果的规则。“偏移”指叠加在目标位置上的额外位移。
| 效果 | 粒子怎么动 |
|---|---|
| 量子凝聚态 | 每个粒子沿直线从起点走向目标,前 80% 的时间匀速,之后略微冲过头再回落定型。 |
| 莫比乌斯拓扑光流 | 同样沿直线走向目标,再加一道沿点序传播的正弦波,把粒子沿离开中心的方向推出推回,最多 4.5 像素,随 Logo 成形逐渐减弱。 |
| 星系引力坍缩 | 每个粒子的起点在目标位置绕中心转 2.25 圈、再向外推出画布较长边的 65%;随进度推进这两个偏移都收到 0,粒子就一边转一边向内收紧。 |
| 全息光栅断层扫描 | 一条扫描线从画面顶部移到底部;目标在线上方的粒子飞到目标位置,其余粒子留在起点附近,透明度很低。 |
| 扩散降噪重构 | 每个粒子朝目标位置移动,同时叠加一个最多 ±24 像素的随机偏移,进度到 100 时偏移收到 0。 |
| 超导磁通涟漪 | 一圈波环从中心向外扩散。离波环 60 像素以内的粒子被沿离开中心的方向推开最多 14 像素,并以满亮度绘制;所有粒子同时走向目标。 |
| 纳米蜂群智能组装 | 粒子按序号分成四组。每组绕中心转圈,半径是画布较短边的 45%,随进度最多收窄 80%,粒子再平滑地从轨道上落到各自的目标。 |
| 引力弹弓时间逆流 | 前 38% 的时间里,每个粒子比目标位置更远离中心 85 像素;之后猛地收向目标,带一段最多 14 像素、逐渐衰减的抖动。 |
| 克拉尼声波共振 | 每个粒子朝目标位置移动,同时叠加一个随机偏移;偏移大小跟随画面上的二维驻波纹理(正弦乘余弦),随进度收到 0,最大 ±18 像素。 |
第 4 步:怎么画出来?
- 所有粒子画在同一块画布上,用 2D 接口:方块用
fillRect,圆点用arc,整个动画只有一个requestAnimationFrame循环。每个粒子直接设置填充色和透明度,不逐个调用save和restore。 - 每个粒子的大小会在基础大小的约 35% 到 100%–165% 之间缓慢变化,让粒子云有轻微的闪烁感。
- 鼠标靠近时,70 像素以内的粒子会被推开(效果开始之前是 85 像素);点击会给附近的粒子一个冲量,每帧衰减 12%。
一个能直接运行的 Canvas 示例
下面这段代码只用了上面的前三步,没有任何依赖:
<canvas id="logo" width="480" height="480"></canvas>
<script>
const canvas = document.getElementById('logo');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = start;
img.src = 'logo.png'; // 必须同源;用背景透明的 PNG 或 SVG
function start() {
// 1. 把 Logo 画到屏幕外的画布上,读取像素
const off = document.createElement('canvas');
off.width = canvas.width;
off.height = canvas.height;
const octx = off.getContext('2d', { willReadFrequently: true });
const scale = Math.min(off.width / img.width, off.height / img.height) * 0.8;
const w = img.width * scale, h = img.height * scale;
octx.drawImage(img, (off.width - w) / 2, (off.height - h) / 2, w, h);
const data = octx.getImageData(0, 0, off.width, off.height).data;
// 2. 每隔 4 个像素取一个不透明的像素,作为目标位置
const step = 4;
const particles = [];
for (let y = 0; y < off.height; y += step) {
for (let x = 0; x < off.width; x += step) {
const i = (y * off.width + x) * 4;
if (data[i + 3] > 128) {
particles.push({
x: Math.random() * canvas.width, // 随机起点
y: Math.random() * canvas.height,
tx: x, ty: y, // 在 Logo 上的目标位置
color: 'rgb(' + data[i] + ',' + data[i + 1] + ',' + data[i + 2] + ')'
});
}
}
}
// 3. 每一帧让每个粒子朝目标走完剩余距离的一小部分
function frame() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const p of particles) {
p.x += (p.tx - p.x) * 0.06;
p.y += (p.ty - p.y) * 0.06;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - 1, p.y - 1, 2, 2);
}
requestAnimationFrame(frame);
}
frame();
}
</script>
- 示例里按“透明度大于 128”取点,所以图片必须有透明背景(PNG 或 SVG)。如果图片是不透明背景,每个像素都会通过检查,得到的是一个填满的方块。工坊的做法是把像素和背景色比较。
getImageData只能读取同源图片;图片来自别的域名,或者页面是用file://打开的,画布会被标记为受污染,读取时会抛出安全错误。请把页面和图片放在同一个服务器上,或把图片换成 data URL。- 每帧剩余距离乘以 0.94,大约 75 帧(60 帧每秒时约 1.2 秒)后,粒子离目标只剩原来距离的 1%。
- 我们用一张 256 × 256 的透明 PNG 测试过这段代码:步长 4 时生成 4,260 个粒子,推进 150 帧后全部落在 Logo 的轮廓上,颜色保持原图。
常见问题
Particle Logo 用 WebGL 或 Three.js 吗?
不用。导出的文件用浏览器自带的 2D 画布接口绘制,运行时没有任何依赖。
导出的文件里最多有多少个粒子?
最多 1,600 个。工坊里采样出的点可以更多:默认步长下,我们的三张测试图是 589 到 7,082 个。
为什么采样出来的 Logo 有缺口?
通常是步长太大,或阈值不合适。调小步长可以采到更多点,再用阈值和反相开关调整哪些像素算 Logo;工坊的“点云透视”可以看到实际采到了哪些点。
想直接用现成的组件,而不是自己写?
参见接入教程:HTML、Vue 3 和 React 组件都已经带有九种效果、颜色配置、鼠标交互和 progress 参数。