Add 5 procedural LED effects, gradient presets, auto-crop min aspect ratio, static source polling optimization
New features: - Procedural effect source type with fire, meteor, plasma, noise, and aurora algorithms using palette LUT system and 1D value noise generator - 12 predefined gradient presets (rainbow, sunset, ocean, forest, fire, lava, aurora, ice, warm, cool, neon, pastel) selectable from a dropdown in the gradient editor - Auto-crop filter: min aspect ratio parameter to prevent false-positive cropping in dark scenes on ultrawide displays Optimization: - Static/gradient sources without animation: stream thread sleeps 0.25s instead of frame_time; processor repolls at frame_time instead of 5ms (~40x fewer iterations) - Inverted isinstance checks in routes to test for PictureColorStripSource only Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -89,7 +89,8 @@ import {
|
||||
// Layer 5: color-strip sources
|
||||
import {
|
||||
showCSSEditor, closeCSSEditorModal, forceCSSEditorClose, saveCSSEditor, deleteColorStrip,
|
||||
onCSSTypeChange, onAnimationTypeChange, colorCycleAddColor, colorCycleRemoveColor,
|
||||
onCSSTypeChange, onEffectTypeChange, onAnimationTypeChange, colorCycleAddColor, colorCycleRemoveColor,
|
||||
applyGradientPreset,
|
||||
} from './features/color-strips.js';
|
||||
|
||||
// Layer 5: calibration
|
||||
@@ -274,9 +275,11 @@ Object.assign(window, {
|
||||
saveCSSEditor,
|
||||
deleteColorStrip,
|
||||
onCSSTypeChange,
|
||||
onEffectTypeChange,
|
||||
onAnimationTypeChange,
|
||||
colorCycleAddColor,
|
||||
colorCycleRemoveColor,
|
||||
applyGradientPreset,
|
||||
|
||||
// calibration
|
||||
showCalibration,
|
||||
|
||||
@@ -31,6 +31,13 @@ class CSSEditorModal extends Modal {
|
||||
animation_speed: document.getElementById('css-editor-animation-speed').value,
|
||||
cycle_speed: document.getElementById('css-editor-cycle-speed').value,
|
||||
cycle_colors: JSON.stringify(_colorCycleColors),
|
||||
effect_type: document.getElementById('css-editor-effect-type').value,
|
||||
effect_speed: document.getElementById('css-editor-effect-speed').value,
|
||||
effect_palette: document.getElementById('css-editor-effect-palette').value,
|
||||
effect_color: document.getElementById('css-editor-effect-color').value,
|
||||
effect_intensity: document.getElementById('css-editor-effect-intensity').value,
|
||||
effect_scale: document.getElementById('css-editor-effect-scale').value,
|
||||
effect_mirror: document.getElementById('css-editor-effect-mirror').checked,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -45,8 +52,11 @@ export function onCSSTypeChange() {
|
||||
document.getElementById('css-editor-static-section').style.display = type === 'static' ? '' : 'none';
|
||||
document.getElementById('css-editor-color-cycle-section').style.display = type === 'color_cycle' ? '' : 'none';
|
||||
document.getElementById('css-editor-gradient-section').style.display = type === 'gradient' ? '' : 'none';
|
||||
document.getElementById('css-editor-effect-section').style.display = type === 'effect' ? '' : 'none';
|
||||
|
||||
// Animation section — shown for static/gradient only (color_cycle is always animating)
|
||||
if (type === 'effect') onEffectTypeChange();
|
||||
|
||||
// Animation section — shown for static/gradient only
|
||||
const animSection = document.getElementById('css-editor-animation-section');
|
||||
const animTypeSelect = document.getElementById('css-editor-animation-type');
|
||||
const noneOpt = `<option value="none">${t('color_strip.animation.type.none')}</option>`;
|
||||
@@ -119,6 +129,31 @@ function _syncAnimationSpeedState() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Effect type helpers ──────────────────────────────────────── */
|
||||
|
||||
export function onEffectTypeChange() {
|
||||
const et = document.getElementById('css-editor-effect-type').value;
|
||||
// palette: all except meteor
|
||||
document.getElementById('css-editor-effect-palette-group').style.display = et !== 'meteor' ? '' : 'none';
|
||||
// color picker: meteor only
|
||||
document.getElementById('css-editor-effect-color-group').style.display = et === 'meteor' ? '' : 'none';
|
||||
// intensity: fire, meteor, aurora
|
||||
document.getElementById('css-editor-effect-intensity-group').style.display =
|
||||
['fire', 'meteor', 'aurora'].includes(et) ? '' : 'none';
|
||||
// scale: plasma, noise, aurora
|
||||
document.getElementById('css-editor-effect-scale-group').style.display =
|
||||
['plasma', 'noise', 'aurora'].includes(et) ? '' : 'none';
|
||||
// mirror: meteor only
|
||||
document.getElementById('css-editor-effect-mirror-group').style.display = et === 'meteor' ? '' : 'none';
|
||||
// description
|
||||
const descEl = document.getElementById('css-editor-effect-type-desc');
|
||||
if (descEl) {
|
||||
const desc = t('color_strip.effect.' + et + '.desc') || '';
|
||||
descEl.textContent = desc;
|
||||
descEl.style.display = desc ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Color Cycle helpers ──────────────────────────────────────── */
|
||||
|
||||
const _DEFAULT_CYCLE_COLORS = ['#ff0000', '#ffff00', '#00ff00', '#00ffff', '#0000ff', '#ff00ff'];
|
||||
@@ -197,6 +232,7 @@ export function createColorStripCard(source, pictureSourceMap) {
|
||||
const isStatic = source.source_type === 'static';
|
||||
const isGradient = source.source_type === 'gradient';
|
||||
const isColorCycle = source.source_type === 'color_cycle';
|
||||
const isEffect = source.source_type === 'effect';
|
||||
|
||||
const anim = (isStatic || isGradient) && source.animation && source.animation.enabled ? source.animation : null;
|
||||
const animBadge = anim
|
||||
@@ -246,6 +282,15 @@ export function createColorStripCard(source, pictureSourceMap) {
|
||||
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">💡 ${source.led_count}</span>` : ''}
|
||||
${animBadge}
|
||||
`;
|
||||
} else if (isEffect) {
|
||||
const effectLabel = t('color_strip.effect.' + (source.effect_type || 'fire')) || source.effect_type || 'fire';
|
||||
const paletteLabel = source.palette ? (t('color_strip.palette.' + source.palette) || source.palette) : '';
|
||||
propsHtml = `
|
||||
<span class="stream-card-prop">⚡ ${escapeHtml(effectLabel)}</span>
|
||||
${paletteLabel ? `<span class="stream-card-prop" title="${t('color_strip.effect.palette')}">🎨 ${escapeHtml(paletteLabel)}</span>` : ''}
|
||||
<span class="stream-card-prop" title="${t('color_strip.effect.speed')}">⏩ ${(source.speed || 1.0).toFixed(1)}×</span>
|
||||
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">💡 ${source.led_count}</span>` : ''}
|
||||
`;
|
||||
} else {
|
||||
const srcName = (pictureSourceMap && pictureSourceMap[source.picture_source_id])
|
||||
? pictureSourceMap[source.picture_source_id].name
|
||||
@@ -259,8 +304,8 @@ export function createColorStripCard(source, pictureSourceMap) {
|
||||
`;
|
||||
}
|
||||
|
||||
const icon = isStatic ? '🎨' : isColorCycle ? '🔄' : isGradient ? '🌈' : '🎞️';
|
||||
const calibrationBtn = (!isStatic && !isGradient && !isColorCycle)
|
||||
const icon = isStatic ? '🎨' : isColorCycle ? '🔄' : isGradient ? '🌈' : isEffect ? '⚡' : '🎞️';
|
||||
const calibrationBtn = (!isStatic && !isGradient && !isColorCycle && !isEffect)
|
||||
? `<button class="btn btn-icon btn-secondary" onclick="showCSSCalibration('${source.id}')" title="${t('calibration.title')}">📐</button>`
|
||||
: '';
|
||||
|
||||
@@ -319,11 +364,24 @@ export async function showCSSEditor(cssId = null) {
|
||||
} else if (sourceType === 'color_cycle') {
|
||||
_loadColorCycleState(css);
|
||||
} else if (sourceType === 'gradient') {
|
||||
document.getElementById('css-editor-gradient-preset').value = '';
|
||||
gradientInit(css.stops || [
|
||||
{ position: 0.0, color: [255, 0, 0] },
|
||||
{ position: 1.0, color: [0, 0, 255] },
|
||||
]);
|
||||
_loadAnimationState(css.animation);
|
||||
} else if (sourceType === 'effect') {
|
||||
document.getElementById('css-editor-effect-type').value = css.effect_type || 'fire';
|
||||
onEffectTypeChange();
|
||||
document.getElementById('css-editor-effect-speed').value = css.speed ?? 1.0;
|
||||
document.getElementById('css-editor-effect-speed-val').textContent = parseFloat(css.speed ?? 1.0).toFixed(1);
|
||||
document.getElementById('css-editor-effect-palette').value = css.palette || 'fire';
|
||||
document.getElementById('css-editor-effect-color').value = rgbArrayToHex(css.color || [255, 80, 0]);
|
||||
document.getElementById('css-editor-effect-intensity').value = css.intensity ?? 1.0;
|
||||
document.getElementById('css-editor-effect-intensity-val').textContent = parseFloat(css.intensity ?? 1.0).toFixed(1);
|
||||
document.getElementById('css-editor-effect-scale').value = css.scale ?? 1.0;
|
||||
document.getElementById('css-editor-effect-scale-val').textContent = parseFloat(css.scale ?? 1.0).toFixed(1);
|
||||
document.getElementById('css-editor-effect-mirror').checked = css.mirror || false;
|
||||
} else {
|
||||
sourceSelect.value = css.picture_source_id || '';
|
||||
|
||||
@@ -369,7 +427,18 @@ export async function showCSSEditor(cssId = null) {
|
||||
document.getElementById('css-editor-led-count').value = 0;
|
||||
_loadAnimationState(null);
|
||||
_loadColorCycleState(null);
|
||||
document.getElementById('css-editor-effect-type').value = 'fire';
|
||||
document.getElementById('css-editor-effect-speed').value = 1.0;
|
||||
document.getElementById('css-editor-effect-speed-val').textContent = '1.0';
|
||||
document.getElementById('css-editor-effect-palette').value = 'fire';
|
||||
document.getElementById('css-editor-effect-color').value = '#ff5000';
|
||||
document.getElementById('css-editor-effect-intensity').value = 1.0;
|
||||
document.getElementById('css-editor-effect-intensity-val').textContent = '1.0';
|
||||
document.getElementById('css-editor-effect-scale').value = 1.0;
|
||||
document.getElementById('css-editor-effect-scale-val').textContent = '1.0';
|
||||
document.getElementById('css-editor-effect-mirror').checked = false;
|
||||
document.getElementById('css-editor-title').textContent = t('color_strip.add');
|
||||
document.getElementById('css-editor-gradient-preset').value = '';
|
||||
gradientInit([
|
||||
{ position: 0.0, color: [255, 0, 0] },
|
||||
{ position: 1.0, color: [0, 0, 255] },
|
||||
@@ -438,6 +507,23 @@ export async function saveCSSEditor() {
|
||||
animation: _getAnimationPayload(),
|
||||
};
|
||||
if (!cssId) payload.source_type = 'gradient';
|
||||
} else if (sourceType === 'effect') {
|
||||
payload = {
|
||||
name,
|
||||
effect_type: document.getElementById('css-editor-effect-type').value,
|
||||
speed: parseFloat(document.getElementById('css-editor-effect-speed').value),
|
||||
palette: document.getElementById('css-editor-effect-palette').value,
|
||||
intensity: parseFloat(document.getElementById('css-editor-effect-intensity').value),
|
||||
scale: parseFloat(document.getElementById('css-editor-effect-scale').value),
|
||||
mirror: document.getElementById('css-editor-effect-mirror').checked,
|
||||
led_count: parseInt(document.getElementById('css-editor-led-count').value) || 0,
|
||||
};
|
||||
// Meteor uses a color picker
|
||||
if (payload.effect_type === 'meteor') {
|
||||
const hex = document.getElementById('css-editor-effect-color').value;
|
||||
payload.color = [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];
|
||||
}
|
||||
if (!cssId) payload.source_type = 'effect';
|
||||
} else {
|
||||
payload = {
|
||||
name,
|
||||
@@ -596,6 +682,101 @@ export function gradientInit(stops) {
|
||||
gradientRenderAll();
|
||||
}
|
||||
|
||||
/* ── Presets ──────────────────────────────────────────────────── */
|
||||
|
||||
const _GRADIENT_PRESETS = {
|
||||
rainbow: [
|
||||
{ position: 0.0, color: [255, 0, 0] },
|
||||
{ position: 0.17, color: [255, 165, 0] },
|
||||
{ position: 0.33, color: [255, 255, 0] },
|
||||
{ position: 0.5, color: [0, 255, 0] },
|
||||
{ position: 0.67, color: [0, 100, 255] },
|
||||
{ position: 0.83, color: [75, 0, 130] },
|
||||
{ position: 1.0, color: [148, 0, 211] },
|
||||
],
|
||||
sunset: [
|
||||
{ position: 0.0, color: [255, 60, 0] },
|
||||
{ position: 0.3, color: [255, 120, 20] },
|
||||
{ position: 0.6, color: [200, 40, 80] },
|
||||
{ position: 0.8, color: [120, 20, 120] },
|
||||
{ position: 1.0, color: [40, 10, 60] },
|
||||
],
|
||||
ocean: [
|
||||
{ position: 0.0, color: [0, 10, 40] },
|
||||
{ position: 0.3, color: [0, 60, 120] },
|
||||
{ position: 0.6, color: [0, 140, 180] },
|
||||
{ position: 0.8, color: [100, 220, 240] },
|
||||
{ position: 1.0, color: [200, 240, 255] },
|
||||
],
|
||||
forest: [
|
||||
{ position: 0.0, color: [0, 40, 0] },
|
||||
{ position: 0.3, color: [0, 100, 20] },
|
||||
{ position: 0.6, color: [60, 180, 30] },
|
||||
{ position: 0.8, color: [140, 220, 50] },
|
||||
{ position: 1.0, color: [220, 255, 80] },
|
||||
],
|
||||
fire: [
|
||||
{ position: 0.0, color: [0, 0, 0] },
|
||||
{ position: 0.25, color: [80, 0, 0] },
|
||||
{ position: 0.5, color: [255, 40, 0] },
|
||||
{ position: 0.75, color: [255, 160, 0] },
|
||||
{ position: 1.0, color: [255, 255, 60] },
|
||||
],
|
||||
lava: [
|
||||
{ position: 0.0, color: [0, 0, 0] },
|
||||
{ position: 0.3, color: [120, 0, 0] },
|
||||
{ position: 0.6, color: [255, 60, 0] },
|
||||
{ position: 0.8, color: [255, 160, 40] },
|
||||
{ position: 1.0, color: [255, 255, 120] },
|
||||
],
|
||||
aurora: [
|
||||
{ position: 0.0, color: [0, 20, 40] },
|
||||
{ position: 0.25, color: [0, 200, 100] },
|
||||
{ position: 0.5, color: [0, 100, 200] },
|
||||
{ position: 0.75, color: [120, 0, 200] },
|
||||
{ position: 1.0, color: [0, 200, 140] },
|
||||
],
|
||||
ice: [
|
||||
{ position: 0.0, color: [255, 255, 255] },
|
||||
{ position: 0.3, color: [180, 220, 255] },
|
||||
{ position: 0.6, color: [80, 160, 255] },
|
||||
{ position: 0.85, color: [20, 60, 180] },
|
||||
{ position: 1.0, color: [10, 20, 80] },
|
||||
],
|
||||
warm: [
|
||||
{ position: 0.0, color: [255, 255, 80] },
|
||||
{ position: 0.33, color: [255, 160, 0] },
|
||||
{ position: 0.67, color: [255, 60, 0] },
|
||||
{ position: 1.0, color: [160, 0, 0] },
|
||||
],
|
||||
cool: [
|
||||
{ position: 0.0, color: [0, 255, 200] },
|
||||
{ position: 0.33, color: [0, 120, 255] },
|
||||
{ position: 0.67, color: [60, 0, 255] },
|
||||
{ position: 1.0, color: [120, 0, 180] },
|
||||
],
|
||||
neon: [
|
||||
{ position: 0.0, color: [255, 0, 200] },
|
||||
{ position: 0.25, color: [0, 255, 255] },
|
||||
{ position: 0.5, color: [0, 255, 50] },
|
||||
{ position: 0.75, color: [255, 255, 0] },
|
||||
{ position: 1.0, color: [255, 0, 100] },
|
||||
],
|
||||
pastel: [
|
||||
{ position: 0.0, color: [255, 180, 180] },
|
||||
{ position: 0.2, color: [255, 220, 160] },
|
||||
{ position: 0.4, color: [255, 255, 180] },
|
||||
{ position: 0.6, color: [180, 255, 200] },
|
||||
{ position: 0.8, color: [180, 200, 255] },
|
||||
{ position: 1.0, color: [220, 180, 255] },
|
||||
],
|
||||
};
|
||||
|
||||
export function applyGradientPreset(key) {
|
||||
if (!key || !_GRADIENT_PRESETS[key]) return;
|
||||
gradientInit(_GRADIENT_PRESETS[key]);
|
||||
}
|
||||
|
||||
/* ── Render ───────────────────────────────────────────────────── */
|
||||
|
||||
export function gradientRenderAll() {
|
||||
|
||||
Reference in New Issue
Block a user