feat: pattern editor

This commit is contained in:
2025-10-12 12:28:33 +02:00
parent 9a75b23169
commit cb172595b3
16 changed files with 4816 additions and 3 deletions

494
presets/building-blocks.js Normal file
View 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
View 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
View 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!

View 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
}
}
}

View 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
}
}
}

View 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
}
}
}

View 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
}
}
}