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,
|
||||
};
|
||||
|
||||
417
presets/custom-preset.js
Normal file
417
presets/custom-preset.js
Normal file
@@ -0,0 +1,417 @@
|
||||
// Custom Preset - A configurable preset based on JSON configuration
|
||||
|
||||
const BasePreset = require('./base-preset');
|
||||
const { createFrame } = require('./frame-utils');
|
||||
const { Shapes, Transforms, ColorGenerators, Animations, Patterns } = require('./building-blocks');
|
||||
|
||||
/**
|
||||
* Custom Preset Configuration Schema:
|
||||
*
|
||||
* {
|
||||
* "name": "My Custom Preset",
|
||||
* "description": "Description of the preset",
|
||||
* "layers": [
|
||||
* {
|
||||
* "type": "shape",
|
||||
* "shape": "circle|rectangle|triangle|line|point|blob",
|
||||
* "position": { "x": 8, "y": 8 },
|
||||
* "size": { "width": 5, "height": 5, "radius": 3 },
|
||||
* "color": {
|
||||
* "type": "solid|gradient|palette|rainbow|radial",
|
||||
* "value": "#ff0000",
|
||||
* "stops": [...]
|
||||
* },
|
||||
* "animation": {
|
||||
* "type": "move|rotate|scale|pulse|fade|bounce",
|
||||
* "params": { ... }
|
||||
* },
|
||||
* "blendMode": "normal|add|multiply"
|
||||
* },
|
||||
* {
|
||||
* "type": "pattern",
|
||||
* "pattern": "trail|energyField|radial|spiral",
|
||||
* "params": { ... }
|
||||
* }
|
||||
* ],
|
||||
* "parameters": {
|
||||
* "speed": { "type": "range", "min": 0.1, "max": 2.0, "default": 1.0 },
|
||||
* ...
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
class CustomPreset extends BasePreset {
|
||||
constructor(width = 16, height = 16, configuration = null) {
|
||||
super(width, height);
|
||||
|
||||
this.configuration = configuration || this.getDefaultConfiguration();
|
||||
this.animationStates = new Map();
|
||||
this.time = 0;
|
||||
this.lastFrameTime = Date.now();
|
||||
|
||||
this.initializeParameters();
|
||||
this.initializeAnimations();
|
||||
}
|
||||
|
||||
getDefaultConfiguration() {
|
||||
return {
|
||||
name: 'Custom Preset',
|
||||
description: 'A configurable preset',
|
||||
layers: [],
|
||||
parameters: {
|
||||
speed: { type: 'range', min: 0.1, max: 2.0, step: 0.1, default: 1.0 },
|
||||
brightness: { type: 'range', min: 0.1, max: 1.0, step: 0.1, default: 1.0 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
setConfiguration(configuration) {
|
||||
this.configuration = configuration;
|
||||
this.initializeParameters();
|
||||
this.initializeAnimations();
|
||||
}
|
||||
|
||||
initializeParameters() {
|
||||
this.defaultParameters = {};
|
||||
|
||||
if (this.configuration.parameters) {
|
||||
Object.entries(this.configuration.parameters).forEach(([name, config]) => {
|
||||
this.defaultParameters[name] = config.default;
|
||||
});
|
||||
}
|
||||
|
||||
this.resetToDefaults();
|
||||
}
|
||||
|
||||
initializeAnimations() {
|
||||
this.animationStates.clear();
|
||||
|
||||
this.configuration.layers?.forEach((layer, index) => {
|
||||
if (layer.animation) {
|
||||
const animation = this.createAnimation(layer.animation);
|
||||
this.animationStates.set(index, animation);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createAnimation(animConfig) {
|
||||
const type = animConfig.type;
|
||||
const params = animConfig.params || {};
|
||||
|
||||
switch (type) {
|
||||
case 'move':
|
||||
return Animations.linearMove(
|
||||
params.startX || 0,
|
||||
params.startY || 0,
|
||||
params.endX || this.width,
|
||||
params.endY || this.height,
|
||||
params.duration || 2.0
|
||||
);
|
||||
|
||||
case 'rotate':
|
||||
return Animations.rotation(params.speed || 1.0);
|
||||
|
||||
case 'pulse':
|
||||
return Animations.pulse(
|
||||
params.minScale || 0.5,
|
||||
params.maxScale || 1.5,
|
||||
params.frequency || 1.0
|
||||
);
|
||||
|
||||
case 'fade':
|
||||
return Animations.fade(
|
||||
params.duration || 2.0,
|
||||
params.fadeIn !== false
|
||||
);
|
||||
|
||||
case 'oscillateX':
|
||||
return Animations.oscillate(
|
||||
params.center || this.width / 2,
|
||||
params.amplitude || this.width / 4,
|
||||
params.frequency || 0.5,
|
||||
params.phase || 0
|
||||
);
|
||||
|
||||
case 'oscillateY':
|
||||
return Animations.oscillate(
|
||||
params.center || this.height / 2,
|
||||
params.amplitude || this.height / 4,
|
||||
params.frequency || 0.5,
|
||||
params.phase || 0
|
||||
);
|
||||
|
||||
default:
|
||||
return () => ({});
|
||||
}
|
||||
}
|
||||
|
||||
createColorGenerator(colorConfig) {
|
||||
if (!colorConfig) {
|
||||
return ColorGenerators.solid('ff0000');
|
||||
}
|
||||
|
||||
const type = colorConfig.type || 'solid';
|
||||
|
||||
switch (type) {
|
||||
case 'solid':
|
||||
return ColorGenerators.solid(colorConfig.value || 'ff0000');
|
||||
|
||||
case 'gradient':
|
||||
return ColorGenerators.gradient(
|
||||
colorConfig.color1 || 'ff0000',
|
||||
colorConfig.color2 || '0000ff'
|
||||
);
|
||||
|
||||
case 'palette':
|
||||
return ColorGenerators.palette(colorConfig.stops || [
|
||||
{ position: 0, color: '000000' },
|
||||
{ position: 1, color: 'ffffff' }
|
||||
]);
|
||||
|
||||
case 'rainbow':
|
||||
return ColorGenerators.rainbow();
|
||||
|
||||
case 'radial':
|
||||
return ColorGenerators.radial(
|
||||
colorConfig.color1 || 'ff0000',
|
||||
colorConfig.color2 || '0000ff',
|
||||
colorConfig.centerX || this.width / 2,
|
||||
colorConfig.centerY || this.height / 2,
|
||||
colorConfig.maxRadius || Math.hypot(this.width / 2, this.height / 2)
|
||||
);
|
||||
|
||||
default:
|
||||
return ColorGenerators.solid('ff0000');
|
||||
}
|
||||
}
|
||||
|
||||
renderFrame() {
|
||||
const now = Date.now();
|
||||
const deltaTime = (now - this.lastFrameTime) / 1000;
|
||||
this.lastFrameTime = now;
|
||||
|
||||
const speed = this.getParameter('speed') || 1.0;
|
||||
const brightness = this.getParameter('brightness') || 1.0;
|
||||
this.time += deltaTime * speed;
|
||||
|
||||
let frame = createFrame(this.width, this.height, '000000');
|
||||
|
||||
// Render each layer
|
||||
this.configuration.layers?.forEach((layer, index) => {
|
||||
this.renderLayer(frame, layer, index, brightness);
|
||||
});
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
renderLayer(frame, layer, layerIndex, globalBrightness) {
|
||||
const type = layer.type;
|
||||
|
||||
if (type === 'shape') {
|
||||
this.renderShape(frame, layer, layerIndex, globalBrightness);
|
||||
} else if (type === 'pattern') {
|
||||
this.renderPattern(frame, layer, layerIndex, globalBrightness);
|
||||
}
|
||||
}
|
||||
|
||||
renderShape(frame, layer, layerIndex, globalBrightness) {
|
||||
let position = layer.position || { x: this.width / 2, y: this.height / 2 };
|
||||
let size = layer.size || { radius: 3 };
|
||||
let rotation = 0;
|
||||
let scale = 1.0;
|
||||
let intensity = (layer.intensity || 1.0) * globalBrightness;
|
||||
|
||||
// Apply animation
|
||||
if (this.animationStates.has(layerIndex)) {
|
||||
const animation = this.animationStates.get(layerIndex);
|
||||
const animState = animation();
|
||||
|
||||
if (animState.x !== undefined && animState.y !== undefined) {
|
||||
position = { x: animState.x, y: animState.y };
|
||||
}
|
||||
|
||||
if (animState.angle !== undefined) {
|
||||
rotation = animState.angle;
|
||||
}
|
||||
|
||||
if (animState.scale !== undefined) {
|
||||
scale = animState.scale;
|
||||
}
|
||||
|
||||
if (animState.intensity !== undefined) {
|
||||
intensity *= animState.intensity;
|
||||
}
|
||||
|
||||
if (animState.value !== undefined && layer.animation?.axis === 'x') {
|
||||
position.x = animState.value;
|
||||
} else if (animState.value !== undefined && layer.animation?.axis === 'y') {
|
||||
position.y = animState.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Create color generator
|
||||
const colorGen = this.createColorGenerator(layer.color);
|
||||
const color = typeof colorGen === 'function' ? colorGen(0.5) : colorGen;
|
||||
|
||||
// Apply scale to size
|
||||
const scaledSize = {
|
||||
radius: (size.radius || 3) * scale,
|
||||
width: (size.width || 5) * scale,
|
||||
height: (size.height || 5) * scale,
|
||||
};
|
||||
|
||||
// Render shape
|
||||
const shapeType = layer.shape || 'circle';
|
||||
|
||||
switch (shapeType) {
|
||||
case 'circle':
|
||||
Shapes.circle(
|
||||
frame,
|
||||
this.width,
|
||||
this.height,
|
||||
position.x,
|
||||
position.y,
|
||||
scaledSize.radius,
|
||||
color,
|
||||
intensity
|
||||
);
|
||||
break;
|
||||
|
||||
case 'rectangle':
|
||||
Shapes.rectangle(
|
||||
frame,
|
||||
this.width,
|
||||
this.height,
|
||||
position.x - scaledSize.width / 2,
|
||||
position.y - scaledSize.height / 2,
|
||||
scaledSize.width,
|
||||
scaledSize.height,
|
||||
color,
|
||||
intensity
|
||||
);
|
||||
break;
|
||||
|
||||
case 'blob':
|
||||
Shapes.blob(
|
||||
frame,
|
||||
this.width,
|
||||
this.height,
|
||||
position.x,
|
||||
position.y,
|
||||
scaledSize.radius,
|
||||
color,
|
||||
layer.falloffPower || 2
|
||||
);
|
||||
break;
|
||||
|
||||
case 'point':
|
||||
Shapes.point(
|
||||
frame,
|
||||
this.width,
|
||||
this.height,
|
||||
position.x,
|
||||
position.y,
|
||||
color,
|
||||
intensity
|
||||
);
|
||||
break;
|
||||
|
||||
case 'triangle':
|
||||
// Triangle with rotation
|
||||
const triSize = scaledSize.radius || 3;
|
||||
const points = [
|
||||
{ x: 0, y: -triSize },
|
||||
{ x: -triSize, y: triSize },
|
||||
{ x: triSize, y: triSize }
|
||||
];
|
||||
|
||||
const rotated = points.map(p =>
|
||||
Transforms.rotate(p.x, p.y, 0, 0, rotation)
|
||||
);
|
||||
|
||||
Shapes.triangle(
|
||||
frame,
|
||||
this.width,
|
||||
this.height,
|
||||
position.x + rotated[0].x,
|
||||
position.y + rotated[0].y,
|
||||
position.x + rotated[1].x,
|
||||
position.y + rotated[1].y,
|
||||
position.x + rotated[2].x,
|
||||
position.y + rotated[2].y,
|
||||
color,
|
||||
intensity
|
||||
);
|
||||
break;
|
||||
|
||||
case 'line':
|
||||
const lineParams = layer.lineParams || {};
|
||||
Shapes.line(
|
||||
frame,
|
||||
this.width,
|
||||
this.height,
|
||||
lineParams.x1 || position.x,
|
||||
lineParams.y1 || position.y,
|
||||
lineParams.x2 || position.x + 5,
|
||||
lineParams.y2 || position.y + 5,
|
||||
color,
|
||||
lineParams.thickness || 1,
|
||||
intensity
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
renderPattern(frame, layer, layerIndex, globalBrightness) {
|
||||
const patternType = layer.pattern;
|
||||
const params = layer.params || {};
|
||||
const intensity = (layer.intensity || 1.0) * globalBrightness;
|
||||
|
||||
switch (patternType) {
|
||||
case 'trail':
|
||||
Patterns.trail(frame, params.decayFactor || 0.8);
|
||||
break;
|
||||
|
||||
case 'radial':
|
||||
const radialColorGen = this.createColorGenerator(layer.color);
|
||||
Patterns.radial(
|
||||
frame,
|
||||
this.width,
|
||||
this.height,
|
||||
params.centerX || this.width / 2,
|
||||
params.centerY || this.height / 2,
|
||||
radialColorGen,
|
||||
intensity
|
||||
);
|
||||
break;
|
||||
|
||||
case 'spiral':
|
||||
const spiralColorGen = this.createColorGenerator(layer.color);
|
||||
Patterns.spiral(
|
||||
frame,
|
||||
this.width,
|
||||
this.height,
|
||||
params.centerX || this.width / 2,
|
||||
params.centerY || this.height / 2,
|
||||
params.arms || 5,
|
||||
this.time * (params.rotationSpeed || 1.0),
|
||||
spiralColorGen,
|
||||
intensity
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: this.configuration.name || 'Custom Preset',
|
||||
description: this.configuration.description || 'A custom configurable preset',
|
||||
parameters: this.configuration.parameters || {},
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CustomPreset;
|
||||
|
||||
146
presets/examples/README.md
Normal file
146
presets/examples/README.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Example Custom Presets
|
||||
|
||||
This directory contains example custom preset configurations that demonstrate the capabilities of the LEDLab Preset Editor.
|
||||
|
||||
## Available Examples
|
||||
|
||||
### 1. Pulsing Circle (`pulsing-circle.json`)
|
||||
A simple preset featuring a single circle at the center that pulses in and out using rainbow colors.
|
||||
|
||||
**Features:**
|
||||
- Rainbow color generation
|
||||
- Pulse animation (scale oscillation)
|
||||
- Centered positioning
|
||||
|
||||
### 2. Bouncing Squares (`bouncing-squares.json`)
|
||||
Multiple colored rectangles bouncing around the matrix in different directions.
|
||||
|
||||
**Features:**
|
||||
- Multiple layers with different shapes
|
||||
- Oscillating animations on X and Y axes
|
||||
- Different colors per layer
|
||||
- Phase offsets for varied motion
|
||||
|
||||
### 3. Spiral Rainbow (`spiral-rainbow.json`)
|
||||
A rotating spiral pattern with a full rainbow gradient.
|
||||
|
||||
**Features:**
|
||||
- Pattern layer (spiral)
|
||||
- Multi-stop color palette
|
||||
- Configurable rotation speed and arm count
|
||||
|
||||
### 4. Moving Triangle (`moving-triangle.json`)
|
||||
A triangle that moves linearly across the screen with a gradient color scheme.
|
||||
|
||||
**Features:**
|
||||
- Triangle shape
|
||||
- Linear movement animation
|
||||
- Gradient colors
|
||||
|
||||
## How to Use
|
||||
|
||||
1. Open the LEDLab application
|
||||
2. Navigate to the **🎨 Editor** view
|
||||
3. Click the **📥 Import JSON** button
|
||||
4. Select one of the example JSON files
|
||||
5. Review the configuration in the editor
|
||||
6. Click **▶️ Preview** to test on a selected node
|
||||
7. Modify parameters to customize
|
||||
8. Click **💾 Save** to store your customized version
|
||||
|
||||
## Creating Your Own Presets
|
||||
|
||||
Use these examples as templates for creating your own custom presets. The general structure is:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Preset Name",
|
||||
"description": "Description of what the preset does",
|
||||
"layers": [
|
||||
{
|
||||
"type": "shape" | "pattern",
|
||||
"shape": "circle" | "rectangle" | "triangle" | "blob" | "point" | "line",
|
||||
"pattern": "trail" | "radial" | "spiral",
|
||||
"position": { "x": 8, "y": 8 },
|
||||
"size": { "radius": 3, "width": 5, "height": 5 },
|
||||
"color": {
|
||||
"type": "solid" | "gradient" | "palette" | "rainbow" | "radial",
|
||||
"value": "hexcolor",
|
||||
...
|
||||
},
|
||||
"animation": {
|
||||
"type": "move" | "rotate" | "pulse" | "oscillateX" | "oscillateY" | "fade",
|
||||
"params": { ... }
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"speed": { "type": "range", "min": 0.1, "max": 2.0, "step": 0.1, "default": 1.0 },
|
||||
"brightness": { "type": "range", "min": 0.1, "max": 1.0, "step": 0.1, "default": 1.0 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Building Blocks Reference
|
||||
|
||||
### Shape Types
|
||||
- **circle**: Round shape with radius
|
||||
- **rectangle**: Four-sided shape with width and height
|
||||
- **triangle**: Three-sided shape with radius (determines size)
|
||||
- **blob**: Soft circle with energy field falloff
|
||||
- **point**: Single pixel
|
||||
- **line**: Line between two points
|
||||
|
||||
### Pattern Types
|
||||
- **trail**: Fade effect on previous frames
|
||||
- **radial**: Gradient radiating from center
|
||||
- **spiral**: Rotating spiral arms
|
||||
|
||||
### Color Types
|
||||
- **solid**: Single color
|
||||
- **gradient**: Linear blend between two colors
|
||||
- **palette**: Multi-stop gradient
|
||||
- **rainbow**: Color wheel effect
|
||||
- **radial**: Gradient from center outward
|
||||
|
||||
### Animation Types
|
||||
- **move**: Linear movement from point A to B
|
||||
- **rotate**: Rotation around center
|
||||
- **pulse**: Scale oscillation
|
||||
- **oscillateX/Y**: Sine wave movement on X or Y axis
|
||||
- **fade**: Fade in or out
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Layer Order Matters**: Layers are rendered in order, so later layers appear on top
|
||||
2. **Combine Multiple Shapes**: Create complex effects by layering shapes with different animations
|
||||
3. **Use Phase Offsets**: For oscillating animations, use different phase values to desynchronize motion
|
||||
4. **Experiment with Colors**: Try different color combinations and gradients
|
||||
5. **Start Simple**: Begin with one layer and gradually add complexity
|
||||
6. **Test Early**: Preview frequently to see how your changes look in real-time
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
### Creating Trails
|
||||
Add a trail pattern layer with high decay factor combined with moving shapes:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "pattern",
|
||||
"pattern": "trail",
|
||||
"params": {
|
||||
"decayFactor": 0.85
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Synchronized Motion
|
||||
Use the same frequency and phase for oscillating animations to create synchronized movement.
|
||||
|
||||
### Color Cycling
|
||||
Use the rainbow color type with pulse or rotate animations for dynamic color effects.
|
||||
|
||||
## Need Help?
|
||||
|
||||
Refer to the main LEDLab documentation or experiment in the editor. The preview function lets you see changes in real-time!
|
||||
|
||||
100
presets/examples/bouncing-squares.json
Normal file
100
presets/examples/bouncing-squares.json
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"name": "Bouncing Squares",
|
||||
"description": "Multiple colored squares bouncing around",
|
||||
"layers": [
|
||||
{
|
||||
"type": "shape",
|
||||
"shape": "rectangle",
|
||||
"position": {
|
||||
"x": 8,
|
||||
"y": 8
|
||||
},
|
||||
"size": {
|
||||
"width": 3,
|
||||
"height": 3
|
||||
},
|
||||
"color": {
|
||||
"type": "solid",
|
||||
"value": "ff0000"
|
||||
},
|
||||
"intensity": 1.0,
|
||||
"animation": {
|
||||
"type": "oscillateX",
|
||||
"axis": "x",
|
||||
"params": {
|
||||
"center": 8,
|
||||
"amplitude": 6,
|
||||
"frequency": 0.5,
|
||||
"phase": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "shape",
|
||||
"shape": "rectangle",
|
||||
"position": {
|
||||
"x": 8,
|
||||
"y": 8
|
||||
},
|
||||
"size": {
|
||||
"width": 3,
|
||||
"height": 3
|
||||
},
|
||||
"color": {
|
||||
"type": "solid",
|
||||
"value": "00ff00"
|
||||
},
|
||||
"intensity": 1.0,
|
||||
"animation": {
|
||||
"type": "oscillateY",
|
||||
"axis": "y",
|
||||
"params": {
|
||||
"center": 8,
|
||||
"amplitude": 6,
|
||||
"frequency": 0.7,
|
||||
"phase": 1.57
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "shape",
|
||||
"shape": "rectangle",
|
||||
"position": {
|
||||
"x": 8,
|
||||
"y": 8
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 2
|
||||
},
|
||||
"color": {
|
||||
"type": "solid",
|
||||
"value": "0000ff"
|
||||
},
|
||||
"intensity": 1.0,
|
||||
"animation": {
|
||||
"type": "rotate",
|
||||
"params": {
|
||||
"speed": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"speed": {
|
||||
"type": "range",
|
||||
"min": 0.1,
|
||||
"max": 2.0,
|
||||
"step": 0.1,
|
||||
"default": 1.0
|
||||
},
|
||||
"brightness": {
|
||||
"type": "range",
|
||||
"min": 0.1,
|
||||
"max": 1.0,
|
||||
"step": 0.1,
|
||||
"default": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
50
presets/examples/moving-triangle.json
Normal file
50
presets/examples/moving-triangle.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "Moving Triangle",
|
||||
"description": "A triangle moving back and forth with gradient colors",
|
||||
"layers": [
|
||||
{
|
||||
"type": "shape",
|
||||
"shape": "triangle",
|
||||
"position": {
|
||||
"x": 8,
|
||||
"y": 8
|
||||
},
|
||||
"size": {
|
||||
"radius": 4
|
||||
},
|
||||
"color": {
|
||||
"type": "gradient",
|
||||
"color1": "ff00ff",
|
||||
"color2": "00ffff"
|
||||
},
|
||||
"intensity": 1.0,
|
||||
"animation": {
|
||||
"type": "move",
|
||||
"params": {
|
||||
"startX": 3,
|
||||
"startY": 8,
|
||||
"endX": 13,
|
||||
"endY": 8,
|
||||
"duration": 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"speed": {
|
||||
"type": "range",
|
||||
"min": 0.1,
|
||||
"max": 2.0,
|
||||
"step": 0.1,
|
||||
"default": 1.0
|
||||
},
|
||||
"brightness": {
|
||||
"type": "range",
|
||||
"min": 0.1,
|
||||
"max": 1.0,
|
||||
"step": 0.1,
|
||||
"default": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
46
presets/examples/pulsing-circle.json
Normal file
46
presets/examples/pulsing-circle.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "Pulsing Circle",
|
||||
"description": "A pulsing circle in the center with rainbow colors",
|
||||
"layers": [
|
||||
{
|
||||
"type": "shape",
|
||||
"shape": "circle",
|
||||
"position": {
|
||||
"x": 8,
|
||||
"y": 8
|
||||
},
|
||||
"size": {
|
||||
"radius": 4
|
||||
},
|
||||
"color": {
|
||||
"type": "rainbow"
|
||||
},
|
||||
"intensity": 1.0,
|
||||
"animation": {
|
||||
"type": "pulse",
|
||||
"params": {
|
||||
"minScale": 0.5,
|
||||
"maxScale": 1.5,
|
||||
"frequency": 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"speed": {
|
||||
"type": "range",
|
||||
"min": 0.1,
|
||||
"max": 2.0,
|
||||
"step": 0.1,
|
||||
"default": 1.0
|
||||
},
|
||||
"brightness": {
|
||||
"type": "range",
|
||||
"min": 0.1,
|
||||
"max": 1.0,
|
||||
"step": 0.1,
|
||||
"default": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
66
presets/examples/spiral-rainbow.json
Normal file
66
presets/examples/spiral-rainbow.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "Spiral Rainbow",
|
||||
"description": "A rotating spiral pattern with rainbow gradient",
|
||||
"layers": [
|
||||
{
|
||||
"type": "pattern",
|
||||
"pattern": "spiral",
|
||||
"color": {
|
||||
"type": "palette",
|
||||
"stops": [
|
||||
{
|
||||
"position": 0.0,
|
||||
"color": "ff0000"
|
||||
},
|
||||
{
|
||||
"position": 0.17,
|
||||
"color": "ff8800"
|
||||
},
|
||||
{
|
||||
"position": 0.33,
|
||||
"color": "ffff00"
|
||||
},
|
||||
{
|
||||
"position": 0.5,
|
||||
"color": "00ff00"
|
||||
},
|
||||
{
|
||||
"position": 0.67,
|
||||
"color": "0088ff"
|
||||
},
|
||||
{
|
||||
"position": 0.83,
|
||||
"color": "8800ff"
|
||||
},
|
||||
{
|
||||
"position": 1.0,
|
||||
"color": "ff0088"
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"centerX": 8,
|
||||
"centerY": 8,
|
||||
"arms": 5,
|
||||
"rotationSpeed": 1.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"speed": {
|
||||
"type": "range",
|
||||
"min": 0.1,
|
||||
"max": 3.0,
|
||||
"step": 0.1,
|
||||
"default": 1.0
|
||||
},
|
||||
"brightness": {
|
||||
"type": "range",
|
||||
"min": 0.1,
|
||||
"max": 1.0,
|
||||
"step": 0.1,
|
||||
"default": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user