diff --git a/templates/editor.html b/templates/editor.html
index 2960a9d..f578cc0 100644
--- a/templates/editor.html
+++ b/templates/editor.html
@@ -108,15 +108,28 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
/* Animations */
@keyframes spin{100%{transform-origin:center;transform:rotate(360deg)}}
-@keyframes pulse-glow{0%,100%{opacity:0.3}50%{opacity:1}}
+@keyframes pulse-glow{0%,100%{opacity:0.4}50%{opacity:1}}
@keyframes scroll-belt{100%{stroke-dashoffset:-20}}
@keyframes fall-particles{0%{opacity:1;transform:translateY(0)}100%{opacity:0;transform:translateY(30px)}}
+@keyframes blink{0%,100%{opacity:1}50%{opacity:0.1}}
+@keyframes vibrate{0%,100%{transform:translateX(0)}25%{transform:translateX(-2px)}75%{transform:translateX(2px)}}
+@keyframes glow-pulse{0%,100%{filter:drop-shadow(0 0 2px currentColor)}50%{filter:drop-shadow(0 0 12px currentColor)}}
+@keyframes flow-dash{100%{stroke-dashoffset:-30}}
.fan-anim{animation:spin 1s linear infinite}
.burner-glow{animation:pulse-glow 1.5s ease-in-out infinite}
.conveyor-anim{animation:scroll-belt 0.5s linear infinite}
.discharge-anim{animation:fall-particles 1s ease-out infinite}
.agitator-anim{animation:spin 2s linear infinite}
.pulse-anim{animation:pulse-glow 1.5s ease-in-out infinite}
+.blink-anim{animation:blink 1s step-end infinite}
+.vibrate-anim{animation:vibrate 0.1s linear infinite}
+.glow-anim{animation:glow-pulse 2s ease-in-out infinite}
+.flow-anim{animation:flow-dash 1s linear infinite}
+
+/* Static OFF state for linked elements */
+.equip-off{fill:var(--red) !important;fill-opacity:0.3 !important;stroke:var(--red) !important}
+.equip-on{stroke-width:2 !important}
+.static-off-ring{fill:none;stroke:var(--red);stroke-width:2;opacity:0.6}
/* Help panel */
.help-panel{position:fixed;right:0;top:48px;width:400px;height:calc(100vh - 48px);background:var(--card);border-left:2px solid var(--border);z-index:150;display:none;flex-direction:column;overflow-y:auto;padding:24px}
@@ -679,8 +692,8 @@ function ctxAction(action) {
saveEquip(eq); renderSchematic();
}
} else if (action === 'anim') {
- const anims = ['none', 'fan', 'agitator', 'burner', 'conveyor', 'pulse'];
- const choice = prompt('Animation — type one of: none, fan, agitator, burner, conveyor, pulse\nCurrent: ' + (eq.anim || 'none'), eq.anim || 'none');
+ const anims = ['none', 'fan', 'agitator', 'burner', 'conveyor', 'pulse', 'blink', 'vibrate', 'glow', 'flow'];
+ const choice = prompt('Animation — type one of:\n none, fan, agitator, burner, conveyor,\n pulse, blink, vibrate, glow, flow\n\nCurrent: ' + (eq.anim || 'none'), eq.anim || 'none');
if (choice && anims.includes(choice)) {
eq.anim = choice === 'none' ? null : choice;
saveEquip(eq); renderSchematic();
@@ -792,14 +805,32 @@ function renderSchematic() {
const t = mkSvgEl('text', {'class':'equip-label', x:eq.x+w/2, y:eq.y+h/2+5, ...(col?{fill:col}:{})});
t.textContent = eq.label; g.appendChild(t);
+ // Static OFF ring for linked elements
+ if (eq.link) {
+ g.appendChild(mkSvgEl('rect', {'class':'static-off-ring', id:'off-'+eq.id,
+ x:eq.x-1, y:eq.y-1, width:w+2, height:h+2, rx:'7', fill:'none'}));
+ }
+ // Animation overlay (only visible when ON)
+ const ac = col||'var(--green)';
if (eq.anim === 'conveyor') {
g.appendChild(mkSvgEl('line', {'class':'anim-overlay conveyor-anim', id:'sim-'+eq.id,
x1:eq.x+10, y1:eq.y+h-5, x2:eq.x+w-10, y2:eq.y+h-5,
- stroke: col||'var(--green)', 'stroke-width':'3', 'stroke-dasharray':'10 10'}));
+ stroke:ac, 'stroke-width':'3', 'stroke-dasharray':'10 10'}));
} else if (eq.anim === 'pulse') {
g.appendChild(mkSvgEl('rect', {'class':'anim-overlay pulse-anim', id:'sim-'+eq.id,
- x:eq.x, y:eq.y, width:w, height:h, rx:'6',
- fill:'none', stroke: col||'var(--green)', 'stroke-width':'3'}));
+ x:eq.x, y:eq.y, width:w, height:h, rx:'6', fill:'none', stroke:ac, 'stroke-width':'3'}));
+ } else if (eq.anim === 'blink') {
+ g.appendChild(mkSvgEl('rect', {'class':'anim-overlay blink-anim', id:'sim-'+eq.id,
+ x:eq.x, y:eq.y, width:w, height:h, rx:'6', fill:ac, 'fill-opacity':'0.3'}));
+ } else if (eq.anim === 'vibrate') {
+ g.appendChild(mkSvgEl('rect', {'class':'anim-overlay vibrate-anim', id:'sim-'+eq.id,
+ x:eq.x+2, y:eq.y+2, width:w-4, height:h-4, rx:'4', fill:'none', stroke:ac, 'stroke-width':'2'}));
+ } else if (eq.anim === 'glow') {
+ g.appendChild(mkSvgEl('rect', {'class':'anim-overlay glow-anim', id:'sim-'+eq.id,
+ x:eq.x-2, y:eq.y-2, width:w+4, height:h+4, rx:'8', fill:'none', stroke:ac, 'stroke-width':'3', color:ac}));
+ } else if (eq.anim === 'flow') {
+ g.appendChild(mkSvgEl('rect', {'class':'anim-overlay flow-anim', id:'sim-'+eq.id,
+ x:eq.x, y:eq.y, width:w, height:h, rx:'6', fill:'none', stroke:ac, 'stroke-width':'2', 'stroke-dasharray':'8 8'}));
}
// ── CIRCLE ──
@@ -811,6 +842,12 @@ function renderSchematic() {
const t = mkSvgEl('text', {'class':'equip-label', x:eq.x, y:eq.y+4, 'font-size':r<15?'8':'10', ...(col?{fill:col}:{})});
t.textContent = eq.label; g.appendChild(t);
+ // Static OFF ring
+ if (eq.link) {
+ g.appendChild(mkSvgEl('circle', {'class':'static-off-ring', id:'off-'+eq.id,
+ cx:eq.x, cy:eq.y, r:r+3}));
+ }
+ const cc = col||'var(--green)';
if (eq.anim === 'burner') {
g.appendChild(mkSvgEl('circle', {'class':'anim-overlay burner-glow', id:'sim-'+eq.id,
cx:eq.x, cy:eq.y, r:r+4, fill:'none', stroke:col||'var(--amber)', 'stroke-width':'4'}));
@@ -820,7 +857,19 @@ function renderSchematic() {
g.appendChild(ag);
} else if (eq.anim === 'pulse') {
g.appendChild(mkSvgEl('circle', {'class':'anim-overlay pulse-anim', id:'sim-'+eq.id,
- cx:eq.x, cy:eq.y, r:r+3, fill:'none', stroke:col||'var(--green)', 'stroke-width':'2'}));
+ cx:eq.x, cy:eq.y, r:r+3, fill:'none', stroke:cc, 'stroke-width':'2'}));
+ } else if (eq.anim === 'blink') {
+ g.appendChild(mkSvgEl('circle', {'class':'anim-overlay blink-anim', id:'sim-'+eq.id,
+ cx:eq.x, cy:eq.y, r:r, fill:cc, 'fill-opacity':'0.3'}));
+ } else if (eq.anim === 'vibrate') {
+ g.appendChild(mkSvgEl('circle', {'class':'anim-overlay vibrate-anim', id:'sim-'+eq.id,
+ cx:eq.x, cy:eq.y, r:r-2, fill:'none', stroke:cc, 'stroke-width':'2'}));
+ } else if (eq.anim === 'glow') {
+ g.appendChild(mkSvgEl('circle', {'class':'anim-overlay glow-anim', id:'sim-'+eq.id,
+ cx:eq.x, cy:eq.y, r:r+4, fill:'none', stroke:cc, 'stroke-width':'3', color:cc}));
+ } else if (eq.anim === 'flow') {
+ g.appendChild(mkSvgEl('circle', {'class':'anim-overlay flow-anim', id:'sim-'+eq.id,
+ cx:eq.x, cy:eq.y, r:r, fill:'none', stroke:cc, 'stroke-width':'2', 'stroke-dasharray':'6 6'}));
}
// ── LABEL ──
@@ -949,11 +998,34 @@ function renameEquip(eq) {
function updateSchematicAnimations() {
const equip = getEquipment();
equip.forEach(eq => {
- if (!eq.link || !eq.anim) return;
- const el = document.getElementById('sim-' + eq.id);
- if (!el) return;
+ if (!eq.link) return;
const s = simState[eq.link];
- el.classList.toggle('active', s && s.on);
+ const isOn = s && s.on;
+
+ // Update the animation overlay
+ const animEl = document.getElementById('sim-' + eq.id);
+ if (animEl) animEl.classList.toggle('active', isOn);
+
+ // Update the main shape — show red when OFF, normal when ON
+ const g = document.querySelector(`[data-eq-id="${eq.id}"]`);
+ if (!g) return;
+ const shape = g.querySelector('.equip');
+ if (!shape) return;
+
+ // Static OFF indicator (red tint)
+ const offRing = document.getElementById('off-' + eq.id);
+
+ if (!simEditMode) {
+ if (isOn) {
+ shape.classList.remove('equip-off');
+ shape.classList.add('equip-on');
+ if (offRing) offRing.style.display = 'none';
+ } else {
+ shape.classList.add('equip-off');
+ shape.classList.remove('equip-on');
+ if (offRing) offRing.style.display = '';
+ }
+ }
});
}