From 0975d031a311b5bcc0f8bacc087360307795cbf0 Mon Sep 17 00:00:00 2001 From: Richard Sauer Date: Wed, 8 Apr 2026 12:57:07 +1000 Subject: [PATCH] Fix fan/agitator animations, add proper SVG graphics - Fan: 4 curved blades spinning from center, centered rotation - Agitator: 3 flat paddles with hub, slower spin, distinct look - Both now rotate properly around their center (translate+rotate) - Default equipment: shaker=vibrate, mill=vibrate, brush=glow, discharge=flow, temp probes=pulse - Saved layouts preserved (only default.json changed) Co-Authored-By: Claude Opus 4.6 (1M context) --- templates/editor.html | 61 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/templates/editor.html b/templates/editor.html index f578cc0..e414b50 100644 --- a/templates/editor.html +++ b/templates/editor.html @@ -107,7 +107,7 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var( .schematic .anim-overlay.active{opacity:1} /* Animations */ -@keyframes spin{100%{transform-origin:center;transform:rotate(360deg)}} +@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}} @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)}} @@ -622,10 +622,10 @@ const defaultEquipment = [ {id:'eq_agit1', label:'A1', shape:'circle', x:440, y:170, r:12, anim:'agitator', link:'motor_3'}, {id:'eq_agit2', label:'A2', shape:'circle', x:510, y:170, r:12, anim:'agitator', link:'motor_4'}, {id:'eq_spinner', label:'Spin', shape:'circle', x:475, y:155, r:14, anim:'fan', link:'motor_2'}, - {id:'eq_discharge',label:'Discharge', shape:'rect', x:580, y:160, w:80, h:60, anim:null, link:null}, - {id:'eq_mill', label:'Mill', shape:'rect', x:580, y:250, w:70, h:50, anim:null, link:'output_4'}, - {id:'eq_shaker', label:'Shaker Sep.',shape:'rect', x:670, y:250, w:90, h:50, anim:null, link:'output_5'}, - {id:'eq_brush', label:'Brush', shape:'rect', x:670, y:160, w:60, h:40, anim:null, link:'output_1'}, + {id:'eq_discharge',label:'Discharge', shape:'rect', x:580, y:160, w:80, h:60, anim:'flow', link:'output_0'}, + {id:'eq_mill', label:'Mill', shape:'rect', x:580, y:250, w:70, h:50, anim:'vibrate', link:'output_4'}, + {id:'eq_shaker', label:'Shaker Sep.',shape:'rect', x:670, y:250, w:90, h:50, anim:'vibrate', link:'output_5'}, + {id:'eq_brush', label:'Brush', shape:'rect', x:670, y:160, w:60, h:40, anim:'glow', link:'output_1'}, {id:'eq_in', label:'IN \u25B6', shape:'label', x:60, y:148, anim:null, link:null, color:'#4a5670'}, {id:'eq_out', label:'\u25B6 OUT', shape:'label', x:740, y:320, anim:null, link:null, color:'#4a5670'}, {id:'eq_t_heat', label:'T1', shape:'circle', x:230, y:180, r:10, anim:'pulse', link:'temp_0', color:'#ff4444'}, @@ -851,9 +851,54 @@ function renderSchematic() { 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'})); - } else if (eq.anim === 'fan' || eq.anim === 'agitator') { - const ag = mkSvgEl('g', {'class':`anim-overlay ${eq.anim==='fan'?'fan-anim':'agitator-anim'}`, id:'sim-'+eq.id, 'transform-origin':`${eq.x} ${eq.y}`}); - ag.innerHTML = ``; + } else if (eq.anim === 'fan') { + // Fan: 4 curved blades spinning inside the circle + const ag = mkSvgEl('g', {'class':'anim-overlay', id:'sim-'+eq.id}); + const inner = mkSvgEl('g', {'class':'fan-anim', style:`transform-origin:0 0`}); + const fc = col||'var(--blue)'; + const b = r * 0.8; + // 4 fan blades as arcs + for (let i = 0; i < 4; i++) { + const a = (i * 90) * Math.PI / 180; + const x1 = Math.cos(a) * b * 0.2, y1 = Math.sin(a) * b * 0.2; + const x2 = Math.cos(a) * b, y2 = Math.sin(a) * b; + const cx1 = Math.cos(a + 0.6) * b * 0.6, cy1 = Math.sin(a + 0.6) * b * 0.6; + inner.appendChild(mkSvgEl('path', { + d: `M ${x1} ${y1} Q ${cx1} ${cy1} ${x2} ${y2}`, + fill:'none', stroke:fc, 'stroke-width':'3', 'stroke-linecap':'round' + })); + } + // Center dot + inner.appendChild(mkSvgEl('circle', {cx:0, cy:0, r:r*0.15, fill:fc})); + ag.setAttribute('transform', `translate(${eq.x},${eq.y})`); + ag.appendChild(inner); + g.appendChild(ag); + } else if (eq.anim === 'agitator') { + // Agitator: 3 flat paddles spinning slower + const ag = mkSvgEl('g', {'class':'anim-overlay', id:'sim-'+eq.id}); + const inner = mkSvgEl('g', {'class':'agitator-anim', style:`transform-origin:0 0`}); + const ac = col||'var(--amber)'; + const pr = r * 0.75; + // 3 rectangular paddles + for (let i = 0; i < 3; i++) { + const a = (i * 120) * Math.PI / 180; + const x1 = Math.cos(a) * pr, y1 = Math.sin(a) * pr; + // Paddle: thick line from center to edge + inner.appendChild(mkSvgEl('line', { + x1: 0, y1: 0, x2: x1, y2: y1, + stroke:ac, 'stroke-width':'4', 'stroke-linecap':'round' + })); + // Paddle head: small rect at end + inner.appendChild(mkSvgEl('rect', { + x: x1 - 3, y: y1 - 5, width: 6, height: 10, + fill:ac, rx:'2', + transform: `rotate(${i*120} ${x1} ${y1})` + })); + } + // Center hub + inner.appendChild(mkSvgEl('circle', {cx:0, cy:0, r:r*0.2, fill:ac, stroke:ac, 'stroke-width':'1'})); + ag.setAttribute('transform', `translate(${eq.x},${eq.y})`); + ag.appendChild(inner); g.appendChild(ag); } else if (eq.anim === 'pulse') { g.appendChild(mkSvgEl('circle', {'class':'anim-overlay pulse-anim', id:'sim-'+eq.id,