// Building Blocks for Custom Presets // Extracted reusable components from existing presets const { hexToRgb, rgbToHex, lerpRgb, clamp, toIndex, samplePalette } = require('./frame-utils'); /** * Building Block Categories: * 1. Shapes - Draw geometric shapes * 2. Transforms - Position/scale/rotate operations * 3. Color Generators - Generate colors based on various algorithms * 4. Animations - Time-based modifications * 5. Compositors - Combine multiple elements */ // ==================== SHAPES ==================== const Shapes = { /** * Draw a circle at a position with radius */ circle: (frame, width, height, centerX, centerY, radius, color, intensity = 1.0) => { for (let row = 0; row < height; row++) { for (let col = 0; col < width; col++) { const dx = col - centerX; const dy = row - centerY; const distance = Math.hypot(dx, dy); if (distance <= radius) { const falloff = 1 - (distance / radius); const pixelIntensity = falloff * intensity; setPixelColor(frame, col, row, width, color, pixelIntensity); } } } }, /** * Draw a rectangle */ rectangle: (frame, width, height, x, y, rectWidth, rectHeight, color, intensity = 1.0) => { for (let row = Math.floor(y); row < Math.min(height, y + rectHeight); row++) { for (let col = Math.floor(x); col < Math.min(width, x + rectWidth); col++) { if (row >= 0 && col >= 0) { setPixelColor(frame, col, row, width, color, intensity); } } } }, /** * Draw a line from point A to point B */ line: (frame, width, height, x1, y1, x2, y2, color, thickness = 1, intensity = 1.0) => { const dx = x2 - x1; const dy = y2 - y1; const distance = Math.hypot(dx, dy); const steps = Math.ceil(distance); for (let i = 0; i <= steps; i++) { const t = i / steps; const x = x1 + dx * t; const y = y1 + dy * t; // Draw with thickness for (let ty = -thickness / 2; ty <= thickness / 2; ty++) { for (let tx = -thickness / 2; tx <= thickness / 2; tx++) { const px = Math.round(x + tx); const py = Math.round(y + ty); if (px >= 0 && px < width && py >= 0 && py < height) { setPixelColor(frame, px, py, width, color, intensity); } } } } }, /** * Draw a triangle */ triangle: (frame, width, height, x1, y1, x2, y2, x3, y3, color, intensity = 1.0) => { // Simple filled triangle using barycentric coordinates const minX = Math.max(0, Math.floor(Math.min(x1, x2, x3))); const maxX = Math.min(width - 1, Math.ceil(Math.max(x1, x2, x3))); const minY = Math.max(0, Math.floor(Math.min(y1, y2, y3))); const maxY = Math.min(height - 1, Math.ceil(Math.max(y1, y2, y3))); for (let row = minY; row <= maxY; row++) { for (let col = minX; col <= maxX; col++) { if (isPointInTriangle(col, row, x1, y1, x2, y2, x3, y3)) { setPixelColor(frame, col, row, width, color, intensity); } } } }, /** * Draw a single pixel/point */ point: (frame, width, height, x, y, color, intensity = 1.0) => { const col = Math.round(x); const row = Math.round(y); if (col >= 0 && col < width && row >= 0 && row < height) { setPixelColor(frame, col, row, width, color, intensity); } }, /** * Draw a blob (soft circle with energy field) */ blob: (frame, width, height, centerX, centerY, radius, color, falloffPower = 2) => { for (let row = 0; row < height; row++) { for (let col = 0; col < width; col++) { const dx = col - centerX; const dy = row - centerY; const distance = Math.hypot(dx, dy); const falloff = Math.max(0, 1 - distance / radius); const intensity = Math.pow(falloff, falloffPower); if (intensity > 0.01) { setPixelColor(frame, col, row, width, color, intensity); } } } }, }; // ==================== TRANSFORMS ==================== const Transforms = { /** * Rotate a point around a center */ rotate: (x, y, centerX, centerY, angle) => { const cos = Math.cos(angle); const sin = Math.sin(angle); const dx = x - centerX; const dy = y - centerY; return { x: centerX + dx * cos - dy * sin, y: centerY + dx * sin + dy * cos, }; }, /** * Scale a point from a center */ scale: (x, y, centerX, centerY, scaleX, scaleY) => { return { x: centerX + (x - centerX) * scaleX, y: centerY + (y - centerY) * scaleY, }; }, /** * Translate a point */ translate: (x, y, dx, dy) => { return { x: x + dx, y: y + dy, }; }, /** * Apply multiple transforms in sequence */ compose: (x, y, transforms) => { let point = { x, y }; for (const transform of transforms) { point = transform(point.x, point.y); } return point; }, }; // ==================== COLOR GENERATORS ==================== const ColorGenerators = { /** * Solid color */ solid: (color) => () => color, /** * Linear gradient between two colors */ gradient: (color1, color2) => (t) => { const rgb1 = hexToRgb(color1); const rgb2 = hexToRgb(color2); return rgbToHex(lerpRgb(rgb1, rgb2, clamp(t, 0, 1))); }, /** * Multi-stop gradient (palette) */ palette: (colorStops) => (t) => { const paletteStops = colorStops.map(stop => ({ stop: stop.position, color: hexToRgb(stop.color), })); return samplePalette(paletteStops, clamp(t, 0, 1)); }, /** * Rainbow color wheel */ rainbow: () => (t) => { const pos = Math.floor(clamp(t, 0, 1) * 255); let r, g, b; const wheelPos = 255 - pos; if (wheelPos < 85) { r = 255 - wheelPos * 3; g = 0; b = wheelPos * 3; } else if (wheelPos < 170) { const adjusted = wheelPos - 85; r = 0; g = adjusted * 3; b = 255 - adjusted * 3; } else { const adjusted = wheelPos - 170; r = adjusted * 3; g = 255 - adjusted * 3; b = 0; } return rgbToHex({ r, g, b }); }, /** * HSV to RGB color generation */ hsv: (hue, saturation = 1.0, value = 1.0) => () => { const h = hue / 60; const c = value * saturation; const x = c * (1 - Math.abs((h % 2) - 1)); const m = value - c; let r, g, b; if (h < 1) { [r, g, b] = [c, x, 0]; } else if (h < 2) { [r, g, b] = [x, c, 0]; } else if (h < 3) { [r, g, b] = [0, c, x]; } else if (h < 4) { [r, g, b] = [0, x, c]; } else if (h < 5) { [r, g, b] = [x, 0, c]; } else { [r, g, b] = [c, 0, x]; } return rgbToHex({ r: Math.round((r + m) * 255), g: Math.round((g + m) * 255), b: Math.round((b + m) * 255), }); }, /** * Radial gradient from center */ radial: (color1, color2, centerX, centerY, maxRadius) => (x, y) => { const dx = x - centerX; const dy = y - centerY; const distance = Math.hypot(dx, dy); const t = clamp(distance / maxRadius, 0, 1); const rgb1 = hexToRgb(color1); const rgb2 = hexToRgb(color2); return rgbToHex(lerpRgb(rgb1, rgb2, t)); }, }; // ==================== ANIMATIONS ==================== const Animations = { /** * Linear movement */ linearMove: (startX, startY, endX, endY, duration) => { const startTime = Date.now(); return () => { const elapsed = (Date.now() - startTime) / 1000; const t = clamp((elapsed % duration) / duration, 0, 1); return { x: startX + (endX - startX) * t, y: startY + (endY - startY) * t, t, }; }; }, /** * Oscillating movement (sine wave) */ oscillate: (center, amplitude, frequency, phase = 0) => { const startTime = Date.now(); return () => { const elapsed = (Date.now() - startTime) / 1000; const value = center + amplitude * Math.sin(2 * Math.PI * frequency * elapsed + phase); return { value, elapsed }; }; }, /** * Rotation animation */ rotation: (speed) => { const startTime = Date.now(); return () => { const elapsed = (Date.now() - startTime) / 1000; return { angle: elapsed * speed * Math.PI * 2, elapsed }; }; }, /** * Pulsing animation (scale) */ pulse: (minScale, maxScale, frequency) => { const startTime = Date.now(); return () => { const elapsed = (Date.now() - startTime) / 1000; const t = (Math.sin(2 * Math.PI * frequency * elapsed) + 1) / 2; return { scale: minScale + (maxScale - minScale) * t, t, elapsed }; }; }, /** * Bounce physics */ bounce: (position, velocity, bounds) => { let pos = position; let vel = velocity; return (dt) => { pos += vel * dt; if (pos < bounds.min) { pos = bounds.min + (bounds.min - pos); vel = Math.abs(vel); } else if (pos > bounds.max) { pos = bounds.max - (pos - bounds.max); vel = -Math.abs(vel); } return { position: pos, velocity: vel }; }; }, /** * Fade in/out animation */ fade: (duration, fadeIn = true) => { const startTime = Date.now(); return () => { const elapsed = (Date.now() - startTime) / 1000; const t = clamp(elapsed / duration, 0, 1); return { intensity: fadeIn ? t : (1 - t), t, elapsed }; }; }, }; // ==================== PATTERNS ==================== const Patterns = { /** * Trail effect (fade previous frames) */ trail: (frame, decayFactor = 0.8) => { for (let i = 0; i < frame.length; i++) { const rgb = hexToRgb(frame[i]); frame[i] = rgbToHex({ r: Math.round(rgb.r * decayFactor), g: Math.round(rgb.g * decayFactor), b: Math.round(rgb.b * decayFactor), }); } }, /** * Energy field (distance-based intensity) */ energyField: (frame, width, height, points, colorGenerator) => { for (let row = 0; row < height; row++) { for (let col = 0; col < width; col++) { let totalEnergy = 0; points.forEach(point => { const dx = col - point.x; const dy = row - point.y; const distance = Math.hypot(dx, dy); const falloff = Math.max(0, 1 - distance / point.radius); totalEnergy += point.intensity * Math.pow(falloff, 2); }); const energy = clamp(totalEnergy, 0, 1); const color = colorGenerator(energy); frame[toIndex(col, row, width)] = color; } } }, /** * Radial pattern from center */ radial: (frame, width, height, centerX, centerY, colorGenerator, intensity = 1.0) => { const maxRadius = Math.hypot(width / 2, height / 2); for (let row = 0; row < height; row++) { for (let col = 0; col < width; col++) { const dx = col - centerX; const dy = row - centerY; const distance = Math.hypot(dx, dy); const t = clamp(distance / maxRadius, 0, 1); const color = colorGenerator(t); setPixelColor(frame, col, row, width, color, intensity); } } }, /** * Angular/spiral pattern */ spiral: (frame, width, height, centerX, centerY, arms, rotation, colorGenerator, intensity = 1.0) => { const maxRadius = Math.hypot(width / 2, height / 2); for (let row = 0; row < height; row++) { for (let col = 0; col < width; col++) { const dx = col - centerX; const dy = row - centerY; const distance = Math.hypot(dx, dy); const angle = Math.atan2(dy, dx); const spiralValue = (Math.sin(arms * (angle + rotation)) + 1) / 2; const radiusValue = distance / maxRadius; const t = clamp(spiralValue * 0.5 + radiusValue * 0.5, 0, 1); const color = colorGenerator(t); setPixelColor(frame, col, row, width, color, intensity); } } }, }; // ==================== HELPER FUNCTIONS ==================== function setPixelColor(frame, col, row, width, color, intensity = 1.0) { const index = toIndex(col, row, width); if (intensity >= 1.0) { frame[index] = color; } else { const currentRgb = hexToRgb(frame[index]); const newRgb = hexToRgb(color); const blended = { r: Math.round(currentRgb.r + (newRgb.r - currentRgb.r) * intensity), g: Math.round(currentRgb.g + (newRgb.g - currentRgb.g) * intensity), b: Math.round(currentRgb.b + (newRgb.b - currentRgb.b) * intensity), }; frame[index] = rgbToHex(blended); } } function isPointInTriangle(px, py, x1, y1, x2, y2, x3, y3) { const d1 = sign(px, py, x1, y1, x2, y2); const d2 = sign(px, py, x2, y2, x3, y3); const d3 = sign(px, py, x3, y3, x1, y1); const hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0); const hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0); return !(hasNeg && hasPos); } function sign(px, py, x1, y1, x2, y2) { return (px - x2) * (y1 - y2) - (x1 - x2) * (py - y2); } module.exports = { Shapes, Transforms, ColorGenerators, Animations, Patterns, };