复刻明日方舟官网粒子特效
复刻明日方舟官网粒子特效
原版:明日方舟 - Arknights (hypergryph.com)
我的:Arknights_particle_animation (akejyo.github.io)
看到这个粒子效果,我第一反应是真实的物理模拟,然后我嗯写俩小时加速度写了个抽象玩意出来😎,思路错的很。还好洗澡的时候想明白了()
图片转粒子图
我采用的办法是预设一个 blockSize 值作为块的大小,将原图的每 blockSize*blockSize 个像素转化成一个粒子:按这个块所有像素的 RGB 平均值来决定是否是一个粒子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function convertImageToParticles(img) {
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
    var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    var data = imageData.data;
    for (var i = 0; i < data.length; i += 4) {// rgb 取平均
        var avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
        data[i] = avg;
        data[i + 1] = avg;
        data[i + 2] = avg;
    }
    ctx.putImageData(imageData, 0, 0);
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    var blockX = Math.ceil(canvas.width / blockSize);
    var blockY = Math.ceil(canvas.height / blockSize);
    for (var y = 0; y < blockY; y++) {
        for (var x = 0; x < blockX; x++) {
            var sum = 0;
            for (var j = 0; j < blockSize; j++) {
                for (var i = 0; i < blockSize; i++) {
                    var index = ((y * blockSize + j) * canvas.width + x * blockSize + i) * 4;
                    sum += data[index]; //红色通道,前面已经处理成灰度
                }
            }
            var average = Math.floor(sum / (blockSize * blockSize));
            if (average > 128) {
                particles.push(new Particles(x*7, y*7)); //乘一个数使得粒子之间有间隔
            }
        }
    }
}
粒子运动
接下来最核心的问题:粒子与鼠标之间的斥力,以及粒子趋向复位受到的引力。
粒子复位的速度是越来越慢的,可以用距离除上一个固定值来模拟;斥力直接用距离的反比例函数模拟。
- 首先给每个粒子记录其原始坐标 - originalX、- originalY,由当前粒子的实时坐标- x、- y,直接计算坐标差,除一个固定值得到当前状态的复位速度:- 1 2 3 4 - var dToOriginalX = particle.x - particle.originalX; var dToOriginalY = particle.y - particle.originalY; particle.speedX = -dToOriginalX / 30; particle.speedY = -dToOriginalY / 30; 
- 监听获得鼠标的坐标,计算与粒子之间的距离,取倒数得到斥力,乘一个 - forceRatio调整大小,叠加到粒子的速度上:- 1 2 3 4 5 6 7 8 9 10 - var dx = particle.x - mouseX; var dy = particle.y - mouseY; var distance = Math.sqrt(dx * dx + dy * dy); var force = 1 / distance; force = Math.min(force, 5); particle.speedX += dx / distance * force * forceRatio; particle.speedY += dy / distance * force * forceRatio; particle.speedX = Math.min(particle.speedX, 5); particle.speedY = Math.min(particle.speedY, 5); 
这样做的话,距离鼠标较近的粒子是完全过不来的,当鼠标远离时,粒子可以较丝滑的复位。
添加少许细节
- 随机初始化粒子的的坐标,这样加载粒子图时粒子会四面八方聚集到正确位置,比较有观赏性; 
- 给粒子一个带有随机的 - alpha属性作为最终透明度,可以让最后的图案有变化感;- 1 2 3 4 5 6 7 - function Particles(x, y) { this.x = Math.random() * window.innerWidth; this.y = Math.random() * window.innerHeight; ... this.alpha = 1 - Math.random() * randomRatio; this.alphaNow = 0; } 
- 给粒子一个 - alphaNow属性作为当前透明度,可以在最开始加载的时候让这个值逐渐加到- alpha,这样视觉上有个由暗到明的渐变,具体在粒子的- draw方法;- 1 2 3 4 - Particles.prototype.draw = function() { ctx.fillStyle = `rgba(173, 216, 230, ${this.alphaNow + 0.005 > this.alpha ? this.alpha : this.alphaNow += 0.005})`; ctx.fillRect(this.x, this.y, this.size, this.size); } 
- 与随机透明同理,最终坐标也可以上随机,具体在作粒子图的部分调整 - 1 2 3 4 5 6 7 - img.onload = () => { ... if (average > 128) { particles.push(new Particles((x + Math.random() * randomRatio) * 7, (y + Math.random() * randomRatio) * 7)); } ... } 
2024/5/12 添加了多粒子图转换、图片跟随鼠标
粒子图切换
涉及到多个粒子图时,粒子图的转换我是这样搞的:
- 先塞一个新图的粒子数组 - newParticles;
- 打乱 - particles和- newParticles;
- 已存在的粒子直接赋新值,粒子不够的 New 一个 - push进去:- 1 2 3 4 5 6 7 8 9 10 11 12 - shuffle(newParticles); shuffle(particles); for (var i = 0; i < newParticles.length; i++) { if (i >= particleCount) { particles.push(newParticles[i]); } else { particles[i].needed = true; particles[i].originalX = newParticles[i].originalX; particles[i].originalY = newParticles[i].originalY; } } particleCount = newParticles.length; - 洗牌打乱: - 1 2 3 4 5 6 7 8 9 10 - function shuffle(arr) { var currentIndex = arr.length, randomIndex; while (currentIndex != 0) { randomIndex = Math.floor(Math.random() * currentIndex); currentIndex--; [arr[currentIndex], arr[randomIndex]] = [arr[randomIndex], arr[currentIndex]]; } return arr; } 
- 多余的粒子设置其 - needed属性为- false,在画粒子的部分对这部分粒子特殊处理:速度归零、透明度逐渐下降,下降到 0 时踢掉该粒子。- 1 2 3 4 - Particle.prototype.disappear = function() { ctx.fillStyle = `rgba(173, 216, 230, ${this.alphaNow -= 0.01})`; ctx.fillRect(this.x, this.y, this.size, this.size); } - 1 2 3 4 5 6 7 8 9 10 11 - if (particle.needed) { particle.update(); particle.draw(); } else { particle.speedX = 0; particle.speedY = 0; particle.disappear(); if (particle.alphaNow <= 0) { particles.splice(particles.indexOf(particle), 1); } } 
图片平滑跟随鼠标
当鼠标移到列表时,对应的图片的会出现在鼠标下面,并平滑跟随鼠标。
本来我是想利用下 CSS 的 transition 的,没弄出来,最大的问题在于鼠标坐标改变时 transition 的速度要平滑变化过去,不然会有卡顿感。最后还是上了 js。
- 鼠标在 list区域时,图片追着鼠标走,速度设置为与 $\sqrt{distance}$ 成正比;
- 鼠标离开 list区域时,记录离开的坐标点,当鼠标回到list区域时图片从记录点开始移动。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var imgSpeedRatio = 0.7;
var listItems = document.getElementsByClassName("listItem");
var aniId;
var imgX, imgY;
var lastX, laxtY;
function moveImg(img) {
    var rect = img.getBoundingClientRect(); //get the position of the img
    imgX = rect.left + img.width / 2;
    imgY = rect.top + img.height / 2;
    var distanceX = mouseX - imgX;
    var distanceY = mouseY - imgY;
    var distance = Math.sqrt(Math.pow(distanceX, 2) + Math.pow(distanceY, 2));
    var speedX = distanceX / Math.sqrt(distance) * imgSpeedRatio;
    var speedY = distanceY / Math.sqrt(distance) * imgSpeedRatio;
    if (lastX != undefined && lastY != undefined && img.style.opacity == 0) {
        img.style.left = (lastX - img.width / 2) + 'px';
        img.style.top = (lastY - img.height / 2) + 'px';
    } else {
        img.style.left = (imgX + speedX - img.width / 2) + 'px';
        img.style.top = (imgY + speedY - img.height / 2) + 'px';
    }
    aniId = requestAnimationFrame(() => moveImg(img));
}
for (let i = 0; i < listItems.length; i++) {
    listItems[i].addEventListener('mousemove', () => {
        var img = listItems[i].getElementsByTagName('img')[0];
        img.style.opacity = 0.7;
        lastX = mouseX;
        lastY = mouseY;
        if (aniId === undefined) { //init
            for (let j = 0; j < listItems.length; j++) {
                var img = listItems[j].getElementsByTagName('img')[0];
                moveImg(img);
            }
        }
    });
    listItems[i].addEventListener('mouseleave', () => {
        var img = listItems[i].getElementsByTagName('img')[0];
        img.style.opacity = 0;
    });
}
