Show floating grain particles in water when grain_mass > 0

35 particles distributed across full pot height, clipped to dynamic
water area via water-clip-rect updated alongside water fill level.
Particle opacity transitions on grain_mass changes per step.

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:27:05 +02:00
co-authored by Claude Sonnet 4.6
parent 7dfe88b89e
commit bc4a11dcb7
3 changed files with 76 additions and 0 deletions
+21
View File
@@ -320,6 +320,14 @@ function currentStepWaterMass() {
return sudWaterMass;
}
function currentStepGrainMass() {
if (sudStepIndex !== null && sudStepIndex >= 0 && sudStepIndex < sudSchedule.length) {
const step = sudSchedule[sudStepIndex];
if ('grain_mass' in step) return step.grain_mass;
}
return sudGrainMass;
}
function updatePotVisualization() {
// Thermometer mercury column (0°C = y=174, 100°C = y=28, tube length = 146)
const tubeBottom = 174;
@@ -338,9 +346,14 @@ function updatePotVisualization() {
const potInnerBottom = 172;
const potInnerH = potInnerBottom - potInnerTop; // 142
const particlesEl = document.getElementById('grain-particles');
const waterClipRect = document.getElementById('water-clip-rect');
if (sudEmpty || sudVolumen <= 0) {
waterFillEl.setAttribute('height', '0');
waterWaveEl.setAttribute('d', '');
if (waterClipRect) waterClipRect.setAttribute('height', '0');
if (particlesEl) particlesEl.style.opacity = '0';
return;
}
@@ -350,6 +363,14 @@ function updatePotVisualization() {
waterFillEl.setAttribute('y', waterTop.toFixed(1));
waterFillEl.setAttribute('height', waterH.toFixed(1));
waterWaveEl.setAttribute('d', wavePathD(63, 177, waterTop, 3, 28));
if (waterClipRect) {
waterClipRect.setAttribute('y', waterTop.toFixed(1));
waterClipRect.setAttribute('height', waterH.toFixed(1));
}
if (particlesEl) {
particlesEl.style.opacity = currentStepGrainMass() > 0 ? '1' : '0';
}
}
// --- Status line - mirrors update_status_step_label(). ---
+42
View File
@@ -55,6 +55,9 @@
<clipPath id="tube-clip">
<rect x="215" y="28" width="8" height="146"/>
</clipPath>
<clipPath id="water-clip">
<rect id="water-clip-rect" x="63" y="172" width="114" height="0"/>
</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"/>
@@ -93,6 +96,45 @@
fill="none" stroke="#4a9ed8" stroke-width="2"
clip-path="url(#pot-clip)"/>
<!-- Grain particles (shown when grain_mass > 0, clipped to water area) -->
<g id="grain-particles" clip-path="url(#water-clip)" opacity="0">
<circle class="grain-p" cx="80" cy="165" r="2.5" style="--fp-dur:3.2s;--fp-del:-0.5s"/>
<circle class="grain-p" cx="155" cy="158" r="2" style="--fp-dur:4.0s;--fp-del:-1.2s"/>
<circle class="grain-p" cx="105" cy="150" r="2.5" style="--fp-dur:2.8s;--fp-del:-2.1s"/>
<circle class="grain-p" cx="140" cy="143" r="2" style="--fp-dur:3.6s;--fp-del:-0.8s"/>
<circle class="grain-p" cx="75" cy="135" r="2.5" style="--fp-dur:4.2s;--fp-del:-1.5s"/>
<circle class="grain-p" cx="125" cy="125" r="2" style="--fp-dur:3.0s;--fp-del:-2.8s"/>
<circle class="grain-p" cx="90" cy="115" r="2.5" style="--fp-dur:3.8s;--fp-del:-0.3s"/>
<circle class="grain-p" cx="160" cy="105" r="2" style="--fp-dur:2.6s;--fp-del:-1.9s"/>
<circle class="grain-p" cx="110" cy="90" r="2.5" style="--fp-dur:3.4s;--fp-del:-1.1s"/>
<circle class="grain-p" cx="82" cy="75" r="2" style="--fp-dur:4.1s;--fp-del:-2.4s"/>
<circle class="grain-p" cx="148" cy="68" r="2.5" style="--fp-dur:2.9s;--fp-del:-0.7s"/>
<circle class="grain-p" cx="118" cy="55" r="2" style="--fp-dur:3.5s;--fp-del:-3.0s"/>
<circle class="grain-p" cx="72" cy="48" r="2.5" style="--fp-dur:4.3s;--fp-del:-1.6s"/>
<circle class="grain-p" cx="163" cy="40" r="2" style="--fp-dur:3.1s;--fp-del:-2.2s"/>
<circle class="grain-p" cx="118" cy="168" r="2" style="--fp-dur:3.7s;--fp-del:-0.9s"/>
<circle class="grain-p" cx="170" cy="162" r="2.5" style="--fp-dur:2.7s;--fp-del:-2.5s"/>
<circle class="grain-p" cx="68" cy="155" r="2" style="--fp-dur:4.4s;--fp-del:-1.0s"/>
<circle class="grain-p" cx="133" cy="148" r="2.5" style="--fp-dur:3.3s;--fp-del:-2.9s"/>
<circle class="grain-p" cx="95" cy="140" r="2" style="--fp-dur:2.5s;--fp-del:-0.4s"/>
<circle class="grain-p" cx="165" cy="132" r="2.5" style="--fp-dur:3.9s;--fp-del:-1.7s"/>
<circle class="grain-p" cx="78" cy="122" r="2" style="--fp-dur:4.5s;--fp-del:-2.3s"/>
<circle class="grain-p" cx="145" cy="112" r="2.5" style="--fp-dur:3.0s;--fp-del:-0.6s"/>
<circle class="grain-p" cx="100" cy="100" r="2" style="--fp-dur:2.7s;--fp-del:-1.4s"/>
<circle class="grain-p" cx="170" cy="88" r="2.5" style="--fp-dur:4.2s;--fp-del:-2.7s"/>
<circle class="grain-p" cx="68" cy="80" r="2" style="--fp-dur:3.6s;--fp-del:-0.2s"/>
<circle class="grain-p" cx="133" cy="70" r="2.5" style="--fp-dur:2.8s;--fp-del:-1.8s"/>
<circle class="grain-p" cx="95" cy="60" r="2" style="--fp-dur:4.0s;--fp-del:-2.6s"/>
<circle class="grain-p" cx="165" cy="52" r="2.5" style="--fp-dur:3.2s;--fp-del:-1.3s"/>
<circle class="grain-p" cx="78" cy="42" r="2" style="--fp-dur:2.6s;--fp-del:-3.1s"/>
<circle class="grain-p" cx="145" cy="35" r="2.5" style="--fp-dur:3.8s;--fp-del:-0.8s"/>
<circle class="grain-p" cx="112" cy="130" r="2" style="--fp-dur:4.1s;--fp-del:-2.0s"/>
<circle class="grain-p" cx="88" cy="97" r="2.5" style="--fp-dur:3.4s;--fp-del:-1.5s"/>
<circle class="grain-p" cx="155" cy="78" r="2" style="--fp-dur:2.9s;--fp-del:-0.1s"/>
<circle class="grain-p" cx="70" cy="62" r="2.5" style="--fp-dur:4.3s;--fp-del:-2.4s"/>
<circle class="grain-p" cx="138" cy="45" r="2" style="--fp-dur:3.5s;--fp-del:-1.0s"/>
</g>
<!-- Pot outline (transparent walls) -->
<line x1="60" y1="28" x2="60" y2="174" stroke="#bbb" stroke-width="4" stroke-linecap="round"/>
<line x1="180" y1="28" x2="180" y2="174" stroke="#bbb" stroke-width="4" stroke-linecap="round"/>
+13
View File
@@ -153,6 +153,19 @@ header#connection-bar {
display: block;
}
/* Grain particle animation */
@keyframes float-p {
0%, 100% { transform: translate(0px, 0px); }
25% { transform: translate(1.5px, -3px); }
50% { transform: translate(-1px, 2px); }
75% { transform: translate(2px, -1.5px); }
}
.grain-p {
fill: #c8a060;
animation: float-p var(--fp-dur, 3s) ease-in-out infinite var(--fp-del, 0s);
}
#grain-particles { transition: opacity 0.5s; }
/* Stirrer animation */
@keyframes stir-rotate {
to { transform: rotate(360deg); }