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) <noreply@anthropic.com>
This commit is contained in:
Richard Sauer 2026-04-08 12:57:07 +10:00
parent 1d26f78592
commit 0975d031a3

View File

@ -107,7 +107,7 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
.schematic .anim-overlay.active{opacity:1} .schematic .anim-overlay.active{opacity:1}
/* Animations */ /* 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 pulse-glow{0%,100%{opacity:0.4}50%{opacity:1}}
@keyframes scroll-belt{100%{stroke-dashoffset:-20}} @keyframes scroll-belt{100%{stroke-dashoffset:-20}}
@keyframes fall-particles{0%{opacity:1;transform:translateY(0)}100%{opacity:0;transform:translateY(30px)}} @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_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_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_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_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:null, link:'output_4'}, {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:null, link:'output_5'}, {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:null, link:'output_1'}, {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_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_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'}, {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') { if (eq.anim === 'burner') {
g.appendChild(mkSvgEl('circle', {'class':'anim-overlay burner-glow', id:'sim-'+eq.id, 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'})); 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') { } else if (eq.anim === 'fan') {
const ag = mkSvgEl('g', {'class':`anim-overlay ${eq.anim==='fan'?'fan-anim':'agitator-anim'}`, id:'sim-'+eq.id, 'transform-origin':`${eq.x} ${eq.y}`}); // Fan: 4 curved blades spinning inside the circle
ag.innerHTML = `<line x1="${eq.x}" y1="${eq.y-r}" x2="${eq.x}" y2="${eq.y+r}" stroke="${col||'var(--blue)'}" stroke-width="2"/><line x1="${eq.x-r}" y1="${eq.y}" x2="${eq.x+r}" y2="${eq.y}" stroke="${col||'var(--blue)'}" stroke-width="2"/>`; 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); g.appendChild(ag);
} else if (eq.anim === 'pulse') { } else if (eq.anim === 'pulse') {
g.appendChild(mkSvgEl('circle', {'class':'anim-overlay pulse-anim', id:'sim-'+eq.id, g.appendChild(mkSvgEl('circle', {'class':'anim-overlay pulse-anim', id:'sim-'+eq.id,