diff --git a/web/app.js b/web/app.js index 919f001..91d953e 100644 --- a/web/app.js +++ b/web/app.js @@ -36,6 +36,8 @@ let initialStirrerActivateSync = true; let ambientTemp = null; let warpFactor = 1.0; +let heaterPowerIst = 0; +let heaterMaxPower = 100; // Sud state - mirrors the sud_* fields on client/brewpi_gui.py's Window. let sudSchedule = []; @@ -113,9 +115,15 @@ function mergeDefaults(defaults, override) { function buildStep(number, defaults, rawStep) { const step = {number: number}; - for (const key of ['descr', 'user_message', 'user_wait_for_continue', 'grain_mass', 'water_mass']) { + for (const key of ['descr', 'user_message', 'user_wait_for_continue']) { step[key] = (key in rawStep) ? rawStep[key] : defaults[key]; } + const rawPot = rawStep.pot || {}; + const defaultPot = defaults.pot || {}; + for (const key of ['grain_mass', 'water_mass']) { + if (key in rawPot) step[key] = rawPot[key]; + else if (key in defaultPot) step[key] = defaultPot[key]; + } // Deliberately not defaulted from default.step - a step omitting it // has no new target of its own and keeps whatever the previous step // left running (see components/sud.py's _build_step()). @@ -265,6 +273,21 @@ function updateStepPlates() { }); } +// --- Fire visualization --- + +function updateFireVisualization() { + const g = document.getElementById('fire-group'); + if (!g) return; + if (heaterPowerIst <= 0) { + g.style.opacity = '0'; + g.style.transform = 'scaleY(0.3)'; + } else { + const intensity = Math.min(1, heaterPowerIst / heaterMaxPower); + g.style.transform = `scaleY(${(0.45 + 0.55 * intensity).toFixed(2)})`; + g.style.opacity = String((0.6 + 0.4 * intensity).toFixed(2)); + } +} + // --- Pot visualization --- function wavePathD(x1, x2, y, amplitude, wavelength) { @@ -432,14 +455,17 @@ function onHeaterChanged(msg) { } else if (key === 'PowerSet') { document.getElementById('lcd-power-soll').textContent = msg.PowerSet; } else if (key === 'Power') { + heaterPowerIst = msg.Power; document.getElementById('lcd-power-ist').textContent = msg.Power; if (initialPowerSync) { document.getElementById('heater-power').value = msg.Power; document.getElementById('heater-power-readout').textContent = msg.Power; initialPowerSync = false; } + updateFireVisualization(); } else if (key === 'Capabilities') { const power = msg.Capabilities.Power; + heaterMaxPower = power.Max || 100; const slider = document.getElementById('heater-power'); slider.min = power.Min; slider.max = power.Max; diff --git a/web/index.html b/web/index.html index 1709815..2a2597f 100644 --- a/web/index.html +++ b/web/index.html @@ -47,7 +47,7 @@
- + @@ -55,8 +55,37 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/style.css b/web/style.css index 9350c79..aa0f207 100644 --- a/web/style.css +++ b/web/style.css @@ -152,6 +152,39 @@ header#connection-bar { display: block; } +/* Fire animations */ +@keyframes flicker-1 { + 0%, 100% { transform: scaleY(1.00) scaleX(1.00); } + 25% { transform: scaleY(1.09) scaleX(0.93); } + 50% { transform: scaleY(0.93) scaleX(1.06); } + 75% { transform: scaleY(1.06) scaleX(0.96); } +} +@keyframes flicker-2 { + 0%, 100% { transform: scaleY(0.95) scaleX(1.04); } + 30% { transform: scaleY(1.10) scaleX(0.92); } + 65% { transform: scaleY(0.91) scaleX(1.07); } +} +@keyframes flicker-3 { + 0%, 100% { transform: scaleY(1.05) scaleX(0.95); } + 40% { transform: scaleY(0.89) scaleX(1.08); } + 70% { transform: scaleY(1.12) scaleX(0.91); } +} + +.flame { + transform-box: fill-box; + transform-origin: 50% 100%; +} +#flame-outer { animation: flicker-1 0.70s ease-in-out infinite; } +#flame-mid { animation: flicker-2 0.55s ease-in-out infinite 0.08s; } +#flame-inner { animation: flicker-3 0.45s ease-in-out infinite 0.15s; } + +#fire-group { + transform-box: fill-box; + transform-origin: 50% 100%; + opacity: 0; + transition: opacity 0.4s; +} + /* --- Tabs --- */ nav#tabs {