Ledakan prosedural — roket dan neutron
Dua jenis ledakan berbeda — roket/launcher (kilat, cincin, puff bola api, asap besar, percikan) dan neutron (Cherenkov: gumpalan 5 disc, sinar, gelombang kejut) — plus cahaya glow dan getaran layar.
Satu ledakan = beberapa lapis yang hidup dengan jam masing-masing. Pilih jenis di demo: bentuknya sengaja berbeda supaya pemain langsung tahu senjata apa yang meledak.
Demo interaktif butuh JavaScript. Kode lengkap ada di bawah.
Klik untuk meledakkan. Pilih "roket" atau "neutron".
Ledakan roket (PX.Rocket)
Berasal dari gaya fx.explosion (Sprite Fusion) dan bigBlast (Stickman Pixel). Urutan gambar, dari belakang ke depan:
- Asap besar (22 gumpal): menyebar cepat dari pusat lalu melambat (
v·e^(−2.2t)). Tiap gumpal = 3 disc bergeser (tubuh, bayangan sedikit ke kanan-bawah, highlight kiri-atas) agar terlihat bervolume. 6 gumpal pertama berwarna merah panas, sisanya meredup lewat ramp 4 abu. - Puff bola api (38 puff, 3 kelompok): inti (5, pendek 0.2 dtk), menyebar (28, 0.55–1 dtk, miring ke atas), naik (5, ke atas, tertunda). Warna dipilih dari ramp 9 tingkat (putih → kuning → oranye → merah → coklat) menurut umur:
idx = (u^0.62 + noise·0.1)·8. Ukuran membesar cepat lalu menyusut:h = size·(0.55 + 0.45·min(1, 7u))·(1 − 0.55u). Setiap puff digambar sebagai tubuh + disc bergeser + "benjolan" atas + highlight, bukan satu lingkaran polos. - Kilat: disc putih radius
1.05Rhanya 0.06 detik. Singkat — kilat panjang menutupi bola api. - Dua cincin: kuning (
0.1R → 2.7R, 0.5 dtk) dan kuning pucat (0.1R → 1.9R, 0.34 dtk). Radius memakai easing kubik1−(1−u)³, alpha(1−u)^1.4, tebal menyusut ke minimum 0.9 px. - Percikan (90): kecepatan acak, gravitasi, 1 piksel + 1 piksel ekor redup di belakangnya. Warna acak dari 5 tingkat putih → oranye.
- Cahaya:
PX.Lightoranye radius3.4Rredup selama 0.7 dtk + putih1.6Rselama 0.12 dtk — lihat glow. - Getaran:
translateacak pada kanvas, amplitudo turun linear (≈0.33 dtk). Di game, dikalikan jarak ke pemain.
Ledakan neutron (PX.Blast)
Cherenkov biru, lebih "bersih" dan geometris:
- Haze: disc radius
R(1+0.9e)alpha 0.14 warna paling gelap. - Kilat: 14% awal umur, disc putih membesar sambil memudar.
- Gumpalan: 10 lobus acak; tiap lobus = 5 disc konsentris gelap → putih (radius
1, .86, .64, .42, .22; alpha.55, .7, .85, .95, 1). - Sinar: 26 garis dari
0.3R, panjang acak, putih → tengah → gelap; 10% piksel dilewati agar bergerigi. - Gelombang kejut: 2 cincin dengan radius
rr·(1 + 0.07·sin3θ + 0.05·sin5θ + 0.03·sin9θ)+ jitter, supaya tidak bulat sempurna. - Partikel 60 piksel, kecepatan
√random; asap 7 gumpal biru-abu.
Beda roket vs neutron
| Roket | Neutron | |
|---|---|---|
| Karakter | api, volumetrik, kotor | energi, tajam, simetris |
| Dominan | puff + asap + percikan | sinar + gelombang kejut |
| Warna | ramp api 9 tingkat | ramp biru 5 tingkat |
| Cahaya | oranye hangat, lama | biru, mengikuti umur |
| Asap | banyak, besar, lama | sedikit |
Kaidah umum: durasi tiap lapis berbeda (kilat 0.06 dtk, cincin 0.3–0.5, puff 0.5–1, asap 1–2), easing keluar 1−(1−u)², alpha (1−u)^1.1, dan semua posisi di-round oleh PX.Buf.
Ganti palet = ganti tema
PX.PALS berisi ramp 5 warna + 4 warna asap; PX.FB ramp api roket. Ledakan jenis baru cukup menukar ramp dan memilih lapisan yang dipakai.
Kode
Kode: src/code/41-demo-blast.js
// Dua jenis ledakan. PX.Rocket (roket/launcher): kilat -> 2 cincin -> puff bola api -> asap besar -> percikan berparabola.
// PX.Blast (neutron): kilat -> gumpalan 5 disc -> sinar -> gelombang kejut tak beraturan -> partikel -> asap. Keduanya memberi cahaya lewat PX.Light.
PX.PALS = {
api: { c: ['#8a1f17', '#f2561d', '#ff8a2a', '#ffe26b', '#ffffff'], smoke: ['#7e3030', '#4a2a2a', '#2e2024', '#1a161c'] },
neutron: { c: ['#1b2fb0', '#3a5cff', '#8fb4ff', '#c9d8ff', '#ffffff'], smoke: ['#5a6aa8', '#3a4278', '#262a4c', '#14162a'] },
};
PX.Blast = class {
constructor(x, y, R, pal = PX.PALS.api) {
const r = Math.random; this.x = Math.round(x); this.y = Math.round(y); this.R = R; this.t = 0; this.life = 0.95 + r() * 0.25;
this.c = pal.c.map(PX.hex); this.sm = pal.smoke.map(PX.hex);
this.rays = Array.from({ length: 26 }, () => ({ a: r() * 6.283, len: 0.4 + r() * 1.9, w: r() < 0.25, t0: r() * 0.12 }));
this.lobes = Array.from({ length: 10 }, () => ({ a: r() * 6.283, d: r() * 0.75, r: 0.2 + r() * 0.28, t0: r() * 0.22, tl: 0.45 + r() * 0.4 }));
this.ph = [r() * 6.283, r() * 6.283, r() * 6.283];
this.parts = Array.from({ length: 60 }, () => { const g = r() * 6.283, v = (0.4 + Math.sqrt(r()) * 2.4) * R * 3; return { x: this.x, y: this.y, vx: Math.cos(g) * v, vy: Math.sin(g) * v - R, life: 0.25 + r() * 0.65, ci: (r() * 4) | 0 }; });
this.smoke = Array.from({ length: 7 }, () => ({ x: this.x + (r() - 0.5) * R, y: this.y + (r() - 0.5) * R, vx: (r() - 0.5) * 8, vy: -4 - r() * 9, life: 1 + r() * 0.6, r0: R * 0.18, r1: R * (0.4 + r() * 0.4) }));
}
get done() { return this.t > this.life + 0.8; }
step(dt) {
this.t += dt;
for (const p of this.parts) { p.x += p.vx * dt; p.y += p.vy * dt; p.vy += this.R * 14 * dt; }
for (const s of this.smoke) { s.x += s.vx * dt; s.y += s.vy * dt; }
}
draw(b) {
const { x: cx, y: cy, R, c } = this, u = this.t / this.life, e = 1 - (1 - u) * (1 - u), a = Math.pow(Math.max(0, 1 - u), 1.1), rnd = Math.random;
for (const s of this.smoke) { const su = this.t / s.life; if (su > 1 || su < 0.12) continue; b.disc(s.x, s.y, s.r0 + (s.r1 - s.r0) * su, this.sm[Math.min(3, (su * 4) | 0)], 0.55 * (1 - su)); }
if (u < 1) {
b.disc(cx, cy, R * (1 + 0.9 * e), c[0], a * 0.14);
if (u < 0.14) b.disc(cx, cy, R * (0.5 + (0.9 * u) / 0.14), c[4], 1 - u / 0.14); // kilat
for (const l of this.lobes) { // gumpalan bola api: 5 disc konsentris
const lu = (u - l.t0) / l.tl; if (lu < 0 || lu > 1) continue;
const le = 1 - (1 - lu) * (1 - lu), al = Math.pow(1 - lu, 1.1), sz = l.r * R * (0.4 + 1.1 * le),
lx = cx + Math.cos(l.a) * l.d * R * 1.5 * le, ly = cy + Math.sin(l.a) * l.d * R * 1.5 * le;
[[0, 1, 0.55], [1, 0.86, 0.7], [2, 0.64, 0.85], [3, 0.42, 0.95], [4, 0.22 * (1 - lu * 0.6), 1]].forEach(([ci, k, A]) => b.disc(lx, ly, sz * k, c[ci], al * A));
}
for (const r of this.rays) { // sinar radiasi
const ru = (u - r.t0) / (1 - r.t0); if (ru < 0) continue;
const re = 1 - (1 - ru) * (1 - ru), L = R * r.len * 1.3 * re, s0 = R * 0.3, ca = Math.cos(r.a), sa = Math.sin(r.a), al = Math.pow(1 - ru, 1.2);
for (let d = s0; d < s0 + L; d++) {
if (rnd() < 0.1) continue; const f = (d - s0) / L, col = f < 0.25 ? c[4] : f < 0.6 ? c[2] : c[1];
b.px(cx + ca * d, cy + sa * d, col, al * (1 - f) * 0.95);
}
}
for (let k = 0; k < 2; k++) { // gelombang kejut 2 lapis, radius bergelombang
const rr = R * (k ? 0.3 + 1.2 * e : 0.4 + 1.7 * e), n = Math.ceil(rr * 6.3), aa = a * (k ? 0.55 : 0.9); if (rr < 1) continue;
for (let i = 0; i < n; i++) {
if (rnd() < 0.22) continue; const g = (i / n) * 6.283,
rd = rr + rr * (Math.sin(g * 3 + this.ph[0]) * 0.07 + Math.sin(g * 5 + this.ph[1]) * 0.05 + Math.sin(g * 9 + this.ph[2]) * 0.03) + (rnd() - 0.5) * 1.4;
b.px(cx + Math.cos(g) * (rd + 1.5), cy + Math.sin(g) * (rd + 1.5), c[1], aa); b.px(cx + Math.cos(g) * rd, cy + Math.sin(g) * rd, k ? c[2] : c[4], aa);
}
}
}
for (const p of this.parts) { const pu = this.t / p.life; if (pu < 1) b.px(p.x, p.y, c[Math.min(4, 4 - ((pu * 3) | 0))], 1 - pu * 0.5); }
}
};
// ---- Ledakan roket: port gaya fx.explosion (Sprite Fusion) + bigBlast (Stickman Pixel). Skala: R = radius ledakan dalam piksel kanvas.
PX.FB = ['#ffffff', '#fff3a6', '#ffe26b', '#ffc23d', '#ff8a2a', '#f2561d', '#c9281f', '#8a1f17', '#4a1712'].map(PX.hex);
PX.Rocket = class {
constructor(x, y, R) {
const r = Math.random, rg = (a, b) => a + r() * (b - a), z = R * 0.95; this.x = Math.round(x); this.y = Math.round(y); this.R = R; this.t = 0; this.shake = 1;
this.rings = [{ r0: R * 0.1, r1: R * 2.7, life: 0.5, th: 1.8, c: PX.hex('#ffd84a') }, { r0: R * 0.1, r1: R * 1.9, life: 0.34, th: 1.2, c: PX.hex('#fff1a8') }];
this.puffs = []; const puff = (px, py, vx, vy, life, size, d = 0) => this.puffs.push({ x: px, y: py, vx, vy, life, size, d, sd: r() });
for (let i = 0; i < 5; i++) puff(x + rg(-1, 1), y + rg(-1, 1), rg(-10, 10), rg(-20, -5), rg(0.18, 0.3), rg(z * 0.8, z * 1.1)); // inti
for (let i = 0; i < 28; i++) { const a = r() * 6.283, e = rg(0, R * 0.55), sp = rg(20, 105); puff(x + Math.cos(a) * e, y + Math.sin(a) * e, Math.cos(a) * sp, Math.sin(a) * sp * 0.8 - 20, rg(0.55, 1), rg(z * 0.45, z * 0.9)); } // menyebar
for (let i = 0; i < 5; i++) puff(x + rg(-R * 0.25, R * 0.25), y, rg(-6, 6), rg(-90, -50), rg(0.7, 1), rg(z * 0.5, z * 0.7), rg(0, 0.1)); // naik
this.smoke = Array.from({ length: 22 }, (_, i) => { const a = r() * 6.283, v = R * (1 + r() * 2), l = 1 + r() * 0.9; return { x: x + Math.cos(a) * R * 0.1, y: y + Math.sin(a) * R * 0.1, vx: Math.cos(a) * v, vy: Math.sin(a) * v * 0.85 - 5, life: l, r1: 2 + r() * 3.5, hot: i < 6 }; });
const PAL = ['#ffffff', '#fff3b0', '#ffc23a', '#ff8a1f', '#ff6a1a'].map(PX.hex);
this.sparks = Array.from({ length: 90 }, () => { const a = r() * 6.283, v = 60 + r() * 240; return { x, y, vx: Math.cos(a) * v, vy: Math.sin(a) * v - 50, life: 0.45 + r() * 0.85, c: PAL[(r() * 5) | 0] }; });
}
get done() { return this.t > 2.2; }
step(dt) {
this.t += dt; this.shake = Math.max(0, this.shake - dt * 3);
for (const p of this.puffs) { p.d -= dt; if (p.d > 0) continue; p.x += p.vx * dt; p.y += p.vy * dt; p.vx *= Math.exp(-2.5 * dt); p.vy *= Math.exp(-2.5 * dt); p.vy -= 12 * dt; p.t = (p.t || 0) + dt; }
for (const s of this.smoke) { s.x += s.vx * dt; s.y += s.vy * dt; s.vx *= Math.exp(-2.2 * dt); s.vy *= Math.exp(-2.2 * dt); }
for (const s of this.sparks) { s.x += s.vx * dt; s.y += s.vy * dt; s.vy += 380 * dt; }
}
light(L) { const u = Math.min(1, this.t / 0.7); L.glow(this.x, this.y, this.R * 3.4, [255, 176, 80], 0.6 * Math.pow(1 - u, 1.5)); if (this.t < 0.12) L.glow(this.x, this.y, this.R * 1.6, [255, 255, 255], 0.6 * (1 - this.t / 0.12)); }
draw(b) {
const SM = ['#7e3030', '#4a2a2a', '#2e2024', '#1a161c'].map(PX.hex), c = this.t;
for (const s of this.smoke) { // asap: bola besar menyebar, 3 disc bergeser (tubuh + bayangan + highlight)
const t = c / s.life; if (t >= 1 || t < 0.04) continue; const r = 1.5 + (s.r1 * 1.25 - 1.5) * (1 - (1 - t) * (1 - t)), a = Math.min(1, t * 12) * Math.min(1, (1 - t) * 2.4) * 0.6, col = s.hot && t < 0.25 ? PX.hex('#a8402a') : SM[Math.min(3, (t * 4) | 0)];
b.disc(s.x, s.y, r, col, a * 0.45); b.disc(s.x + r * 0.05, s.y + r * 0.06, r * 0.74, col, a * 0.62); if (t < 0.5) b.disc(s.x - r * 0.22, s.y - r * 0.3, r * 0.5, SM[Math.max(0, Math.min(3, (t * 4) | 0) - 1)], a * 0.4 * (1 - t * 2));
}
if (c < 0.06) b.disc(this.x, this.y, this.R * 1.05, PX.FB[0], 1); // kilat 0.06 dtk
for (const p of this.puffs) { // puff bola api: warna turun sepanjang ramp 9 tingkat
if (p.d > 0) continue; const T = p.t || 0, at = T / p.life; if (at >= 1) continue;
const et = 1 - at, it = Math.min(0.999, Math.pow(at, 0.62) + p.sd * 0.1), lt = Math.min(7, Math.floor(it * 8)), h = p.size * (0.55 + 0.45 * Math.min(1, at * 7)) * (1 - at * 0.55), FB = PX.FB;
if (lt >= 7) b.disc(p.x, p.y, h, FB[lt], Math.min(1, et * 3) * 0.85);
else { b.disc(p.x, p.y, h, FB[Math.min(8, lt + 2)]); b.disc(p.x - h * 0.16, p.y - h * 0.22, h * 0.78, FB[lt]); if (h > 1.6) b.disc(p.x, p.y - h * 0.9, h * 0.42, FB[lt]); if (lt <= 3 && h > 1.8) b.disc(p.x - h * 0.28, p.y - h * 0.34, h * 0.36, FB[Math.max(0, lt - 1)]); }
}
for (const q of this.rings) { // cincin: radius easing kubik, alpha (1-u)^1.4, tebal menyusut
const u = c / q.life; if (u >= 1) continue; const e = 1 - Math.pow(1 - u, 3), Rc = q.r0 + (q.r1 - q.r0) * e, th = Math.max(0.9, q.th * (1 - u * 0.5)), n = Math.ceil(Rc * 6.3), al = Math.pow(1 - u, 1.4);
for (let i = 0; i < n; i++) { const g = (i / n) * 6.283; for (let k = 0; k < th; k++) b.px(this.x + Math.cos(g) * (Rc + k), this.y + Math.sin(g) * (Rc + k), q.c, al); }
}
for (const s of this.sparks) { const u = c / s.life; if (u >= 1) continue; b.px(s.x, s.y, s.c, 1 - u * 0.5); b.px(s.x - s.vx * 0.02, s.y - s.vy * 0.02, s.c, (1 - u) * 0.5); }
}
};
PX.demos.blast = (host) => {
const W = 240, H = 135, cv = PX.canvas(host, W, H, 640), c = cv.getContext('2d'), buf = new PX.Buf(W, H), lit = new PX.Light(W, H), p = PX.pointer(cv);
cv.style.background = '#12121c';
let kind = 'roket', list = [], idle = 0, shake = 0;
PX.ui(host).select('Jenis', ['roket', 'neutron'], 'roket', (v) => (kind = v));
const boom = (x, y, k = kind) => list.push(k === 'roket' ? new PX.Rocket(x, y, 11) : new PX.Blast(x, y, 16, PX.PALS.neutron));
PX.loop(host, (dt) => {
for (const k of p.clicks.splice(0)) boom(k.x, k.y);
idle -= dt; if (idle <= 0) { idle = 2; if (!p.in) boom(50 + Math.random() * 140, 40 + Math.random() * 60); }
list = list.filter((q) => !q.done); buf.clear(); lit.clear(); shake = 0;
for (const q of list) { q.step(dt); q.draw(buf); if (q.light) q.light(lit); else lit.glow(q.x, q.y, q.R * 3, [90, 130, 255], 0.5 * Math.pow(Math.max(0, 1 - q.t / q.life), 1.5)); if (q.shake) shake = Math.max(shake, q.shake); }
buf.flush(c); lit.flush(c);
cv.style.transform = shake > 0.02 ? `translate(${((Math.random() - 0.5) * 6 * shake).toFixed(1)}px,${((Math.random() - 0.5) * 6 * shake).toFixed(1)}px)` : '';
});
};