495 lines
13 KiB
JavaScript
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,
|
|
};
|
|
|