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:
+27
-1
@@ -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;
|
||||
|
||||
+30
-1
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
|
||||
<div id="pot-view">
|
||||
<svg id="pot-svg" viewBox="0 0 290 205" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg id="pot-svg" viewBox="0 0 290 255" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<clipPath id="pot-clip">
|
||||
<rect x="63" y="30" width="114" height="142"/>
|
||||
@@ -55,8 +55,37 @@
|
||||
<clipPath id="tube-clip">
|
||||
<rect x="215" y="28" width="8" height="146"/>
|
||||
</clipPath>
|
||||
<!-- Flame gradients: solid at base (y=1), transparent at tip (y=0) -->
|
||||
<linearGradient id="fg-red" x1="0" y1="1" x2="0" y2="0">
|
||||
<stop offset="0%" stop-color="#ff2200" stop-opacity="0.95"/>
|
||||
<stop offset="100%" stop-color="#ff5500" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="fg-orange" x1="0" y1="1" x2="0" y2="0">
|
||||
<stop offset="0%" stop-color="#ff7700" stop-opacity="0.9"/>
|
||||
<stop offset="100%" stop-color="#ffaa00" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="fg-yellow" x1="0" y1="1" x2="0" y2="0">
|
||||
<stop offset="0%" stop-color="#ffe000" stop-opacity="0.85"/>
|
||||
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Fire (shown when heater is on, JS controls scale+opacity) -->
|
||||
<g id="fire-group">
|
||||
<!-- Outer red flame -->
|
||||
<path id="flame-outer" class="flame"
|
||||
d="M76,248 C79,215 92,192 101,208 C106,192 113,177 120,180 C127,177 134,192 139,208 C148,192 161,215 164,248 Z"
|
||||
fill="url(#fg-red)"/>
|
||||
<!-- Middle orange flame -->
|
||||
<path id="flame-mid" class="flame"
|
||||
d="M88,248 C91,222 101,204 109,217 C113,204 118,191 122,195 C126,191 131,204 135,217 C143,204 153,222 156,248 Z"
|
||||
fill="url(#fg-orange)"/>
|
||||
<!-- Inner yellow flame -->
|
||||
<path id="flame-inner" class="flame"
|
||||
d="M100,248 C102,230 110,215 116,224 C118,215 121,205 124,208 C127,205 130,215 132,224 C138,215 147,230 148,248 Z"
|
||||
fill="url(#fg-yellow)"/>
|
||||
</g>
|
||||
|
||||
<!-- Water fill (behind pot walls) -->
|
||||
<rect id="water-fill" x="63" y="172" width="114" height="0"
|
||||
fill="#1a4a72" clip-path="url(#pot-clip)"/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user