|
11 | 11 | const ctx = canvas.getContext('2d'); |
12 | 12 | const COLORS = ['#ffb7c5', '#ff8aa3', '#ffc8d5', '#ffd1dc', '#ffa8b8', '#ff95b0']; |
13 | 13 | const TARGET_FLYING = 18; |
14 | | - const SETTLED_CAP = 60; |
15 | | - const SPAWN_INTERVAL_MS = 900; |
16 | | - // Only the big surfaces — petals on tiny cells (chart, options) obscure content. |
17 | | - const SURFACE_SEL = '.jp-fact, .jp-card, .jp-quiz-prompt, .jp-word-card, .jp-modal-card'; |
| 14 | + const SETTLED_CAP = 220; // memory cap — oldest settled gets dropped past this |
| 15 | + const SPAWN_INTERVAL_MS = 280; |
| 16 | + // Petals can settle on any visible surface, big or small. |
| 17 | + const SURFACE_SEL = '.jp-fact, .jp-card, .jp-card-face, .jp-quiz-prompt, .jp-word-card, .jp-modal-card, .jp-mode, .jp-stat, .jp-opt, .jp-match-tile, .jp-tool-btn, .jp-chart-cell, .jp-pill, .jp-mini-pill, .jp-word-cat, .jp-restart-btn, .jp-btn, .jp-fact-next, .jp-speak-btn'; |
18 | 18 |
|
19 | 19 | let dpr = Math.min(window.devicePixelRatio || 1, 2); |
20 | 20 | let W = window.innerWidth, H = window.innerHeight; |
|
47 | 47 | } |
48 | 48 | } |
49 | 49 |
|
| 50 | + // ===== Wind from the side ===== |
| 51 | + // Layered sines: a slow base direction + slower gust + faster ripple. |
| 52 | + // Returns a value roughly in [-1, +1.6]; positive = blowing rightward. |
| 53 | + function wind(t) { |
| 54 | + return 0.25 |
| 55 | + + Math.sin(t * 0.00035) * 0.55 |
| 56 | + + Math.sin(t * 0.0013) * 0.25 |
| 57 | + + Math.sin(t * 0.0041) * 0.1; |
| 58 | + } |
| 59 | + |
50 | 60 | // ===== Petal ===== |
51 | 61 | class Petal { |
52 | 62 | constructor(initial) { this.reset(initial); } |
|
94 | 104 | // Physics |
95 | 105 | this.vy += 0.012; // gravity |
96 | 106 | this.vx += Math.sin(t * 0.001 + this.swayPhase) * 0.018 * this.swayAmp; // gentle wind |
| 107 | + this.vx += wind(t) * 0.012; // global side wind |
97 | 108 | this.vx *= 0.985; // drag |
98 | 109 | if (this.vy > 2.6) this.vy = 2.6; |
99 | 110 | if (this.vx > 1.6) this.vx = 1.6; |
|
155 | 166 | } |
156 | 167 |
|
157 | 168 | const petals = []; |
158 | | - // Seed with a handful of flying petals already on screen. |
159 | | - for (let i = 0; i < TARGET_FLYING; i++) petals.push(new Petal(true)); |
| 169 | + // Seed petals only ABOVE the viewport so they fall in naturally — no random |
| 170 | + // in-screen positions at start. |
| 171 | + for (let i = 0; i < TARGET_FLYING; i++) { |
| 172 | + const p = new Petal(false); |
| 173 | + p.y = -20 - Math.random() * H * 2; |
| 174 | + petals.push(p); |
| 175 | + } |
160 | 176 |
|
161 | 177 | // Anchor flying petals to the page (not the viewport): when the user |
162 | 178 | // scrolls, shift each flying petal by the same amount so it stays put |
|
178 | 194 | if (surfaceListAge > 1500) { rebuildSurfaceEls(); surfaceListAge = 0; } |
179 | 195 | refreshSurfaceRects(); |
180 | 196 |
|
181 | | - // Maintain a steady stream of new flying petals — settled ones never get replaced. |
| 197 | + // Maintain a steady stream of new flying petals — they keep falling forever. |
182 | 198 | let flyingCount = 0, settledCount = 0; |
183 | 199 | for (let i = 0; i < petals.length; i++) { |
184 | 200 | if (petals[i].settled) settledCount++; |
185 | 201 | else flyingCount++; |
186 | 202 | } |
187 | | - if (flyingCount < TARGET_FLYING && settledCount < SETTLED_CAP && t - lastSpawn > SPAWN_INTERVAL_MS) { |
| 203 | + // Memory cap: drop oldest settled petal once we exceed the cap (FIFO). |
| 204 | + if (settledCount > SETTLED_CAP) { |
| 205 | + for (let i = 0; i < petals.length; i++) { |
| 206 | + if (petals[i].settled) { petals.splice(i, 1); break; } |
| 207 | + } |
| 208 | + } |
| 209 | + // Always keep the air populated with falling petals. |
| 210 | + if (flyingCount < TARGET_FLYING && t - lastSpawn > SPAWN_INTERVAL_MS) { |
188 | 211 | petals.push(new Petal(false)); |
189 | 212 | lastSpawn = t; |
190 | 213 | } |
|
0 commit comments