<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Салют — 43 эффекта (canvas)</title>
<style>
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body {
margin: 0; padding: 0;
width: 100%; height: 100%;
overflow: hidden;
background: radial-gradient(ellipse at 50% 40%, #141a34 0%, #070a1a 55%, #02030a 100%);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #fff;
user-select: none;
cursor: crosshair;
}
#canvas {
position: fixed;
inset: 0;
z-index: 1;
display: block;
}
.info {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
text-align: center;
pointer-events: none;
z-index: 100;
}
.info h1 {
margin: 0;
font-size: 20px;
letter-spacing: 4px;
text-transform: uppercase;
color: #fff;
text-shadow: 0 0 20px rgba(255,214,0,0.7);
}
.info p {
margin: 6px 0 0;
font-size: 12px;
color: #8a8aa0;
letter-spacing: 1px;
}
.info .variant {
margin-top: 8px;
font-size: 13px;
font-weight: 700;
letter-spacing: 3px;
text-transform: uppercase;
color: #ffd600;
text-shadow: 0 0 15px rgba(255,214,0,0.8);
min-height: 18px;
transition: opacity 0.3s, color 0.3s;
}
.info .counter {
margin-top: 4px;
font-size: 11px;
color: #666a80;
letter-spacing: 1px;
}
.controls {
position: fixed;
left: 50%;
bottom: 24px;
transform: translateX(-50%);
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
z-index: 100;
max-width: 95vw;
}
.controls button {
padding: 12px 22px;
font-size: 13px;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
border: 1px solid rgba(255,214,0,0.4);
border-radius: 10px;
background: rgba(30,20,10,0.7);
color: #ffe680;
cursor: pointer;
backdrop-filter: blur(6px);
transition: all 0.15s ease;
}
.controls button:hover {
background: rgba(255,180,0,0.25);
border-color: rgba(255,214,0,0.9);
box-shadow: 0 0 20px rgba(255,214,0,0.5);
}
.controls button:active {
transform: scale(0.96);
}
.controls select {
padding: 12px 16px;
font-size: 12px;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
border: 1px solid rgba(255,214,0,0.4);
border-radius: 10px;
background: rgba(30,20,10,0.85);
color: #ffe680;
cursor: pointer;
backdrop-filter: blur(6px);
max-width: 240px;
}
.controls select option {
background: #1a1408;
color: #ffe680;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div class="info">
<h1>Салют</h1>
<p>Клик в любом месте — залп с полётом</p>
<div class="variant" id="variant-name"></div>
<div class="counter" id="counter"></div>
</div>
<div class="controls">
<button id="btn-fire">Залп</button>
<button id="btn-big">Большой взрыв</button>
<button id="btn-random">Случайный</button>
<select id="variant-select"></select>
</div>
<script>
(() => {
'use strict';
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d', { alpha: true });
let W = 0, H = 0, DPR = 1;
function resize() {
DPR = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W * DPR;
canvas.height = H * DPR;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
}
resize();
window.addEventListener('resize', resize);
// ============================================================
// ПАЛИТРА И УТИЛИТЫ
// ============================================================
const COLORS = [
'#ffd600', '#ff8a00', '#ff3060', '#4aa8ff',
'#8affc8', '#c88aff', '#ffffff', '#ff66cc',
'#66ff66', '#ff4444', '#00ffff', '#ffaa00',
'#ff0099', '#9900ff', '#00ff99', '#ff5500',
'#ffe680', '#e0e8ff', '#fff2b0',
];
const rand = (min, max) => Math.random() * (max - min) + min;
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
function hexToRgb(hex) {
const n = parseInt(hex.slice(1), 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
// ============================================================
// БЕЛЫЕ АКЦЕНТЫ — с индивидуальным шансом
// ============================================================
const WHITE_CHANCE = 0.3;
const flashyColor = (c, chance = WHITE_CHANCE) =>
Math.random() < chance ? '#ffffff' : c;
// ============================================================
// ЕДИНЫЙ ПУЛ ЧАСТИЦ
// ============================================================
const MAX_PARTICLES = 1800;
const particles = [];
const pool = [];
function acquireParticle() {
if (pool.length > 0) return pool.pop();
return {
x: 0, y: 0, vx: 0, vy: 0,
color: '#fff', colorRgb: [255, 255, 255],
size: 2, life: 1, age: 0,
gravity: 200, friction: 0.985, trailLength: 4,
flicker: false, flickerSeed: 0,
prevX: 0, prevY: 0,
};
}
function releaseParticle(p) {
if (pool.length < MAX_PARTICLES) pool.push(p);
}
function spawnSpark(x, y, dx, dy, color, size, life, options = {}) {
if (particles.length >= MAX_PARTICLES) return;
const p = acquireParticle();
p.x = x; p.y = y;
p.prevX = x; p.prevY = y;
p.vx = dx; p.vy = dy;
p.color = color;
p.colorRgb = hexToRgb(color);
p.size = size;
p.life = life / 1000;
p.age = 0;
p.gravity = options.gravity !== undefined ? options.gravity : 220;
p.friction = options.friction !== undefined ? options.friction : 0.985;
p.trailLength = options.trailLength || 3.5;
p.flicker = options.flicker || false;
p.flickerSeed = Math.random() * 1000;
particles.push(p);
}
// ============================================================
// РАКЕТЫ
// ============================================================
const rockets = [];
function launchRocket(targetX, targetY, scale = 1, forceVariant = null) {
const startX = targetX + rand(-100, 100) * scale;
const startY = H + 20;
const controlX = (startX + targetX) / 2 + rand(-150, 150) * scale;
const controlY = Math.min(startY, targetY) - rand(100, 300) * scale;
const variantIndex = forceVariant !== null
? forceVariant
: Math.floor(Math.random() * VARIANTS.length);
rockets.push({
startX, startY, targetX, targetY,
controlX, controlY,
flightTime: rand(0.6, 1.1),
age: 0,
scale,
color: pick(COLORS),
colorRgb: hexToRgb(pick(COLORS)),
variantIndex,
lastTrail: 0,
x: startX, y: startY,
});
const nameEl = document.getElementById('variant-name');
nameEl.textContent = `${variantIndex + 1}. ${VARIANTS[variantIndex].name}`;
nameEl.style.opacity = '1';
nameEl.style.color = '#ffd600';
}
function updateRockets(dt, now) {
for (let i = rockets.length - 1; i >= 0; i--) {
const r = rockets[i];
r.age += dt;
const t = Math.min(r.age / r.flightTime, 1);
const mt = 1 - t;
r.x = mt * mt * r.startX + 2 * mt * t * r.controlX + t * t * r.targetX;
r.y = mt * mt * r.startY + 2 * mt * t * r.controlY + t * t * r.targetY;
if (now - r.lastTrail > 25) {
r.lastTrail = now;
spawnSpark(
r.x, r.y,
rand(-25, 25), rand(30, 70),
flashyColor(r.color, 0.5),
rand(2, 4) * r.scale, rand(200, 450),
{ gravity: 180, friction: 0.97, trailLength: 5, flicker: true }
);
}
if (t >= 1) {
const variant = VARIANTS[r.variantIndex];
spawnFlash(r.targetX, r.targetY, r.scale);
variant.fn(r.targetX, r.targetY, r.scale);
for (let j = 0; j < 5 * r.scale; j++) {
spawnSmoke(r.targetX + rand(-30, 30), r.targetY + rand(-30, 30), r.scale);
}
rockets.splice(i, 1);
}
}
}
// ============================================================
// ВСПЫШКИ И ДЫМ
// ============================================================
const flashes = [];
const smokes = [];
function spawnFlash(x, y, scale, color = [255, 248, 192]) {
flashes.push({ x, y, scale, age: 0, life: 0.5, color });
}
function spawnSmoke(x, y, scale) {
if (smokes.length > 80) return;
smokes.push({
x, y, scale, age: 0,
life: rand(0.9, 1.6),
driftX: rand(-40, 40), driftY: rand(-30, 30),
size: rand(12, 28) * scale,
});
}
// ============================================================
// 43 ЭФФЕКТА
// ============================================================
// 1. ПИОН — 60% белых (плотный, яркий)
function fxPion(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(80 * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n + rand(-0.08, 0.08);
const sp = rand(180, 320) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(Math.random() < 0.7 ? c : pick(COLORS), 0.6),
rand(2.5, 4.5) * s, rand(1100, 1700),
{ gravity: 180, friction: 0.982, trailLength: 4, flicker: true });
}
}
// 2. ИВА — 30% белых (изящная, ниспадающая)
function fxIva(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(65 * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n + rand(-0.1, 0.1);
const sp = rand(120, 220) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(c, 0.3),
rand(2, 3.5) * s, rand(2000, 3000),
{ gravity: 120, friction: 0.975, trailLength: 6, flicker: true });
}
}
// 3. ХРИЗАНТЕМА — 45% белых
function fxHrizantema(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(60 * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n + rand(-0.05, 0.05);
const sp = rand(220, 360) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(Math.random() < 0.75 ? c : pick(COLORS), 0.45),
rand(2.5, 4) * s, rand(1800, 2600),
{ gravity: 60, friction: 0.96, trailLength: 7, flicker: true });
}
}
// 4. ГЕОРГИН — по слоям: горячий центр (0.7), средний (0.5), внешний (0.2)
function fxGeorgin(x, y, s) {
const c = pick(COLORS);
const layers = 3;
const layerChance = [0.2, 0.5, 0.7]; // внешний → внутренний
for (let l = 0; l < layers; l++) {
const n = Math.floor((50 - l * 10) * s);
const speedMul = 1 + l * 0.4;
const col = l === 0 ? c : (l === 1 ? pick(COLORS) : '#ffffff');
const chanceL = layerChance[l];
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n + l * 0.3 + rand(-0.05, 0.05);
const sp = rand(140, 240) * s * speedMul;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(col, chanceL), rand(2.5, 4) * s, rand(1400, 2000),
{ gravity: 130, friction: 0.972, trailLength: 5, flicker: true });
}
}
}
// 5. ПАЛЬМА — 35% белых (ветки преимущественно цветные)
function fxPalma(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(14 * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n;
const sp = rand(200, 300) * s;
for (let j = 0; j < 8; j++) {
const jitter = rand(-0.08, 0.08);
spawnSpark(x, y, Math.cos(a + jitter) * sp, Math.sin(a + jitter) * sp,
flashyColor(Math.random() < 0.6 ? c : pick(COLORS), 0.35),
rand(2.5, 4) * s, rand(1800, 2600),
{ gravity: 100, friction: 0.965, trailLength: 8, flicker: true });
}
}
}
// 6. КОЛЬЦО — 55% белых (контрастное)
function fxKolec(x, y, s) {
const c1 = pick(COLORS), c2 = pick(COLORS);
const n = Math.floor(80 * s);
const tilt = rand(0.3, 0.7);
const rot = rand(0, Math.PI);
const cosR = Math.cos(rot), sinR = Math.sin(rot);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n;
const sp = rand(200, 280) * s;
const ex = Math.cos(a) * sp, ey = Math.sin(a) * sp * tilt;
spawnSpark(x, y, ex * cosR - ey * sinR, ex * sinR + ey * cosR,
flashyColor(i % 2 === 0 ? c1 : c2, 0.55),
rand(2.5, 4) * s, rand(1300, 1900),
{ gravity: 160, friction: 0.983, trailLength: 4, flicker: true });
}
}
// 7. ПУЛЬСАР — 50% белых
function fxPulsar(x, y, s) {
const c = pick(COLORS);
for (let w = 0; w < 4; w++) {
setTimeout(() => {
const n = Math.floor((45 - w * 8) * s);
const speedMul = 1 + w * 0.35;
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n + rand(-0.1, 0.1);
const sp = rand(140, 240) * s * speedMul;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(Math.random() < 0.6 ? c : pick(COLORS), 0.5),
rand(2, 4) * s, rand(1000, 1600),
{ gravity: 200, friction: 0.984, trailLength: 4, flicker: true });
}
}, w * 180);
}
}
// 8. ДВОЙНОЙ — по слоям: внешний (0.4), внутренний (0.6)
function fxDvoinoy(x, y, s) {
const c1 = pick(COLORS), c2 = pick(COLORS);
const n1 = Math.floor(60 * s);
for (let i = 0; i < n1; i++) {
const a = (Math.PI * 2 * i) / n1 + rand(-0.08, 0.08);
const sp = rand(240, 340) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(c1, 0.4), rand(2.5, 4) * s, rand(1300, 1800),
{ gravity: 200, friction: 0.983, trailLength: 4, flicker: true });
}
setTimeout(() => {
const n2 = Math.floor(45 * s);
for (let i = 0; i < n2; i++) {
const a = (Math.PI * 2 * i) / n2 + rand(-0.1, 0.1);
const sp = rand(120, 200) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(c2, 0.6), rand(3, 5) * s, rand(1500, 2100),
{ gravity: 160, friction: 0.98, trailLength: 5, flicker: true });
}
}, 150);
}
// 9. ТРОЙНОЙ — по слоям: 0.3, 0.5, 0.7
function fxTroynoy(x, y, s) {
const colors = [pick(COLORS), pick(COLORS), pick(COLORS)];
const chances = [0.3, 0.5, 0.7];
for (let w = 0; w < 3; w++) {
setTimeout(() => {
const n = Math.floor((55 - w * 10) * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n + rand(-0.1, 0.1);
const sp = rand(180, 280) * s * (1 - w * 0.15);
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(colors[w], chances[w]),
rand(2.5, 4) * s, rand(1400, 2000),
{ gravity: 180, friction: 0.982, trailLength: 4, flicker: true });
}
}, w * 200);
}
}
// 10. СПИРАЛЬ — 45% белых
function fxSpiral(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(75 * s);
const turns = 3;
for (let i = 0; i < n; i++) {
const progress = i / n;
const a = progress * Math.PI * 2 * turns;
const sp = rand(180, 300) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(Math.random() < 0.7 ? c : pick(COLORS), 0.45),
rand(2.5, 4) * s, rand(1400, 2000),
{ gravity: 180, friction: 0.982, trailLength: 5, flicker: true });
}
}
// 11. ДВОЙНАЯ СПИРАЛЬ — 50% белых
function fxDvoynayaSpiral(x, y, s) {
const c1 = pick(COLORS), c2 = pick(COLORS);
const n = Math.floor(75 * s);
const turns = 2.5;
for (let i = 0; i < n; i++) {
const progress = i / n;
const a = progress * Math.PI * 2 * turns;
const sp = rand(180, 300) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(c1, 0.5), rand(2.5, 4) * s, rand(1400, 2000),
{ gravity: 180, friction: 0.982, trailLength: 5, flicker: true });
spawnSpark(x, y, Math.cos(a + Math.PI) * sp, Math.sin(a + Math.PI) * sp,
flashyColor(c2, 0.5), rand(2.5, 4) * s, rand(1400, 2000),
{ gravity: 180, friction: 0.982, trailLength: 5, flicker: true });
}
}
// 12. ШАРОВАЯ МОЛНИЯ — 65% белых (искрит)
function fxMolnija(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(90 * s);
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2;
const sp = rand(100, 400) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(c, 0.65),
rand(2, 4) * s, rand(800, 1800),
{ gravity: 280, friction: 0.978, trailLength: 6, flicker: true });
}
}
// 13. КОМЕТА — 40% белых (хвосты цветные)
function fxKometa(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(40 * s);
for (let i = 0; i < n; i++) {
const a = rand(0, Math.PI * 2);
const sp = rand(200, 400) * s;
const vx = Math.cos(a) * sp, vy = Math.sin(a) * sp;
for (let j = 0; j < 5; j++) {
const f = 1 - j * 0.08;
spawnSpark(x, y, vx * f, vy * f,
flashyColor(c, 0.4),
rand(2, 3.5) * s, rand(1600, 2400),
{ gravity: 120, friction: 0.975, trailLength: 9, flicker: true });
}
}
}
// 14. ЗВЕЗДА — 50% белых
function fxZvezda(x, y, s) {
const c = pick(COLORS);
const points = 5;
const n = Math.floor(75 * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n;
const spikes = points * 2;
const localA = a * spikes / (Math.PI * 2);
const frac = localA - Math.floor(localA);
const radiusMul = 0.5 + 0.5 * Math.abs(Math.cos(frac * Math.PI));
const sp = rand(200, 320) * s * radiusMul;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(Math.random() < 0.7 ? c : pick(COLORS), 0.5),
rand(2.5, 4) * s, rand(1400, 2000),
{ gravity: 170, friction: 0.982, trailLength: 5, flicker: true });
}
}
// 15. РОМБ — 45% белых
function fxRomb(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(70 * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n;
const k = Math.abs(Math.cos(a)) + Math.abs(Math.sin(a));
const sp = rand(200, 300) * s * k;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(Math.random() < 0.7 ? c : pick(COLORS), 0.45),
rand(2.5, 4) * s, rand(1400, 2000),
{ gravity: 180, friction: 0.982, trailLength: 4, flicker: true });
}
}
// 16. КВАДРАТ — 45% белых
function fxKvadrat(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(72 * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n;
const cosA = Math.cos(a), sinA = Math.sin(a);
const maxV = Math.max(Math.abs(cosA), Math.abs(sinA));
const sp = rand(200, 300) * s / maxV;
spawnSpark(x, y, cosA * sp, sinA * sp,
flashyColor(Math.random() < 0.7 ? c : pick(COLORS), 0.45),
rand(2.5, 4) * s, rand(1400, 2000),
{ gravity: 180, friction: 0.982, trailLength: 4, flicker: true });
}
}
// 17. ВОСЬМЁРКА — 50% белых
function fxVosmerka(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(80 * s);
for (let i = 0; i < n; i++) {
const t = (Math.PI * 2 * i) / n;
const sp = rand(180, 300) * s;
const vx = Math.cos(t) * sp;
const vy = Math.sin(t * 2) * sp * 0.6;
spawnSpark(x, y, vx, vy,
flashyColor(Math.random() < 0.7 ? c : pick(COLORS), 0.5),
rand(2.5, 4) * s, rand(1400, 2000),
{ gravity: 180, friction: 0.982, trailLength: 5, flicker: true });
}
}
// 18. КОНУС — 40% белых
function fxKonus(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(65 * s);
const baseA = -Math.PI / 2;
for (let i = 0; i < n; i++) {
const a = baseA + rand(-1.0, 1.0);
const sp = rand(200, 340) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(Math.random() < 0.7 ? c : pick(COLORS), 0.4),
rand(2.5, 4) * s, rand(1400, 2000),
{ gravity: 200, friction: 0.982, trailLength: 5, flicker: true });
}
}
// 19. ВЕЕР — 50% белых
function fxVeer(x, y, s) {
const c1 = pick(COLORS), c2 = pick(COLORS);
const n = Math.floor(55 * s);
const baseA = -Math.PI / 2;
for (let i = 0; i < n; i++) {
const a = baseA + (i / n - 0.5) * 2.4;
const sp = rand(220, 340) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(i % 2 === 0 ? c1 : c2, 0.5),
rand(2.5, 4.5) * s, rand(1500, 2100),
{ gravity: 220, friction: 0.982, trailLength: 5, flicker: true });
}
}
// 20. ФОНТАН — 45% белых
function fxFontan(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(50 * s);
for (let i = 0; i < n; i++) {
const a = -Math.PI / 2 + rand(-0.6, 0.6);
const sp = rand(250, 420) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(c, 0.45),
rand(2.5, 4) * s, rand(1800, 2600),
{ gravity: 350, friction: 0.985, trailLength: 5, flicker: true });
}
}
// 21. ХВОСТ ПАВЛИНА — 40% белых
function fxPavlin(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(45 * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n;
const sp = rand(180, 260) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(c, 0.4), rand(3, 4.5) * s, rand(1600, 2200),
{ gravity: 140, friction: 0.975, trailLength: 6, flicker: true });
for (let j = 0; j < 4; j++) {
const a2 = a + rand(-0.25, 0.25);
const sp2 = sp * rand(0.85, 1.05);
spawnSpark(x, y, Math.cos(a2) * sp2, Math.sin(a2) * sp2,
flashyColor(pick(COLORS), 0.4), rand(2.5, 4) * s, rand(1800, 2400),
{ gravity: 130, friction: 0.972, trailLength: 7, flicker: true });
}
}
}
// 22. ПАУК — 55% белых
function fxPauk(x, y, s) {
const c = pick(COLORS);
const legs = 12;
for (let i = 0; i < legs; i++) {
const a = (Math.PI * 2 * i) / legs;
const sp = rand(200, 300) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(c, 0.55), rand(3, 5) * s, rand(1500, 2200),
{ gravity: 160, friction: 0.975, trailLength: 7, flicker: true });
setTimeout(() => {
spawnSpark(x, y, Math.cos(a + 0.1) * sp, Math.sin(a + 0.1) * sp,
flashyColor(c, 0.55), rand(3, 5) * s, rand(1500, 2200),
{ gravity: 160, friction: 0.975, trailLength: 7, flicker: true });
}, 30);
}
}
// 23. КОРОНА — по слоям: верхние лучи (0.5), нижняя дуга (0.35)
function fxKorona(x, y, s) {
const c1 = pick(COLORS), c2 = pick(COLORS);
const n = Math.floor(75 * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n;
const sp = rand(200, 300) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp * 0.5,
flashyColor(i % 2 === 0 ? c1 : c2, 0.5),
rand(2.5, 4) * s, rand(1800, 2400),
{ gravity: 100, friction: 0.97, trailLength: 8, flicker: true });
}
for (let i = 0; i < 28 * s; i++) {
const a = rand(0, Math.PI);
const sp = rand(150, 250) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(c2, 0.35), rand(2, 3.5) * s, rand(1400, 2000),
{ gravity: 200, friction: 0.98, trailLength: 5 });
}
}
// 24. ГАЛАКТИКА — 45% белых
function fxGalaktika(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(110 * s);
const turns = 4;
for (let i = 0; i < n; i++) {
const progress = i / n;
const a = progress * Math.PI * 2 * turns;
const sp = rand(60, 300) * s * (0.3 + progress);
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(Math.random() < 0.6 ? c : pick(COLORS), 0.45),
rand(2, 4) * s, rand(1600, 2600),
{ gravity: 140, friction: 0.976, trailLength: 6, flicker: true });
}
}
// 25. КОЛЬЦО САТУРНА — кольцо (0.55), сфера (0.6)
function fxSaturn(x, y, s) {
const c1 = pick(COLORS), c2 = pick(COLORS);
const tilt = rand(0.15, 0.3);
const rot = rand(0, Math.PI);
const cosR = Math.cos(rot), sinR = Math.sin(rot);
const n = Math.floor(90 * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n;
const sp = rand(200, 320) * s;
const ex = Math.cos(a) * sp, ey = Math.sin(a) * sp * tilt;
spawnSpark(x, y, ex * cosR - ey * sinR, ex * sinR + ey * cosR,
flashyColor(i % 2 === 0 ? c1 : c2, 0.55),
rand(2.5, 4) * s, rand(1500, 2200),
{ gravity: 150, friction: 0.981, trailLength: 5, flicker: true });
}
for (let i = 0; i < 28 * s; i++) {
const a = Math.random() * Math.PI * 2;
const sp = rand(30, 80) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(pick(COLORS), 0.6), rand(2, 3.5) * s, rand(1200, 1800),
{ gravity: 180, friction: 0.98, trailLength: 4 });
}
}
// 26. ДВОЙНОЕ КОЛЬЦО — внешнее (0.5), внутреннее (0.7)
function fxDvoynoeKolec(x, y, s) {
const c1 = pick(COLORS), c2 = pick(COLORS);
const n = Math.floor(55 * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n;
const sp = rand(240, 320) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(c1, 0.5), rand(2.5, 4) * s, rand(1300, 1800),
{ gravity: 170, friction: 0.983, trailLength: 4, flicker: true });
}
setTimeout(() => {
const n2 = Math.floor(45 * s);
const rot = rand(0, Math.PI);
const tilt = rand(0.3, 0.7);
const cosR = Math.cos(rot), sinR = Math.sin(rot);
for (let i = 0; i < n2; i++) {
const a = (Math.PI * 2 * i) / n2;
const sp = rand(180, 260) * s;
const ex = Math.cos(a) * sp, ey = Math.sin(a) * sp * tilt;
spawnSpark(x, y, ex * cosR - ey * sinR, ex * sinR + ey * cosR,
flashyColor(c2, 0.7), rand(2.5, 4) * s, rand(1400, 2000),
{ gravity: 160, friction: 0.982, trailLength: 5, flicker: true });
}
}, 250);
}
// 27. РАДУГА — 30% белых (цвета важны)
function fxRaduga(x, y, s) {
const rainbow = ['#ff0000', '#ff8800', '#ffff00', '#00ff00', '#00ccff', '#4400ff', '#cc00ff'];
const bands = rainbow.length;
for (let b = 0; b < bands; b++) {
const c = rainbow[b];
const n = Math.floor(14 * s);
const angleOffset = b * 0.08;
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n + angleOffset;
const sp = (200 + b * 15) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(c, 0.3), rand(2.5, 4) * s, rand(1400, 2000),
{ gravity: 180, friction: 0.982, trailLength: 4, flicker: true });
}
}
}
// 28. ЦВЕТОК — лепестки (0.4), сердцевина (0.7)
function fxCvetok(x, y, s) {
const petals = 6;
const baseC = pick(COLORS);
for (let p = 0; p < petals; p++) {
const baseA = (Math.PI * 2 * p) / petals;
const n = Math.floor(12 * s);
for (let i = 0; i < n; i++) {
const spread = (i / n - 0.5) * 0.7;
const a = baseA + spread;
const sp = rand(150, 280) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(p % 2 === 0 ? baseC : pick(COLORS), 0.4),
rand(2.5, 4.5) * s, rand(1500, 2200),
{ gravity: 180, friction: 0.98, trailLength: 6, flicker: true });
}
}
// Сердцевина — яркая
for (let i = 0; i < 14 * s; i++) {
const a = Math.random() * Math.PI * 2;
const sp = rand(40, 90) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor('#ffff88', 0.7), rand(2, 3.5) * s, rand(1200, 1800),
{ gravity: 150, friction: 0.98, trailLength: 4, flicker: true });
}
}
// 29. ВОЛНА — по рядам: 0.4, 0.5, 0.6, 0.7
function fxVolna(x, y, s) {
const c1 = pick(COLORS), c2 = pick(COLORS);
const rows = 4;
const chances = [0.4, 0.5, 0.6, 0.7];
for (let r = 0; r < rows; r++) {
setTimeout(() => {
const n = Math.floor(55 * s);
const offset = r * 0.3;
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n;
const waveA = a + Math.sin(a * 4 + offset) * 0.3;
const sp = rand(180, 280) * s;
spawnSpark(x, y, Math.cos(waveA) * sp, Math.sin(waveA) * sp,
flashyColor(r % 2 === 0 ? c1 : c2, chances[r]),
rand(2.5, 4) * s, rand(1400, 2000),
{ gravity: 170, friction: 0.982, trailLength: 5, flicker: true });
}
}, r * 100);
}
}
// 30. КАСКАД — 45% белых
function fxKaskad(x, y, s) {
const c = pick(COLORS);
const waves = 5;
for (let w = 0; w < waves; w++) {
setTimeout(() => {
const n = Math.floor((38 - w * 5) * s);
const sp0 = (300 - w * 30) * s;
for (let i = 0; i < n; i++) {
const a = -Math.PI / 2 + rand(-0.6, 0.6);
const sp = sp0 * rand(0.8, 1.2);
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(Math.random() < 0.6 ? c : pick(COLORS), 0.45),
rand(2.5, 4) * s, rand(1500, 2200),
{ gravity: 320, friction: 0.982, trailLength: 5, flicker: true });
}
}, w * 180);
}
}
// 31. МЕТЕОР — 50% белых (головы ярче)
function fxMeteor(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(45 * s);
for (let i = 0; i < n; i++) {
const a = rand(0, Math.PI * 2);
const sp = rand(250, 450) * s;
const vx = Math.cos(a) * sp, vy = Math.sin(a) * sp;
for (let j = 0; j < 6; j++) {
const f = 1 - j * 0.1;
spawnSpark(x, y, vx * f, vy * f,
flashyColor(c, j === 0 ? 0.7 : 0.4),
rand(2, 3.5) * s, rand(1200, 2000),
{ gravity: 100, friction: 0.98, trailLength: 10, flicker: true });
}
}
}
// 32. СВЕРХНОВАЯ — 70% белых
function fxSverhnova(x, y, s) {
const c = pick(COLORS);
for (let i = 0; i < 55 * s; i++) {
const a = Math.random() * Math.PI * 2;
const sp = rand(300, 500) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor('#ffffff', 0.7), rand(2, 4) * s, rand(600, 1200),
{ gravity: 150, friction: 0.99, trailLength: 8, flicker: true });
}
setTimeout(() => {
const n = Math.floor(75 * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n + rand(-0.1, 0.1);
const sp = rand(100, 200) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(Math.random() < 0.5 ? c : pick(COLORS), 0.55),
rand(3, 5) * s, rand(1800, 2600),
{ gravity: 200, friction: 0.978, trailLength: 6, flicker: true });
}
}, 100);
}
// 33. ГИПЕРНОВАЯ — 75% белых (вспышка!)
function fxGipernova(x, y, s) {
spawnFlash(x, y, s * 1.4);
const c1 = pick(COLORS), c2 = pick(COLORS), c3 = pick(COLORS);
for (let i = 0; i < 55 * s; i++) {
const a = Math.random() * Math.PI * 2;
const sp = rand(350, 550) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor('#ffffff', 0.75), rand(2, 4) * s, rand(500, 1000),
{ gravity: 180, friction: 0.99, trailLength: 8, flicker: true });
}
setTimeout(() => {
const n = Math.floor(90 * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n + rand(-0.1, 0.1);
const sp = rand(150, 320) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(i % 3 === 0 ? c1 : (i % 3 === 1 ? c2 : c3), 0.5),
rand(2.5, 4.5) * s, rand(1800, 2800),
{ gravity: 160, friction: 0.978, trailLength: 6, flicker: true });
}
}, 120);
setTimeout(() => {
for (let i = 0; i < 45 * s; i++) {
const a = (Math.PI * 2 * i) / (45 * s) + rand(-0.2, 0.2);
const sp = rand(80, 180) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(pick(COLORS), 0.4),
rand(2, 4) * s, rand(2200, 3000),
{ gravity: 100, friction: 0.972, trailLength: 8, flicker: true });
}
}, 280);
}
// 34. ЦИКЛОН — 45% белых
function fxCiklon(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(90 * s);
const turns = 5;
for (let i = 0; i < n; i++) {
const progress = i / n;
const a = progress * Math.PI * 2 * turns;
const sp = rand(100, 300) * s * progress;
const perpA = a + Math.PI / 2;
spawnSpark(x, y, Math.cos(a) * sp + Math.cos(perpA) * 30 * s,
Math.sin(a) * sp + Math.sin(perpA) * 30 * s,
flashyColor(Math.random() < 0.6 ? c : pick(COLORS), 0.45),
rand(2, 4) * s, rand(1600, 2400),
{ gravity: 150, friction: 0.976, trailLength: 6, flicker: true });
}
}
// 35. ПЕСОЧНЫЕ ЧАСЫ — 50% белых
function fxPesochnyeChasy(x, y, s) {
const c1 = pick(COLORS), c2 = pick(COLORS);
const n = Math.floor(65 * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n;
const sp = rand(180, 280) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp * 0.8 - 40 * s,
flashyColor(c1, 0.5), rand(2.5, 4) * s, rand(1400, 2000),
{ gravity: 220, friction: 0.982, trailLength: 4, flicker: true });
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp * 0.8 + 40 * s,
flashyColor(c2, 0.5), rand(2.5, 4) * s, rand(1400, 2000),
{ gravity: 220, friction: 0.982, trailLength: 4, flicker: true });
}
}
// 36. БАБОЧКА — 50% белых
function fxBabochka(x, y, s) {
const c1 = pick(COLORS), c2 = pick(COLORS);
const n = Math.floor(75 * s);
for (let i = 0; i < n; i++) {
const t = (i / n) * Math.PI * 2;
const r = Math.abs(Math.sin(t)) * 200 * s;
const vx = Math.cos(t) * r;
const vy = Math.sin(t) * r * 0.7;
spawnSpark(x, y, vx, vy,
flashyColor(i % 2 === 0 ? c1 : c2, 0.5),
rand(2.5, 4) * s, rand(1600, 2200),
{ gravity: 170, friction: 0.98, trailLength: 5, flicker: true });
}
}
// 37. СЕРДЦЕ — 55% белых (романтичное)
function fxSerdce(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(85 * s);
for (let i = 0; i < n; i++) {
const t = (i / n) * Math.PI * 2;
const hx = 16 * Math.pow(Math.sin(t), 3);
const hy = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));
const sp = 12 * s;
spawnSpark(x, y, hx * sp, hy * sp,
flashyColor(Math.random() < 0.75 ? c : pick(COLORS), 0.55),
rand(2.5, 4) * s, rand(1600, 2200),
{ gravity: 200, friction: 0.98, trailLength: 4, flicker: true });
}
}
// 38. ЗВЕЗДА ДАВИДА — 50% белых
function fxZvezdaDavida(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(75 * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n;
const k = Math.abs(Math.cos(a * 3)) * 0.6 + 0.4;
const sp = rand(200, 300) * s * k;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(Math.random() < 0.75 ? c : pick(COLORS), 0.5),
rand(2.5, 4) * s, rand(1500, 2100),
{ gravity: 180, friction: 0.982, trailLength: 5, flicker: true });
}
}
// 39. ПЕНТАГРАММА — 40% белых (линии чётче)
function fxPentagramma(x, y, s) {
const c = pick(COLORS);
const step = Math.PI * 2 / 5;
const seq = [0, 2, 4, 1, 3, 0];
for (let i = 0; i < seq.length - 1; i++) {
const a1 = seq[i] * step - Math.PI / 2;
const a2 = seq[i + 1] * step - Math.PI / 2;
const steps = Math.floor(14 * s);
for (let j = 0; j < steps; j++) {
const t = j / steps;
const a = a1 + (a2 - a1) * t;
const sp = rand(220, 300) * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(c, 0.4), rand(2.5, 4) * s, rand(1600, 2200),
{ gravity: 180, friction: 0.982, trailLength: 5, flicker: true });
}
}
}
// 40. ГЕКСАГОН — 45% белых
function fxHexagon(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(70 * s);
for (let i = 0; i < n; i++) {
const a = (Math.PI * 2 * i) / n;
const k = Math.cos((a % (Math.PI / 3)) - Math.PI / 6);
const sp = rand(200, 300) * s / k;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(Math.random() < 0.75 ? c : pick(COLORS), 0.45),
rand(2.5, 4) * s, rand(1500, 2100),
{ gravity: 180, friction: 0.982, trailLength: 5, flicker: true });
}
}
// 41. ЦИФЕРБЛАТ — деления (0.5), стрелки (0.7)
function fxCiferblat(x, y, s) {
const c = pick(COLORS);
for (let h = 0; h < 12; h++) {
const a = (Math.PI * 2 * h) / 12 - Math.PI / 2;
const sp = (h % 3 === 0 ? 300 : 240) * s;
const size = h % 3 === 0 ? 4.5 : 3;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(c, 0.5), size * s, rand(1600, 2200),
{ gravity: 180, friction: 0.982, trailLength: 4, flicker: true });
}
const hourA = rand(0, Math.PI * 2);
const minA = rand(0, Math.PI * 2);
for (let i = 0; i < 18; i++) {
const t = i / 18;
spawnSpark(x, y, Math.cos(hourA) * 150 * s * t, Math.sin(hourA) * 150 * s * t,
flashyColor('#ffffff', 0.7), 3 * s, rand(1400, 2000),
{ gravity: 160, friction: 0.98, trailLength: 5, flicker: true });
spawnSpark(x, y, Math.cos(minA) * 220 * s * t, Math.sin(minA) * 220 * s * t,
flashyColor('#ffff88', 0.5), 3 * s, rand(1400, 2000),
{ gravity: 160, friction: 0.98, trailLength: 5, flicker: true });
}
}
// 42. ЛАБИРИНТ — 40% белых
function fxLabirint(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(75 * s);
for (let i = 0; i < n; i++) {
const t = i / n;
const a = t * Math.PI * 2 * 6;
const sp = 50 + t * 250 * s;
spawnSpark(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
flashyColor(Math.random() < 0.7 ? c : pick(COLORS), 0.4),
rand(2, 3.5) * s, rand(1800, 2600),
{ gravity: 130, friction: 0.975, trailLength: 6, flicker: true });
}
}
// 43. ПАРАБОЛА — 45% белых
function fxParabola(x, y, s) {
const c = pick(COLORS);
const n = Math.floor(55 * s);
const angle = rand(0, Math.PI * 2);
for (let i = 0; i < n; i++) {
const t = (i / n - 0.5) * 2;
const sp = rand(220, 340) * s;
const local = t * 0.8;
const a = angle + local;
const speedMul = 1 - Math.abs(t) * 0.3;
spawnSpark(x, y, Math.cos(a) * sp * speedMul, Math.sin(a) * sp * speedMul,
flashyColor(Math.random() < 0.7 ? c : pick(COLORS), 0.45),
rand(2.5, 4.5) * s, rand(1600, 2200),
{ gravity: 240, friction: 0.982, trailLength: 5, flicker: true });
}
}
const VARIANTS = [
{ name: 'Пион', fn: fxPion },
{ name: 'Ива', fn: fxIva },
{ name: 'Хризантема', fn: fxHrizantema },
{ name: 'Георгин', fn: fxGeorgin },
{ name: 'Пальма', fn: fxPalma },
{ name: 'Кольцо', fn: fxKolec },
{ name: 'Пульсар', fn: fxPulsar },
{ name: 'Двойной', fn: fxDvoinoy },
{ name: 'Тройной', fn: fxTroynoy },
{ name: 'Спираль', fn: fxSpiral },
{ name: 'Двойная спираль', fn: fxDvoynayaSpiral },
{ name: 'Шаровая молния', fn: fxMolnija },
{ name: 'Комета', fn: fxKometa },
{ name: 'Звезда', fn: fxZvezda },
{ name: 'Ромб', fn: fxRomb },
{ name: 'Квадрат', fn: fxKvadrat },
{ name: 'Восьмёрка', fn: fxVosmerka },
{ name: 'Конус', fn: fxKonus },
{ name: 'Веер', fn: fxVeer },
{ name: 'Фонтан', fn: fxFontan },
{ name: 'Хвост павлина', fn: fxPavlin },
{ name: 'Паук', fn: fxPauk },
{ name: 'Корона', fn: fxKorona },
{ name: 'Галактика', fn: fxGalaktika },
{ name: 'Кольцо Сатурна', fn: fxSaturn },
{ name: 'Двойное кольцо', fn: fxDvoynoeKolec },
{ name: 'Радуга', fn: fxRaduga },
{ name: 'Цветок', fn: fxCvetok },
{ name: 'Волна', fn: fxVolna },
{ name: 'Каскад', fn: fxKaskad },
{ name: 'Метеор', fn: fxMeteor },
{ name: 'Сверхновая', fn: fxSverhnova },
{ name: 'Гиперновая', fn: fxGipernova },
{ name: 'Циклон', fn: fxCiklon },
{ name: 'Песочные часы', fn: fxPesochnyeChasy },
{ name: 'Бабочка', fn: fxBabochka },
{ name: 'Сердце', fn: fxSerdce },
{ name: 'Звезда Давида', fn: fxZvezdaDavida },
{ name: 'Пентаграмма', fn: fxPentagramma },
{ name: 'Гексагон', fn: fxHexagon },
{ name: 'Циферблат', fn: fxCiferblat },
{ name: 'Лабиринт', fn: fxLabirint },
{ name: 'Парабола', fn: fxParabola },
];
// ============================================================
// БОЛЬШОЙ ВЗРЫВ — случайные эффекты каждый раз
// ============================================================
function launchBigExplosion(x, y) {
const nameEl = document.getElementById('variant-name');
nameEl.textContent = '★ БОЛЬШОЙ ВЗРЫВ ★';
nameEl.style.opacity = '1';
nameEl.style.color = '#ff4400';
// 5 ракет вокруг — случайные эффекты
const count = 5;
for (let i = 0; i < count; i++) {
setTimeout(() => {
const angle = (Math.PI * 2 * i) / count;
const dist = rand(150, 260);
const tx = x + Math.cos(angle) * dist;
const ty = y + Math.sin(angle) * dist * 0.6;
launchRocket(tx, ty, rand(0.9, 1.2), Math.floor(Math.random() * VARIANTS.length));
}, i * 180);
}
// Центральный мега-взрыв — 3 случайных эффекта
setTimeout(() => {
spawnFlash(x, y, 3.5);
const shuffled = [...VARIANTS].sort(() => Math.random() - 0.5);
const bigVariants = shuffled.slice(0, 3);
bigVariants[0].fn(x, y, 2.0);
setTimeout(() => bigVariants[1].fn(x, y, 1.8), 150);
setTimeout(() => bigVariants[2].fn(x, y, 1.6), 300);
// Финальный залп
setTimeout(() => {
const finalCount = 120;
for (let i = 0; i < finalCount; i++) {
const angle = (Math.PI * 2 * i) / finalCount + rand(-0.05, 0.05);
const speed = rand(300, 550);
spawnSpark(x, y, Math.cos(angle) * speed, Math.sin(angle) * speed,
flashyColor(pick(COLORS), 0.6), rand(2, 4), rand(1800, 2600),
{ gravity: 150, friction: 0.98, trailLength: 5, flicker: true });
}
}, 550);
for (let i = 0; i < 12; i++) {
spawnSmoke(x + rand(-80, 80), y + rand(-80, 80), 2.2);
}
}, 1100);
setTimeout(() => {
nameEl.style.color = '#ffd600';
}, 4000);
}
// ============================================================
// ОТРИСОВКА
// ============================================================
function drawParticle(p) {
const t = p.age / p.life;
if (t >= 1) return;
const opacity = 1 - t;
const [r, g, b] = p.colorRgb;
let alpha = opacity;
if (p.flicker) {
alpha *= 0.55 + 0.45 * Math.abs(Math.sin(p.flickerSeed + p.age * 40));
}
const dx = p.x - p.prevX;
const dy = p.y - p.prevY;
const dist = Math.sqrt(dx * dx + dy * dy);
const coreSize = p.size * (1 - t * 0.6);
if (coreSize > 0.3) {
ctx.beginPath();
ctx.arc(p.x, p.y, coreSize, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${r},${g},${b},${alpha * 0.9})`;
ctx.fill();
}
if (dist > 0.5) {
const stretch = Math.min(1 + dist * p.trailLength * 0.35, 10);
const tailX = p.x - dx * stretch;
const tailY = p.y - dy * stretch;
const gradient = ctx.createLinearGradient(p.x, p.y, tailX, tailY);
gradient.addColorStop(0, `rgba(${r},${g},${b},${alpha})`);
gradient.addColorStop(0.4, `rgba(${r},${g},${b},${alpha * 0.5})`);
gradient.addColorStop(1, `rgba(${r},${g},${b},0)`);
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(tailX, tailY);
ctx.strokeStyle = gradient;
ctx.lineWidth = coreSize * 1.2;
ctx.lineCap = 'round';
ctx.stroke();
}
if (coreSize > 1.5) {
ctx.beginPath();
ctx.arc(p.x, p.y, coreSize * 2.5, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${r},${g},${b},${alpha * 0.15})`;
ctx.fill();
}
}
function drawFlash(f) {
const t = f.age / f.life;
if (t >= 1) return;
const alpha = 1 - t;
const size = 160 * f.scale * (0.3 + t * 1.8);
const [r, g, b] = f.color;
const grad = ctx.createRadialGradient(f.x, f.y, 0, f.x, f.y, size);
grad.addColorStop(0, `rgba(255,255,220,${alpha * 0.95})`);
grad.addColorStop(0.3, `rgba(${r},${g},${b},${alpha * 0.6})`);
grad.addColorStop(1, `rgba(${r},${g},${b},0)`);
ctx.beginPath();
ctx.arc(f.x, f.y, size, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
}
function drawSmoke(sm) {
const t = sm.age / sm.life;
if (t >= 1) return;
const alpha = (1 - t) * 0.35;
const size = sm.size * (1 + t * 1.8);
const x = sm.x + sm.driftX * t;
const y = sm.y + sm.driftY * t;
const grad = ctx.createRadialGradient(x, y, 0, x, y, size);
grad.addColorStop(0, `rgba(120,120,140,${alpha})`);
grad.addColorStop(0.5, `rgba(80,80,100,${alpha * 0.5})`);
grad.addColorStop(1, 'rgba(80,80,100,0)');
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
}
function drawRocket(r) {
if (r.x === undefined) return;
ctx.beginPath();
ctx.arc(r.x, r.y, 3, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
ctx.fill();
const [cr, cg, cb] = r.colorRgb;
const grad = ctx.createRadialGradient(r.x, r.y, 0, r.x, r.y, 20);
grad.addColorStop(0, `rgba(255,255,200,0.9)`);
grad.addColorStop(0.3, `rgba(${cr},${cg},${cb},0.5)`);
grad.addColorStop(1, `rgba(${cr},${cg},${cb},0)`);
ctx.beginPath();
ctx.arc(r.x, r.y, 20, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
}
// ============================================================
// ГЛАВНЫЙ ЦИКЛ
// ============================================================
let lastTime = performance.now();
function loop(now) {
const dt = Math.min((now - lastTime) / 1000, 0.05);
lastTime = now;
ctx.clearRect(0, 0, W, H);
updateRockets(dt, now);
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.prevX = p.x;
p.prevY = p.y;
p.age += dt;
if (p.age >= p.life) {
releaseParticle(p);
particles.splice(i, 1);
continue;
}
p.vy += p.gravity * dt;
const fr = Math.pow(p.friction, dt * 60);
p.vx *= fr;
p.vy *= fr;
p.x += p.vx * dt;
p.y += p.vy * dt;
}
for (let i = flashes.length - 1; i >= 0; i--) {
flashes[i].age += dt;
if (flashes[i].age >= flashes[i].life) flashes.splice(i, 1);
}
for (let i = smokes.length - 1; i >= 0; i--) {
smokes[i].age += dt;
if (smokes[i].age >= smokes[i].life) smokes.splice(i, 1);
}
for (let i = 0; i < smokes.length; i++) drawSmoke(smokes[i]);
ctx.globalCompositeOperation = 'lighter';
for (let i = 0; i < flashes.length; i++) drawFlash(flashes[i]);
for (let i = 0; i < rockets.length; i++) drawRocket(rockets[i]);
for (let i = 0; i < particles.length; i++) drawParticle(particles[i]);
ctx.globalCompositeOperation = 'source-over';
requestAnimationFrame(loop);
}
// ============================================================
// UI
// ============================================================
const nameEl = document.getElementById('variant-name');
const counterEl = document.getElementById('counter');
const selectEl = document.getElementById('variant-select');
VARIANTS.forEach((v, i) => {
const opt = document.createElement('option');
opt.value = i;
opt.textContent = `${i + 1}. ${v.name}`;
selectEl.appendChild(opt);
});
const randomOpt = document.createElement('option');
randomOpt.value = '-1';
randomOpt.textContent = '🎲 Случайный';
selectEl.insertBefore(randomOpt, selectEl.firstChild);
selectEl.value = '-1';
counterEl.textContent = `Всего эффектов: ${VARIANTS.length} • Белых искр: до ${Math.round(WHITE_CHANCE * 100)}%`;
// ============================================================
// УПРАВЛЕНИЕ
// ============================================================
function getSelectedIndex() {
const sel = selectEl.value;
return sel === '-1' ? null : parseInt(sel, 10);
}
document.addEventListener('click', (e) => {
if (e.target.closest('button') || e.target.closest('select')) return;
launchRocket(e.clientX, e.clientY, rand(0.9, 1.3), getSelectedIndex());
});
document.getElementById('btn-fire').addEventListener('click', () => {
launchRocket(W / 2, H * 0.45, rand(1.0, 1.4), getSelectedIndex());
});
document.getElementById('btn-big').addEventListener('click', () => {
launchBigExplosion(W / 2, H * 0.4);
});
document.getElementById('btn-random').addEventListener('click', () => {
launchRocket(W * rand(0.25, 0.75), H * rand(0.2, 0.5), rand(1.0, 1.4), null);
});
setTimeout(() => {
launchRocket(W / 2, H * 0.45, 1.2, 0);
}, 600);
requestAnimationFrame(loop);
})();
</script>
</body>
</html>