import { ButtonEffect } from "../effect-base.js"; /** * Fire background effect * Animated flames rising from bottom using particles */ export class FireEffect extends ButtonEffect { constructor() { super({ id: "bg-fire", name: "Fire", type: "background-animation", category: "Background Animations", renderOrder: 10, }); this.particles = []; this.initialized = false; } defineControls() { return [ { id: "animate-fire", type: "checkbox", label: "Fire Effect", defaultValue: false, }, { id: "fire-intensity", type: "range", label: "Intensity", defaultValue: 50, min: 20, max: 100, step: 5, showWhen: "animate-fire", description: "Number of flame particles", }, { id: "fire-height", type: "range", label: "Flame Height", defaultValue: 0.6, min: 0.3, max: 1, step: 0.1, showWhen: "animate-fire", description: "How high flames reach", }, { id: "fire-speed", type: "range", label: "Speed", defaultValue: 1, min: 0.3, max: 3, step: 0.1, showWhen: "animate-fire", description: "Speed of rising flames", }, { id: "fire-color-scheme", type: "select", label: "Color Scheme", defaultValue: "normal", options: [ { value: "normal", label: "Normal Fire" }, { value: "blue", label: "Blue Flame" }, { value: "green", label: "Green Flame" }, { value: "purple", label: "Purple Flame" }, { value: "white", label: "White Hot" }, ], showWhen: "animate-fire", description: "Flame color", }, ]; } isEnabled(controlValues) { return controlValues["animate-fire"] === true; } getFireColors(scheme) { switch (scheme) { case "normal": return [ { r: 255, g: 60, b: 0 }, // Red-orange { r: 255, g: 140, b: 0 }, // Orange { r: 255, g: 200, b: 0 }, // Yellow ]; case "blue": return [ { r: 0, g: 100, b: 255 }, // Blue { r: 100, g: 180, b: 255 }, // Light blue { r: 200, g: 230, b: 255 }, // Very light blue ]; case "green": return [ { r: 0, g: 200, b: 50 }, // Green { r: 100, g: 255, b: 100 }, // Light green { r: 200, g: 255, b: 150 }, // Very light green ]; case "purple": return [ { r: 150, g: 0, b: 255 }, // Purple { r: 200, g: 100, b: 255 }, // Light purple { r: 230, g: 180, b: 255 }, // Very light purple ]; case "white": return [ { r: 255, g: 200, b: 150 }, // Warm white { r: 255, g: 240, b: 200 }, // Light white { r: 255, g: 255, b: 255 }, // Pure white ]; default: return [ { r: 255, g: 60, b: 0 }, { r: 255, g: 140, b: 0 }, { r: 255, g: 200, b: 0 }, ]; } } apply(context, controlValues, animState, renderData) { if (!animState) return; const intensity = controlValues["fire-intensity"] || 50; const height = controlValues["fire-height"] || 0.6; const speed = controlValues["fire-speed"] || 1; const colorScheme = controlValues["fire-color-scheme"] || "normal"; const colors = this.getFireColors(colorScheme); const maxHeight = renderData.height * height; // Spawn new particles at the bottom for (let i = 0; i < intensity / 10; i++) { this.particles.push({ x: Math.random() * renderData.width, y: renderData.height, vx: (Math.random() - 0.5) * 1.5, vy: -(2 + Math.random() * 3) * speed, size: 2 + Math.random() * 6, life: 1.0, colorIndex: Math.random(), }); } // Update and draw particles this.particles = this.particles.filter((particle) => { // Update position particle.x += particle.vx; particle.y += particle.vy; // Add some turbulence particle.vx += (Math.random() - 0.5) * 0.2; particle.vy *= 0.98; // Slow down as they rise // Fade out based on height and time const heightRatio = (renderData.height - particle.y) / renderData.height; particle.life -= 0.015; if (particle.life > 0 && particle.y > renderData.height - maxHeight) { // Choose color based on life (hotter at bottom, cooler at top) const colorProgress = 1 - particle.life; const colorIdx = Math.floor(colorProgress * (colors.length - 1)); const colorBlend = (colorProgress * (colors.length - 1)) % 1; const c1 = colors[Math.min(colorIdx, colors.length - 1)]; const c2 = colors[Math.min(colorIdx + 1, colors.length - 1)]; const r = Math.floor(c1.r + (c2.r - c1.r) * colorBlend); const g = Math.floor(c1.g + (c2.g - c1.g) * colorBlend); const b = Math.floor(c1.b + (c2.b - c1.b) * colorBlend); // Draw particle with gradient const gradient = context.createRadialGradient( particle.x, particle.y, 0, particle.x, particle.y, particle.size ); gradient.addColorStop( 0, `rgba(${r}, ${g}, ${b}, ${particle.life * 0.8})` ); gradient.addColorStop( 0.5, `rgba(${r}, ${g}, ${b}, ${particle.life * 0.5})` ); gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`); context.fillStyle = gradient; context.beginPath(); context.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); context.fill(); return true; } return false; }); // Limit particle count if (this.particles.length > intensity * 5) { this.particles = this.particles.slice(-intensity * 5); } } } export function register(generator) { generator.registerEffect(new FireEffect()); }