Add animated fire visualization and fix step pot param reading

- SVG fire group with 3 layered flame paths (red/orange/yellow gradients)
- CSS keyframe flicker animations with different timings per flame layer
- JS controls fire opacity and scaleY proportional to heater power
- Fix buildStep() reading grain_mass/water_mass from nested step.pot
  instead of flat step keys, matching the JSON structure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DGKe8JzTM2Gr1fdx9wYXTi
This commit is contained in:
2026-06-26 22:04:32 +02:00
co-authored by Claude Sonnet 4.6
parent 59b18b95b6
commit 083bc97651
3 changed files with 90 additions and 2 deletions
+27 -1
View File
@@ -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;