Naucz si臋 kodowa膰 organiczne animacje przypominaj膮ce krople
Metaballs (a mo偶e po polsku meta-kulki?) to organicznie wygl膮daj膮ce kszta艂ty, kt贸re 艂膮cz膮 si臋 i mieszaj膮 p艂ynnie, gdy zbli偶aj膮 si臋 do siebie. S膮 tworzone za pomoc膮 r贸wna艅 pola matematycznego i cz臋sto pojawiaj膮 si臋 w lampach lawowych, symulacjach p艂yn贸w i efektach w grach.
Ka偶dy metaball tworzy niewidzialn膮 "stref臋 wp艂ywu" wok贸艂 siebie. Gdy wiele stref si臋 nak艂ada, ich wp艂ywy si臋 sumuj膮. Tam, gdzie 艂膮czna si艂a pola przekracza pr贸g, rysujemy piksel. To tworzy p艂ynny efekt mieszania.
Pobierz wersj臋 pe艂noekranow? HTML+JS tutaj: metaballs.htm lub przeanalizuj poni偶sze kroki.
Najpierw potrzebujemy elementu canvas do rysowania:
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Dostosuj canvas do rozmiaru okna
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
</script>
Ka偶dy metaball potrzebuje pozycji, pr臋dko艣ci i rozmiaru:
class Metaball {
constructor(x, y, vx, vy, radius) {
this.x = x; // Pozycja X
this.y = y; // Pozycja Y
this.vx = vx; // Pr臋dko艣膰 X
this.vy = vy; // Pr臋dko艣膰 Y
this.radius = radius; // Rozmiar
}
update() {
// Przesu艅 kul臋
this.x += this.vx;
this.y += this.vy;
// Odbij si臋 od 艣cian
if (this.x - this.radius < 0 || this.x + this.radius > canvas.width) {
this.vx *= -1;
}
if (this.y - this.radius < 0 || this.y + this.radius > canvas.height) {
this.vy *= -1;
}
}
}
To jest matematyczne serce metaballs. Dla ka偶dego piksela obliczamy, jak bardzo wp艂ywa na niego ka偶da kula:
Im bli偶ej punkt jest do centrum kuli, tym silniejszy wp艂yw. Sumujemy wszystkie wp艂ywy:
function getMetaballValue(x, y) {
let sum = 0;
// Dodaj wp艂yw ka偶dej kuli
for (const ball of balls) {
// Oblicz kwadrat odleg艂o艣ci (szybsze ni偶 sqrt)
const dx = x - ball.x;
const dy = y - ball.y;
const distSq = dx * dx + dy * dy;
// Dodaj wp艂yw tej kuli
sum += (ball.radius * ball.radius) / distSq;
}
return sum;
}
U偶ywamy kwadratu odleg艂o艣ci zamiast rzeczywistej odleg艂o艣ci, poniewa偶 jest szybszy do obliczenia (nie wymaga pierwiastka kwadratowego) i nadal daje nam p艂ynne zanikanie, kt贸rego chcemy. Odwrotna relacja kwadratowa tworzy naturalnie wygl膮daj膮ce pole.
Sprawdzamy ka偶dy piksel i kolorujemy go na podstawie si艂y pola:
function render() {
const imageData = ctx.createImageData(canvas.width, canvas.height);
const data = imageData.data;
const step = 3; // Sprawd藕 co 3 piksele dla wydajno艣ci
for (let y = 0; y < canvas.height; y += step) {
for (let x = 0; x < canvas.width; x += step) {
const value = getMetaballValue(x, y);
// Je艣li pole jest wystarczaj膮co silne, narysuj piksel
if (value > 1) {
// Kolor na podstawie intensywno艣ci
const intensity = Math.min(value / 3, 1);
const r = Math.floor(100 + intensity * 155);
const g = Math.floor(50 + intensity * 100);
const b = Math.floor(200 + intensity * 55);
// Wype艂nij blok step x step
for (let dy = 0; dy < step; dy++) {
for (let dx = 0; dx < step; dx++) {
const idx = ((y + dy) * canvas.width + (x + dx)) * 4;
data[idx] = r;
data[idx + 1] = g;
data[idx + 2] = b;
data[idx + 3] = 255; // Kana艂 alfa
}
}
}
}
}
ctx.putImageData(imageData, 0, 0);
}
step do pomijania pikseli. Sprawdzanie ka偶dego piksela (step=1) wygl膮da p艂ynniej, ale jest wolniejsze. Krok 2-4 r贸wnowa偶y jako艣膰 i wydajno艣膰.
Stw贸rz niewidzialny metaball, kt贸ry pod膮偶a za kursorem:
const mouseInfluence = {
x: -1000,
y: -1000,
radius: 80,
active: false
};
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseInfluence.x = e.clientX - rect.left;
mouseInfluence.y = e.clientY - rect.top;
mouseInfluence.active = true;
});
canvas.addEventListener('mouseleave', () => {
mouseInfluence.active = false;
});
Nast臋pnie uwzgl臋dnij to w obliczeniach pola:
function getMetaballValue(x, y) {
let sum = 0;
for (const ball of balls) {
const dx = x - ball.x;
const dy = y - ball.y;
const distSq = dx * dx + dy * dy;
sum += (ball.radius * ball.radius) / distSq;
}
// Dodaj wp艂yw myszy
if (mouseInfluence.active) {
const dx = x - mouseInfluence.x;
const dy = y - mouseInfluence.y;
const distSq = dx * dx + dy * dy;
sum += (mouseInfluence.radius * mouseInfluence.radius) / distSq;
}
return sum;
}
function animate() {
// Wyczy艣膰 canvas
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Zaktualizuj wszystkie kule
for (const ball of balls) {
ball.update();
}
// Renderuj metaballs
render();
// P臋tla
requestAnimationFrame(animate);
}
animate();
Dodaj obs艂ug臋 zdarze艅 dotykowych dla urz膮dze艅 mobilnych:
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
mouseInfluence.x = touch.clientX - rect.left;
mouseInfluence.y = touch.clientY - rect.top;
mouseInfluence.active = true;
});
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
balls.push(new Metaball(
e.clientX - rect.left,
e.clientY - rect.top,
(Math.random() - 0.5) * 3,
(Math.random() - 0.5) * 3,
30 + Math.random() * 40
));
});
step 2-4 aby pomija膰 pikseleTeraz, gdy rozumiesz podstawy, wypr贸buj te warianty:
Udanego kodowania! 馃殌