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 = ''; + } + } }); }