import { ButtonEffect } from "../effect-base.js"; /** * Bubbles rising background effect * Floating bubbles that rise with drift */ export class BubblesEffect extends ButtonEffect { constructor() { super({ id: "bg-bubbles", name: "Bubbles", type: "background-animation", category: "Background Animations", renderOrder: 10, }); this.bubbles = []; this.initialized = false; } defineControls() { return [ { id: "animate-bubbles", type: "checkbox", label: "Bubbles Effect", defaultValue: false, }, { id: "bubble-count", type: "range", label: "Bubble Count", defaultValue: 15, min: 5, max: 40, step: 1, showWhen: "animate-bubbles", description: "Number of bubbles", }, { id: "bubble-speed", type: "range", label: "Rise Speed", defaultValue: 1, min: 0.3, max: 3, step: 0.1, showWhen: "animate-bubbles", description: "Speed of rising bubbles", }, { id: "bubble-drift", type: "range", label: "Drift Amount", defaultValue: 1, min: 0, max: 3, step: 0.1, showWhen: "animate-bubbles", description: "Side-to-side drift", }, { id: "bubble-color", type: "color", label: "Bubble Color", defaultValue: "#4da6ff", showWhen: "animate-bubbles", description: "Color of bubbles", }, ]; } isEnabled(controlValues) { return controlValues["animate-bubbles"] === true; } apply(context, controlValues, animState, renderData) { if (!animState) return; const count = controlValues["bubble-count"] || 15; const speed = controlValues["bubble-speed"] || 1; const drift = controlValues["bubble-drift"] || 1; const bubbleColor = controlValues["bubble-color"] || "#4da6ff"; // Initialize bubbles on first frame or count change if (!this.initialized || this.bubbles.length !== count) { this.bubbles = []; for (let i = 0; i < count; i++) { this.bubbles.push({ x: Math.random() * renderData.width, startY: Math.random(), // Store as 0-1 ratio instead of pixel value size: 3 + Math.random() * 8, speedMultiplier: 0.5 + Math.random() * 1, driftPhase: Math.random() * Math.PI * 2, driftSpeed: 0.5 + Math.random() * 1, }); } this.initialized = true; } // Parse color for gradient const hexToRgb = (hex) => { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16), } : { r: 77, g: 166, b: 255 }; }; const rgb = hexToRgb(bubbleColor); // Draw bubbles this.bubbles.forEach((bubble) => { // Calculate Y position based on animation progress for perfect looping // Each bubble has a different starting offset and speed const bubbleProgress = (animState.progress * speed * bubble.speedMultiplier + bubble.startY) % 1; // Convert to pixel position (bubbles rise from bottom to top) const bubbleY = renderData.height + bubble.size - bubbleProgress * (renderData.height + bubble.size * 2); // Drift side to side const driftOffset = Math.sin( animState.getPhase(bubble.driftSpeed * 0.5) + bubble.driftPhase ) * drift * 2; const currentX = bubble.x + driftOffset; // Draw bubble with gradient for 3D effect const gradient = context.createRadialGradient( currentX - bubble.size * 0.3, bubbleY - bubble.size * 0.3, 0, currentX, bubbleY, bubble.size ); gradient.addColorStop( 0, `rgba(${rgb.r + 40}, ${rgb.g + 40}, ${rgb.b + 40}, 0.6)` ); gradient.addColorStop( 0.6, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.4)` ); gradient.addColorStop(1, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.2)`); context.fillStyle = gradient; context.beginPath(); context.arc(currentX, bubbleY, bubble.size, 0, Math.PI * 2); context.fill(); // Add highlight context.fillStyle = "rgba(255, 255, 255, 0.4)"; context.beginPath(); context.arc( currentX - bubble.size * 0.3, bubbleY - bubble.size * 0.3, bubble.size * 0.3, 0, Math.PI * 2 ); context.fill(); }); } } export function register(generator) { generator.registerEffect(new BubblesEffect()); }