fuzzyworld

nordic-skating / mistral-medium-35-littlejs / files / README.md

Branch mistral-medium-35-littlejs · default branch · all branches

README.md

Blame: the commit that last changed each line · file · history

c90277fa 2026-10-09 Pavel Safronov1# Nordic Skating
2
3**Live — <https://pavelsafronov.gitlab.io/nordic-skating/>** · [Source](https://gitlab.com/PavelSafronov/nordic-skating)
4
5An endless skate across an endless lake of thin black ice, for a phone. The ice sings
6under the blades the way real black ice does, and its pitch tells you how thin it is.
7Leave it alone and it skates itself; steer, and you choose where the lake forks and
8how close to the thin ice you dare go.
9
10Inspired by National Geographic's
11[Hear the Otherworldly Sounds of Skating on Thin Ice](https://www.youtube.com/watch?v=v3O9vNi-dkA),
12in which Mårten Ajne skates a small lake outside Stockholm.
13
e5499331 2026-10-09 Pavel Safronov14<img src="docs/screenshots/landscape.png" alt="Black ice laced with white cracks, running between snowy banks of spruce to a foggy horizon" width="640" />
15
c90277fa 2026-10-09 Pavel Safronov16## Status
17
df2b8215 2026-10-10 Pavel Safronov18**Step 5 of 6, and a good deal since.** The lake runs on for ever between snowy, wooded
19shores, forking every few kilometres, pinching to narrow passes, opening out until no
20shore can be seen, and dotted in places with islets of reeds to weave between. The skater skates it, and the ice sings under every stride at
2f7e86b7 2026-10-10 Pavel Safronov21a pitch set by how thick it is there. Thin ice is black, sings high and cracks under
22you. Nobody falls through: how thick the ice is decides only how it sounds.
23
24Every visit is to a different lake, one of a million, by day, at dusk, at night under
0045befd 2026-10-10 Pavel Safronov25the Milky Way, or under the northern lights; in clear air, in falling snow, or in a
26blizzard; with, some nights, a comet, and some days an aeroplane drawing its trail.
2f7e86b7 2026-10-10 Pavel Safronov27
28<img src="docs/screenshots/aurora.png" alt="The lake at night under green curtains of aurora" width="640" />
c90277fa 2026-10-09 Pavel Safronov29
301. ✅ Scaffold: build, tests, deploy to GitLab Pages.
e5499331 2026-10-09 Pavel Safronov312. ✅ An endless lake, skated on autopilot, with steering.
9ff4c3fc 2026-10-09 Pavel Safronov323. ✅ Measure the reference video, then synthesize the ice sound from the ice's thickness.
646446cd 2026-10-09 Pavel Safronov334. ✅ Ice zones that fall out of the physics, and falling through.
1cb1fefc 2026-10-09 Pavel Safronov345. ✅ Forks in the lake, narrow passes, and stretches of open sea.
e3bfc714 2026-10-10 Pavel Safronov356. Game controller, then multiplayer. Postponed: [PLAN.md](PLAN.md) has what comes first.
c90277fa 2026-10-09 Pavel Safronov36
e5499331 2026-10-09 Pavel Safronov37## Controls
38
6e58e276 2026-10-10 Pavel Safronov39The button at the top right turns the sound on and off, as does the M key, and shows
3f3d6bce 2026-10-11 Pavel Safronov40which it is: a speaker, or a speaker crossed out. Sound cannot start by itself: a
6e58e276 2026-10-10 Pavel Safronov41browser lets a page make sound only after it has been touched, so until then the
3f3d6bce 2026-10-11 Pavel Safronov42speaker is crossed out, and the first tap or key anywhere starts it. Turning it off is remembered for next time. On an iPhone the
95f8bb94 2026-10-09 Pavel Safronov43ring/silent switch also silences it.
9ff4c3fc 2026-10-09 Pavel Safronov44
1cb1fefc 2026-10-09 Pavel Safronov45Do nothing and the skater follows the middle of the lake, round every bend and down
46one side of every fork, for as long as you leave it.
e5499331 2026-10-09 Pavel Safronov47
48| To | Touch or mouse | Keyboard |
49|---|---|---|
fe6b506c 2026-10-10 Pavel Safronov50| Steer left or right | Drag sideways from wherever you touch; or tilt the phone, with Tilt on | ← → or A D |
51| Tilt to steer, on or off | The Tilt button at the bottom left (phones and tablets only) | |
df2b8215 2026-10-10 Pavel Safronov52| Turn right round, or circle an islet | Hold the steer over | the same |
b2c4db9f 2026-10-10 Pavel Safronov53| Choose a way at a fork, or a side of an island | A touch towards it on the way up | the same |
95f8bb94 2026-10-09 Pavel Safronov54| Sound on or off | The button at the top right | M |
3f3d6bce 2026-10-11 Pavel Safronov55| Go faster or slower | The pace button at the bottom right: each press is the next pace up, and from the fastest back to the slowest | ↑ ↓ or W S |
2f7e86b7 2026-10-10 Pavel Safronov56| Go to another lake | Randomize, at the top left | R |
57
58At the top left, **About** opens a box that explains the app and its controls, and
59**Settings** one that changes it. The lake waits behind either, skater and sound both,
60until it is closed: by its button, by Escape, or by a touch outside it.
61
62**Randomize** goes to another lake, picked at random, at once and without reloading
63the page. The browser's Back button returns to the lake you left, and Forward goes on
64again.
65
8b498d51 2026-10-10 Pavel Safronov66## Install
67
68The app can be put on a phone's home screen, where it has its own icon, opens full
69screen without the browser's bars, and works with no network.
70
71- **Android (Chrome):** the menu, then "Add to Home screen" or "Install app".
72- **iPhone (Safari):** the Share button, then "Add to Home Screen".
73
74Nothing is fetched once it is installed: the whole app is kept on the phone and the
75lake is computed. A new version is taken up the next time it is opened with a network.
76
2f7e86b7 2026-10-10 Pavel Safronov77## Settings
78
79Each takes effect as it is touched, behind the box, and is remembered for next time.
80
81| Setting | Choices | To begin with |
82|---|---|---|
83| Time of day | Surprise me, Day, Dusk, Night, Aurora | Surprise me: whichever the lake comes with |
0045befd 2026-10-10 Pavel Safronov84| Weather | Surprise me, Clear, Snow, Blizzard | Surprise me: whichever the lake comes with |
3f3d6bce 2026-10-11 Pavel Safronov85| Pace | Meander, Cruise, Brisk, Racing, Olympic | Cruise |
2f7e86b7 2026-10-10 Pavel Safronov86| Impossible ice | on or off | off |
87| Lake | Another lake (the same as Randomize); a link back to this one | a different lake every visit |
88
89<img src="docs/screenshots/settings.png" alt="The settings box at the foot of the screen, under a night sky" width="320" />
90
9f2e2509 2026-10-10 Pavel Safronov91### Tilt to steer
92
3f3d6bce 2026-10-11 Pavel Safronov93On a phone or tablet there is a **Tilt** button at the bottom left, showing a phone held
94in a hand when tilt is on and a phone by itself when it is off; anywhere else it is not
95shown. Turned on, which is remembered, the phone is leaned
9f2e2509 2026-10-10 Pavel Safronov96left or right like a wheel, about the line the skater looks along: more lean, tighter
97turn, full steering at 28°. Within 4° of level counts as level, since nobody holds a
98phone dead level, and held level the skater skates itself. It works held upright or on
99its side, and dragging still works alongside it.
100
fe6b506c 2026-10-10 Pavel Safronov101While tilt is on the screen is stopped from turning with the phone, since leaning a
102phone over is exactly what makes its screen flip. A browser does that only for a page
103that fills the screen: the installed app already does, and in a browser tab the page
104goes full screen while tilt is on and comes out of it when tilt is turned off. An
105iPhone will do neither for a web page, so there the phone's own rotation lock, in
106Control Centre, is the way.
107
9f2e2509 2026-10-10 Pavel Safronov108An iPhone asks leave for this, each visit, at the first touch. If it has been refused
df03cd8b 2026-10-10 Pavel Safronov109it is not asked again, and the app says so where its hint is: Settings, Safari, Motion &
110Orientation Access turns it back on.
9f2e2509 2026-10-10 Pavel Safronov111Browsers give a page the phone's tilt only over HTTPS, which the live site is and
112`npm run dev` on a phone is not, so it cannot be tried on a phone from the dev server.
113
2f7e86b7 2026-10-10 Pavel Safronov114### A different lake every visit
115
116A lake is a number from 1 to 999,999, and everything about it follows from the number:
0045befd 2026-10-10 Pavel Safronov117its bends, widths and forks, its ice, its trees, the time of day and the weather it
118comes with, which way its wind blows, and what is in its sky.
2f7e86b7 2026-10-10 Pavel Safronov119Opening the app picks a number, and so do Randomize and reloading the page: each is a
120new lake under a new sky. Settings shows the number and a link to it, and `?seed=1234`
121on the address asks for a lake by number, which is how to come back to one or send it
122to someone.
95f8bb94 2026-10-09 Pavel Safronov123
2f7e86b7 2026-10-10 Pavel Safronov124Which lake a page is showing is kept with its place in the browser's history, not in
125its address. That is what lets Back return to the lake that was there while a reload,
126which is someone asking for something new, gets a new one. Randomize takes any
127`?seed=` off the address, since it no longer says which lake this is.
e5499331 2026-10-09 Pavel Safronov128
2f7e86b7 2026-10-10 Pavel Safronov129### Times of day
130
131| Time | The sky | How often a lake comes with it |
132|---|---|---|
133| Day | Flat, bright winter noon | 35% |
134| Dusk | An orange horizon under deepening blue, and the first stars | 20% |
135| Night | Moonless and clear: the Milky Way and thousands of stars | 27% |
136| Aurora | The same night under slow green curtains, violet where they reach highest | 18% |
137
138The sky is painted by a shader (`src/render/sky.ts`), and each time has its own light:
139the snow is orange at dusk, blue at night and faintly green under the aurora, and the
140milkiness of thick ice shows as much as there is light to show it. `?time=night` on the
141address asks for a time whatever the settings say.
142
143<img src="docs/screenshots/night.png" alt="The lake at night, the Milky Way overhead" width="640" />
144
0045befd 2026-10-10 Pavel Safronov145### What else is in the sky
146
147- **Shooting stars**, at night and under the aurora: one every quarter of a minute or
148 so, somewhere in the sky, gone in under a second.
149- **A comet**, over one night lake in three: ahead and low, with a tail.
150- **An aeroplane**, over two day or dusk lakes in three: a bright speck crossing the
151 sky ahead in four or five minutes, drawing a trail that widens and fades behind it,
152 white by day and pink at dusk. When it has gone, a while later another comes.
153
154<img src="docs/screenshots/comet.png" alt="A comet with a long tail over the lake at night" width="640" />
155
156### Weather
157
158| Weather | Snow | How far you can see | The sky | Wind | How often a lake comes with it |
159|---|---|---|---|---|---|
160| Clear | none | 600 m | as the time of day has it | none | 64% |
161| Snow | falling quietly | 420 m | mostly cloud | 0.5 to 3 m/s | 26% |
162| Blizzard | thick, and driven | 200 m | all cloud | 7 to 16 m/s, in gusts | 10% |
163
893cda77 2026-10-10 Pavel Safronov164**Snow that falls lies, in drifts** (`src/render/drifts.ts`): long pale streaks on the
165ice, all running the way that lake's wind blows, a few of them under falling snow and
166many in a blizzard. A drift is the lake's bad ice. In the middle of one a skater is
167dragged at four times as hard as on ice and slows to about half speed; the ice under
168it does not ring to a strike or sing to the glide, and the blade shushes instead. Bare
169ice between the drifts is as fast and as loud as ever, so in a blizzard there is a
170reason to pick a way.
171
172Snow in the air is a box of flakes that stays round the skater (`src/render/snow.ts`): they fall,
0045befd 2026-10-10 Pavel Safronov173the wind carries them, and they stream past as the skater moves, which is most of what
174makes a blizzard feel fast. Each lake's wind blows from a direction of its own. The wind
175can be heard, louder and higher in its gusts. Cloud hides the stars, the aurora and
176whatever else is up there, and takes the colour out of a sunset. `?weather=snow` on the
177address asks for a weather whatever the settings say.
178
179<img src="docs/screenshots/snow.png" alt="Snow falling over the lake by day" width="640" />
180
3f3d6bce 2026-10-11 Pavel Safronov181**A game controller** works as well: either stick or the direction buttons steer, up
182and down on the direction buttons or the shoulder buttons change pace, Y or triangle
183goes to another lake, and X or square turns the sound on and off. It needs no
893cda77 2026-10-10 Pavel Safronov184setting up beyond being connected, though a browser shows a page a controller only
185once one of its buttons has been pressed.
186
2f7e86b7 2026-10-10 Pavel Safronov187Steering turns the skater: the harder the steer, the tighter the curve, up to a circle
df2b8215 2026-10-10 Pavel Safronov18816 m in radius, with a moment to lean into it. Held over, the skater turns right round
189and can skate back up the lake, or round and round; that needs open water, about 24 m
190of it on the side being turned towards. A shore cannot be reached; near one the skater
c2835cab 2026-10-10 Pavel Safronov191is turned to run along it.
2f7e86b7 2026-10-10 Pavel Safronov192
193The autopilot does not skate a straight line. It carves long, slow S-curves down the
c2835cab 2026-10-10 Pavel Safronov194lake, 6 m either side of wherever its line is centred.
195
196**Steering changes the autopilot's plan; it does not interrupt it.** Let go, and the
197skater carries on the way you sent them, from where they had got to: the autopilot's
198line is re-centred there and its next curve begins in your direction, wide at first if
199you sent it off at an angle, and settling back to its usual 6 m over about twenty
200seconds. It does not return to where it was, or to the middle of the lake. Steer left
201out of a right-hand curve and you have a left-hand curve.
2f7e86b7 2026-10-10 Pavel Safronov202
3f3d6bce 2026-10-11 Pavel Safronov203The speed breathes with the stride: each push adds about a seventh of it, and the glide
204after it gives that back. Strides are of different lengths, and no two the same, so
205neither are the speeds.
206
207### Pace
208
209| Pace | Speed | Strides | Turning circle | |
210|---|---|---|---|---|
211| Meander | 3.5 m/s, 13 km/h | 2 to 4.5 s, long and lazy | 8 m | Standing tall. Tight enough to circle an islet closely |
212| Cruise | 7 m/s, 25 km/h | 1.4 to 3.2 s | 16 m | Where it starts |
213| Brisk | 10 m/s, 36 km/h | 1 to 2.4 s | 22 m | |
214| Racing | 12.5 m/s, 45 km/h | 0.8 to 1.9 s | 31 m | |
215| Olympic | 15 m/s, 54 km/h | 0.6 to 1.4 s, short and quick | 45 m | Down in a crouch. The pace of a long-track speed skater in a 1500 m, kept up for ever |
216
217A change of pace is worked up to, or eased down to, over a few strides. Everything
218else follows the speed. A faster skater is held to half a g sideways and so turns in a
219wider circle; looks further ahead, and steers round an islet from further off; keeps
220further from the shores, which in ordinary water leaves a racer little room to weave;
221swings wider on the autopilot's line; sees a wider view, which is most of what makes
222speed look like speed; and hears the air rush past. The pace is remembered, and
223`?pace=olympic` on the address asks for one for a visit.
2f7e86b7 2026-10-10 Pavel Safronov224
225The screen is kept awake while the page is open, where the browser allows it.
e5499331 2026-10-09 Pavel Safronov226
df2b8215 2026-10-10 Pavel Safronov227At a fork the lake parts round an island. A touch of steering one way, from 200 m
c2835cab 2026-10-10 Pavel Safronov228before the fork, is taken as asking for that side, and the autopilot does the rest. With
229no touch it takes the side it is already over towards, and down the middle, the side
230the fork prefers. Until the island begins, a little over
df2b8215 2026-10-10 Pavel Safronov231200 m after the fork, you can still change, by steering the other way. After that there
232is land between, though a skater who turns back can go round its tip.
1cb1fefc 2026-10-09 Pavel Safronov233
646446cd 2026-10-09 Pavel Safronov234## The ice
235
2f7e86b7 2026-10-10 Pavel Safronov236How thick the ice is decides how it looks and the note it sings. It does not decide
237whether it holds: for now nobody falls through, and the whole lake is safe to skate.
238
239The zones are not chosen. They are the three thresholds of Gold's formula for what a
240sheet of ice will carry (`src/ice/strength.ts`), worked out for an 80 kg skater, and the
241note is the physics under [The sound](#the-sound).
646446cd 2026-10-09 Pavel Safronov242
243| Ice | Thickness | Looks | Sings at | What happens |
244|---|---|---|---|---|
6e58e276 2026-10-10 Pavel Safronov245| Impossible, if turned on | 0.8 to 2.2 cm | bright pink, a colour nothing on a lake is, strongest where it is thinnest | 1390 to 3800 Hz | Nothing: it sings, very high, and never cracks. Real ice this thin would not hold anyone |
2f7e86b7 2026-10-10 Pavel Safronov246| Cracks | 2.2 to 3.4 cm | nearly black | 900 to 1390 Hz | It cracks around you: more often and nearer the thinner it gets |
646446cd 2026-10-09 Pavel Safronov247| Sings | 3.4 to 4.8 cm | dark grey | 630 to 900 Hz | Sound ice near its limit. The ice of the film is here, at 4.1 cm |
248| Safe | over 4.8 cm | milky grey | below 630 Hz | Nothing. The thicker, the lower and plainer the note |
249
2f7e86b7 2026-10-10 Pavel Safronov250Of the ice a skater can reach, about 30% cracks, 35% sings and 35% is safe.
251
252Impossible ice is a setting, and off to begin with. Off, the thinnest ice is 2.2 cm, the
253thinnest that is real, in patches that are on the point of giving way all the way
5e8b9530 2026-10-10 Pavel Safronov254across and crack every second or so. On, those patches go on thinning to 8 mm at
6e58e276 2026-10-10 Pavel Safronov255their middles and fall silent: about 4% of the ice a skater can reach. They are bright
256pink, as bright at midnight as at noon: lit from within, in a colour chosen because
257nothing on a lake is that colour.
2f7e86b7 2026-10-10 Pavel Safronov258
259Falling through is written and switched off (`RULES` in `src/skater.ts`). Switched on,
260ice under 2.2 cm gives way and the skater is in the water for four seconds. It needs one
261thing first: a line the autopilot can be sure of, since nothing now keeps thin ice off
262its path.
646446cd 2026-10-09 Pavel Safronov263
2f7e86b7 2026-10-10 Pavel Safronov264<img src="docs/screenshots/thin-ice.png" alt="Milky grey ice down the lake, with a black patch of the thinnest ice to the left of the middle" width="640" />
646446cd 2026-10-09 Pavel Safronov265
e5499331 2026-10-09 Pavel Safronov266## How the lake works
267
1cb1fefc 2026-10-09 Pavel Safronov268- **The lake is a function, not a map** (`src/world/lake.ts`). Where a channel's middle
269 is, how wide it is and how the land rises from its shores are all computed from the
270 distance skated and a seed. Any stretch can be made on demand, the same on every
271 device, and nothing is stored.
272- **It forks.** A channel runs from the fork that made it to the fork that ends it: the
273 first fork is 700 m from the start, and each one after comes 1.8 to 4.2 km after the
274 last. A fork's two channels start as the one channel and draw 180 m apart over 600 m,
275 which is always more than both are wide, so there is always an island. Each has a
276 width and forks of its own from there on. Both are drawn for 1.2 km, and then only the
277 one taken: the other is out of sight behind the island by then.
278- **It changes character over kilometres.** About 70% of the way it is ordinary water,
279 12 to 44 m from the middle to a shore. About 10% of the way it pinches to a pass 5 m
280 to a side. And now and then it opens until both shores are beyond the haze, 700 m off
281 or more: a stretch of sea. Near a fork it is always ordinary, so that a fork reads as
282 one.
283
df2b8215 2026-10-10 Pavel Safronov284- **It has islets.** Tufts of reeds on humps of frozen ground, 5 to 15 m across,
285 standing in the ice as they do in the film, where the skater weaves between them.
286 They come in beds: about a quarter of the lake's half-kilometres have some, one every
287 50 m or so, and the rest have none. Each has at least 9 m of ice between it and a
288 shore, and there are none in a narrow pass or near a fork. The autopilot goes round
289 them; a skater steering straight at one is turned aside at the last moment.
290
b2c4db9f 2026-10-10 Pavel Safronov291- **It has long islands.** Low banks of reeds, 180 to 480 m long and up to 32 m across,
292 standing in the middle of a channel with water both sides: the channel parts round
293 one and is one again beyond it. The water either side is as wide as the channel
294 would have been, so the lake swells where an island is. A touch of steering on the
295 way up to one chooses the side; with none, the island has a side it prefers. About
296 one every 5 km, and none near a fork or in a pass.
94b3b29f 2026-10-10 Pavel Safronov297- **Its ice is cracked the way ice cracks** (`src/ice/cracks.ts`). A few long cracks
298 run nearly straight for tens of metres; later ones start from the side of an earlier
299 one, leave it at a wide angle, and stop where they meet another, since a crack cannot
300 cross a crack. So the cracks are a network of long runs joined at T's, with finer ones
301 between, and each is painted as it looks in clear ice: a bright edge at the surface
302 and its face going down into the ice beside it.
b2c4db9f 2026-10-10 Pavel Safronov303- **It keeps the marks made on it** (`src/render/marks.ts`). The skates leave a line,
304 and each push a scratch to one side and then the other; on ice thin enough to crack,
94b3b29f 2026-10-10 Pavel Safronov305 a crack opens where each near one is heard from, and a push on such ice starts more
306 from under the skate. A crack is a star of a few arms that fork, and it runs out from
307 its middle over a moment. Every mark is a hairline, a pixel wide however near. They
308 stay where they were made, so a skater who turns back or circles an islet comes on
309 their own tracks, as in the film; the lake remembers the last few thousand stretches
310 of line and forgets the ones before.
b2c4db9f 2026-10-10 Pavel Safronov311
1cb1fefc 2026-10-09 Pavel Safronov312<img src="docs/screenshots/fork.png" alt="The lake parting round a wooded island ahead" width="640" />
313
b2c4db9f 2026-10-10 Pavel Safronov314<img src="docs/screenshots/island.png" alt="A long island of reeds ahead in the middle of the lake" width="640" />
315
df2b8215 2026-10-10 Pavel Safronov316<img src="docs/screenshots/islets.png" alt="An islet of reeds standing in the ice, with more beyond it" width="640" />
317
1cb1fefc 2026-10-09 Pavel Safronov318<img src="docs/screenshots/sea.png" alt="Ice to a hazy horizon with no shore in sight" width="640" />
319
2f7e86b7 2026-10-10 Pavel Safronov320- **The skater has a place, a direction and a speed** (`src/skater.ts`), and steering is
321 turning. The autopilot steers by the same means and within the same limits, aiming at
322 a point 18 m ahead on its wandering line, so every path on the lake is an arc. The
df2b8215 2026-10-10 Pavel Safronov323 skater may point any way at all, back up the lake included; left alone, the autopilot
324 turns them down it again.
2f7e86b7 2026-10-10 Pavel Safronov325- **The skater is kept on the ice.** They have the width of the channel less 2 m at each
df2b8215 2026-10-10 Pavel Safronov326 shore. Within 24 m of the edge of that they may point less and less towards it, down
327 to not at all at the edge. Where the lake narrows faster than a skater could steer
328 clear, their room starts closing 200 m early, whichever way they are going. Out at sea they may go no further than 150 m from the middle.
646446cd 2026-10-09 Pavel Safronov329- **Thickness is a function too**, and is painted on the ice from a small map of the 768 m
330 around the skater that is redrawn every 32 m (`src/render/ice.ts`).
1cb1fefc 2026-10-09 Pavel Safronov331- **The skater never leaves the origin of what is drawn.** The shores and islands are
332 built 100 m at a time ahead of the skater, thrown away behind, and placed relative to the skater
e5499331 2026-10-09 Pavel Safronov333 every frame (`src/render/shores.ts`); the ice is one sheet whose texture slides the
334 other way (`src/render/ice.ts`). So nothing drawn is ever a large number, and the
335 picture is as sharp 10,000 km down the lake as at the start.
336
c90277fa 2026-10-09 Pavel Safronov337## The sound
338
9ff4c3fc 2026-10-09 Pavel Safronov339The sound is synthesized in the browser. None of it is a recording.
340
341### The physics
342
343A sheet of ice floating on water carries bending waves, and they are dispersive: the
344higher the frequency, the faster it travels. The two sounds of thin ice follow from that
345(`src/ice/acoustics.ts`).
346
347- **The singing note.** A bending wave slower than sound in air cannot radiate into the
348 air at all. The ice gives off sound most strongly at the one frequency whose bending
349 wave travels exactly at the speed of sound in air, where the wave in the ice and the
350 wave in the air keep step. That frequency is very nearly **30.3 Hz·m divided by the
351 thickness**: 4 cm of ice sings at about 760 Hz, 3 cm at about 1010 Hz, 6 cm at about
352 505 Hz. The pitch is a thickness gauge: the higher it sings, the thinner the ice.
353- **The chirps.** A crack is every frequency at once. From some way off the high ones
354 arrive first and the low ones after, ending on the note: a falling "pew". The further
355 the crack, the longer the fall — about a third of a second from 400 m.
356
357The model is from Romeyn, Hanssen, Ruud and Johansen,
358[Sea ice thickness from air-coupled flexural waves](https://doi.org/10.5194/tc-15-2939-2021),
359*The Cryosphere* 15, 2939–2955 (2021), who took the sound of the very film this app is
360modelled on and read about 725 Hz and 4.3 cm of ice from it. With textbook values for
361freshwater ice and nothing tuned, the model here gives 4.2 cm for 725 Hz.
362
363### What the film measures
364
365`npm run analyse-video` measures a local copy of the film (it is not in this repository):
366
367| | Measured | Used for |
368|---|---|---|
369| The note | median 740 Hz; 664–826 Hz over the middle 90% | Ice 4.1 cm thick, 3.7–4.6 cm; the middle of the lake's range of thickness |
370| Time between skate strikes | median 0.66 s | Not used: at that rate the view rocks from leg to leg without rest. Strides here are long glides of 1.4 to 3.2 s, each a different length |
371| How fast a ring dies | 36 dB a second | The ring's decay: to 1/e in 0.24 s |
372
8b498d51 2026-10-10 Pavel Safronov373**The chirps cannot be checked against the film.** Three ways of doing it were tried,
374and the last is still in the tool: split each burst of sound into frequency bands, time
375when each band arrives, and see whether higher bands arrive earlier by as much as the
376model says. Of 122 bursts, 119 show no spread to measure: every band arrives within
37730 ms of every other. The other three do not agree with each other or with the model.
378The likely reason is that the film's microphones were close to the skater, and a sound
379from close by has had no distance to spread out over. So the film gives the note and
380not the chirp. The chirps here are the model's, which is the paper's, and the paper
381measured the spreading directly, with instruments on the ice.
9ff4c3fc 2026-10-09 Pavel Safronov382
383### What is synthesized
384
385`src/audio/iceSound.ts`, from noise and sine waves only:
386
387- **The ring**: at each stride, every 1.4 to 3.2 s, a skate strikes the ice, which answers with a narrow band
388 of noise at its note, starting a little high and falling onto it, then dying away.
389- **The hum and the scrape**: the gliding blade keeps the note faintly alive and hisses.
0045befd 2026-10-10 Pavel Safronov390- **The wind**, when there is one: low, loose noise, louder and higher in its gusts.
893cda77 2026-10-10 Pavel Safronov391- **Snow under the blade**, in a drift: a soft, low shush in place of the ring, the hum
392 and the scrape.
9ff4c3fc 2026-10-09 Pavel Safronov393- **The cracks**: every few seconds the ice cracks somewhere between 40 and 600 m off,
5e8b9530 2026-10-10 Pavel Safronov394 and the chirp for that distance and that thickness arrives. The falling chirp is a
395 sine wave and stops the moment it lands on the note; what rings on after it is the
396 ice, in the same band of noise as after a strike. (Left to ring by itself the sine was
397 a clean, struck, high note, like a sonar ping.) On ice thin enough to crack under the
398 skater the cracks come from a few metres away, and faster the thinner it is, up to
399 one every second or so.
2f7e86b7 2026-10-10 Pavel Safronov400 Impossible ice does not crack at all.
c90277fa 2026-10-09 Pavel Safronov401
9ff4c3fc 2026-10-09 Pavel Safronov402`npm run sound-check` renders twelve seconds of it without playing it and draws its
403spectrogram: six seconds on the film's 4.1 cm, then onto ice thinning to 2.8 cm, with
404three cracks.
c90277fa 2026-10-09 Pavel Safronov405
9ff4c3fc 2026-10-09 Pavel Safronov406<img src="docs/sound/synth.png" alt="Spectrogram: a bright narrow band near 740 Hz struck every two seconds or so and fading between, three thin lines falling steeply onto it, and the band climbing past 1 kHz in the last six seconds" width="800" />
c90277fa 2026-10-09 Pavel Safronov407
408## Develop
409
410Needs Node 22.
411
412```sh
413npm install
414npm run dev # dev server on every interface, with a QR code for a phone on the same Wi-Fi
415npm test # vitest
416npm run build # typecheck, then build into dist/
417npm run preview # serve dist/
e5499331 2026-10-09 Pavel Safronov418npm run screenshots # build, then photograph the app at phone size into docs/screenshots/
9ff4c3fc 2026-10-09 Pavel Safronov419npm run sound-check # render the synthesized sound and draw its spectrogram into docs/sound/
420npm run analyse-video # measure a local copy of the reference film
8b498d51 2026-10-10 Pavel Safronov421npm run icons # remake the home-screen and tab icons from assets/skate-shoes.png
c90277fa 2026-10-09 Pavel Safronov422```
423
2f7e86b7 2026-10-10 Pavel Safronov424On the address: `?seed=1` asks for lake number 1, `?time=night` for a time of day (`day`,
3f3d6bce 2026-10-11 Pavel Safronov425`dusk`, `night`, `aurora`), `?weather=snow` for a weather (`clear`, `snow`, `blizzard`), `?pace=olympic` for a pace
426(`meander`, `cruise`, `brisk`, `racing`, `olympic`),
0045befd 2026-10-10 Pavel Safronov427and `?s=2500` starts that many metres down the lake. The
2f7e86b7 2026-10-10 Pavel Safronov428screenshots use all three, so that they are of the same thing every run.
e5499331 2026-10-09 Pavel Safronov429
9ff4c3fc 2026-10-09 Pavel Safronov430The screenshots and the sound check need Playwright's Chromium once:
431`npx playwright install chromium`. The sound check and the analysis need ffmpeg.
e5499331 2026-10-09 Pavel Safronov432
c90277fa 2026-10-09 Pavel Safronov433## Deploy
434
435`.gitlab-ci.yml` tests and builds every push, and publishes the default branch to
436GitLab Pages. Asset paths are relative (`vite.config.ts`), so the build works at
437`/nordic-skating/` or at the root of a unique Pages domain without configuration.
438
439Static files go in `static/`, not `public/`: GitLab Pages needs its artifact to be
440named `public`, and a `public/` already in the repository would collide with it.
441
e3bfc714 2026-10-10 Pavel Safronov442## Licence
443
3f3d6bce 2026-10-11 Pavel Safronov444[MIT](LICENSE), for everything here but the icons. What it builds on is credited below.
e3bfc714 2026-10-10 Pavel Safronov445
c90277fa 2026-10-09 Pavel Safronov446## Attribution
447
3f3d6bce 2026-10-11 Pavel Safronov448- The app's icon and the pictures on its buttons are by
449 [Icongeek26](https://www.flaticon.com/authors/icongeek26), from Flaticon, used under
450 the Flaticon free licence, which requires attribution. The credit is in the app's
451 About box as well as here. None of them is covered by this project's MIT licence.
452- Of those, the app's icon is a skate. The artwork is `assets/skate-shoes.png`; the icons in `static/` are made from it by `npm run icons`,
8b498d51 2026-10-10 Pavel Safronov453 which uses only its shape: pale on the colour of black ice for a home screen, and
454 as it was drawn, black on a see-through background, for a browser's tab (white where
3f3d6bce 2026-10-11 Pavel Safronov455 the browser is dark). The same skate is on the pace button.
456- And the pictures on the sound button, a speaker and a speaker crossed out, are
457 `assets/sound-on.png` and `assets/sound-off.png`. On a button only a picture's shape
458 is used, in the colour of the button's words.
459- The pictures on the Tilt button, a phone held in a hand for on and a phone by itself
460 for off, are `assets/tilt-on.png` and `assets/tilt-off.png`. **Whether these two are
461 by the same author has not been confirmed**; if they are not, the credit here and in
462 the About box needs their author added.
c90277fa 2026-10-09 Pavel Safronov463- [three.js](https://threejs.org/) — MIT licence.
e5499331 2026-10-09 Pavel Safronov464- [simplex-noise](https://github.com/jwagner/simplex-noise.js) — MIT licence.
8b498d51 2026-10-10 Pavel Safronov465- [Workbox](https://developer.chrome.com/docs/workbox), by way of
466 [vite-plugin-pwa](https://vite-pwa-org.netlify.app/) — MIT licence. It is the service
467 worker that keeps the app on the phone.
9ff4c3fc 2026-10-09 Pavel Safronov468- The physics of the sound: Romeyn, R., Hanssen, A., Ruud, B. O., and Johansen, T. A.,
469 [Sea ice thickness from air-coupled flexural waves](https://doi.org/10.5194/tc-15-2939-2021),
470 *The Cryosphere* 15, 2939–2955, 2021 (CC BY 4.0).
646446cd 2026-10-09 Pavel Safronov471- What ice will carry: Gold, L. W., "Use of ice covers for transportation", *Canadian
472 Geotechnical Journal* 8, 170–181, 1971.
c90277fa 2026-10-09 Pavel Safronov473- The National Geographic video above is the reference this is modelled on. It is
474 copyrighted, and neither it nor any audio taken from it is part of this repository
475 or the app: it is measured locally, and the app's sound is synthesized from those
476 measurements.