Files
spore-ledlab/presets/building-blocks.js
2025-10-12 13:52:22 +02:00

495 lines
13 KiB
JavaScript

// 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,
};