feat: pattern editor
This commit is contained in:
494
presets/building-blocks.js
Normal file
494
presets/building-blocks.js
Normal file
@@ -0,0 +1,494 @@
|
||||
// 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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user