fuzzyworld

nordic-skating / files / README.md

README.md

27.9 KB · history · blame

Nordic Skating

Live — https://apps.fuzzyworld.net/nordic-skating/ · Source

An endless skate across an endless lake of thin black ice, for a phone. The ice sings under the blades the way real black ice does, and its pitch tells you how thin it is. Leave it alone and it skates itself; steer, and you choose where the lake forks and how close to the thin ice you dare go.

Inspired by National Geographic's Hear the Otherworldly Sounds of Skating on Thin Ice, in which Mårten Ajne skates a small lake outside Stockholm.

Black ice laced with white cracks, running between snowy banks of spruce to a foggy horizon

Status

Step 5 of 6, and a good deal since. The lake runs on for ever between snowy, wooded shores, forking every few kilometres, pinching to narrow passes, opening out until no shore 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 a pitch set by how thick it is there. Thin ice is black, sings high and cracks under you. Nobody falls through: how thick the ice is decides only how it sounds.

Every visit is to a different lake, one of a million, by day, at dusk, at night under the Milky Way, or under the northern lights; in clear air, in falling snow, or in a blizzard; with, some nights, a comet, and some days an aeroplane drawing its trail.

The lake at night under green curtains of aurora

  1. ✅ Scaffold: build, tests, deploy to GitLab Pages.
  2. ✅ An endless lake, skated on autopilot, with steering.
  3. ✅ Measure the reference video, then synthesize the ice sound from the ice's thickness.
  4. ✅ Ice zones that fall out of the physics, and falling through.
  5. ✅ Forks in the lake, narrow passes, and stretches of open sea.
  6. Game controller, then multiplayer. Postponed: PLAN.md has what comes first.

Controls

The button at the top right turns the sound on and off, as does the M key, and shows which it is: a speaker, or a speaker crossed out. Sound cannot start by itself: a browser lets a page make sound only after it has been touched, so until then the speaker is crossed out, and the first tap or key anywhere starts it. Turning it off is remembered for next time. On an iPhone the ring/silent switch also silences it.

Do nothing and the skater follows the middle of the lake, round every bend and down one side of every fork, for as long as you leave it.

To Touch or mouse Keyboard
Steer left or right Drag sideways from wherever you touch; or tilt the phone, with Tilt on ← → or A D
Tilt to steer, on or off The Tilt button at the bottom left (phones and tablets only)
Turn right round, or circle an islet Hold the steer over the same
Choose a way at a fork, or a side of an island A touch towards it on the way up the same
Sound on or off The button at the top right M
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
Go to another lake Randomize, at the top left R

At the top left, About opens a box that explains the app and its controls, and Settings one that changes it. The lake waits behind either, skater and sound both, until it is closed: by its button, by Escape, or by a touch outside it.

Randomize goes to another lake, picked at random, at once and without reloading the page. The browser's Back button returns to the lake you left, and Forward goes on again.

Install

The app can be put on a phone's home screen, where it has its own icon, opens full screen without the browser's bars, and works with no network.

Nothing is fetched once it is installed: the whole app is kept on the phone and the lake is computed. A new version is taken up the next time it is opened with a network.

Settings

Each takes effect as it is touched, behind the box, and is remembered for next time.

Setting Choices To begin with
Time of day Surprise me, Day, Dusk, Night, Aurora Surprise me: whichever the lake comes with
Weather Surprise me, Clear, Snow, Blizzard Surprise me: whichever the lake comes with
Pace Meander, Cruise, Brisk, Racing, Olympic Cruise
Impossible ice on or off off
Lake Another lake (the same as Randomize); a link back to this one a different lake every visit

The settings box at the foot of the screen, under a night sky

Tilt to steer

On a phone or tablet there is a Tilt button at the bottom left, showing a phone held in a hand when tilt is on and a phone by itself when it is off; anywhere else it is not shown. Turned on, which is remembered, the phone is leaned left or right like a wheel, about the line the skater looks along: more lean, tighter turn, full steering at 28°. Within 4° of level counts as level, since nobody holds a phone dead level, and held level the skater skates itself. It works held upright or on its side, and dragging still works alongside it.

While tilt is on the screen is stopped from turning with the phone, since leaning a phone over is exactly what makes its screen flip. A browser does that only for a page that fills the screen: the installed app already does, and in a browser tab the page goes full screen while tilt is on and comes out of it when tilt is turned off. An iPhone will do neither for a web page, so there the phone's own rotation lock, in Control Centre, is the way.

An iPhone asks leave for this, each visit, at the first touch. If it has been refused it is not asked again, and the app says so where its hint is: Settings, Safari, Motion & Orientation Access turns it back on. Browsers give a page the phone's tilt only over HTTPS, which the live site is and npm run dev on a phone is not, so it cannot be tried on a phone from the dev server.

A different lake every visit

A lake is a number from 1 to 999,999, and everything about it follows from the number: its bends, widths and forks, its ice, its trees, the time of day and the weather it comes with, which way its wind blows, and what is in its sky. Opening the app picks a number, and so do Randomize and reloading the page: each is a new lake under a new sky. Settings shows the number and a link to it, and ?seed=1234 on the address asks for a lake by number, which is how to come back to one or send it to someone.

Which lake a page is showing is kept with its place in the browser's history, not in its address. That is what lets Back return to the lake that was there while a reload, which is someone asking for something new, gets a new one. Randomize takes any ?seed= off the address, since it no longer says which lake this is.

Times of day

Time The sky How often a lake comes with it
Day Flat, bright winter noon 35%
Dusk An orange horizon under deepening blue, and the first stars 20%
Night Moonless and clear: the Milky Way and thousands of stars 27%
Aurora The same night under slow green curtains, violet where they reach highest 18%

The sky is painted by a shader (src/render/sky.ts), and each time has its own light: the snow is orange at dusk, blue at night and faintly green under the aurora, and the milkiness of thick ice shows as much as there is light to show it. ?time=night on the address asks for a time whatever the settings say.

The lake at night, the Milky Way overhead

What else is in the sky

A comet with a long tail over the lake at night

Weather

Weather Snow How far you can see The sky Wind How often a lake comes with it
Clear none 600 m as the time of day has it none 64%
Snow falling quietly 420 m mostly cloud 0.5 to 3 m/s 26%
Blizzard thick, and driven 200 m all cloud 7 to 16 m/s, in gusts 10%

Snow that falls lies, in drifts (src/render/drifts.ts): long pale streaks on the ice, all running the way that lake's wind blows, a few of them under falling snow and many in a blizzard. A drift is the lake's bad ice. In the middle of one a skater is dragged at four times as hard as on ice and slows to about half speed; the ice under it does not ring to a strike or sing to the glide, and the blade shushes instead. Bare ice between the drifts is as fast and as loud as ever, so in a blizzard there is a reason to pick a way.

Snow in the air is a box of flakes that stays round the skater (src/render/snow.ts): they fall, the wind carries them, and they stream past as the skater moves, which is most of what makes a blizzard feel fast. Each lake's wind blows from a direction of its own. The wind can be heard, louder and higher in its gusts. Cloud hides the stars, the aurora and whatever else is up there, and takes the colour out of a sunset. ?weather=snow on the address asks for a weather whatever the settings say.

Snow falling over the lake by day

A game controller works as well: either stick or the direction buttons steer, up and down on the direction buttons or the shoulder buttons change pace, Y or triangle goes to another lake, and X or square turns the sound on and off. It needs no setting up beyond being connected, though a browser shows a page a controller only once one of its buttons has been pressed.

Steering turns the skater: the harder the steer, the tighter the curve, up to a circle 16 m in radius, with a moment to lean into it. Held over, the skater turns right round and can skate back up the lake, or round and round; that needs open water, about 24 m of it on the side being turned towards. A shore cannot be reached; near one the skater is turned to run along it.

The autopilot does not skate a straight line. It carves long, slow S-curves down the lake, 6 m either side of wherever its line is centred.

Steering changes the autopilot's plan; it does not interrupt it. Let go, and the skater carries on the way you sent them, from where they had got to: the autopilot's line is re-centred there and its next curve begins in your direction, wide at first if you sent it off at an angle, and settling back to its usual 6 m over about twenty seconds. It does not return to where it was, or to the middle of the lake. Steer left out of a right-hand curve and you have a left-hand curve.

The speed breathes with the stride: each push adds about a seventh of it, and the glide after it gives that back. Strides are of different lengths, and no two the same, so neither are the speeds.

Pace

Pace Speed Strides Turning circle
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
Cruise 7 m/s, 25 km/h 1.4 to 3.2 s 16 m Where it starts
Brisk 10 m/s, 36 km/h 1 to 2.4 s 22 m
Racing 12.5 m/s, 45 km/h 0.8 to 1.9 s 31 m
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

A change of pace is worked up to, or eased down to, over a few strides. Everything else follows the speed. A faster skater is held to half a g sideways and so turns in a wider circle; looks further ahead, and steers round an islet from further off; keeps further from the shores, which in ordinary water leaves a racer little room to weave; swings wider on the autopilot's line; sees a wider view, which is most of what makes speed look like speed; and hears the air rush past. The pace is remembered, and ?pace=olympic on the address asks for one for a visit.

The screen is kept awake while the page is open, where the browser allows it.

At a fork the lake parts round an island. A touch of steering one way, from 200 m before the fork, is taken as asking for that side, and the autopilot does the rest. With no touch it takes the side it is already over towards, and down the middle, the side the fork prefers. Until the island begins, a little over 200 m after the fork, you can still change, by steering the other way. After that there is land between, though a skater who turns back can go round its tip.

The ice

How thick the ice is decides how it looks and the note it sings. It does not decide whether it holds: for now nobody falls through, and the whole lake is safe to skate.

The zones are not chosen. They are the three thresholds of Gold's formula for what a sheet of ice will carry (src/ice/strength.ts), worked out for an 80 kg skater, and the note is the physics under The sound.

Ice Thickness Looks Sings at What happens
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
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
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
Safe over 4.8 cm milky grey below 630 Hz Nothing. The thicker, the lower and plainer the note

Of the ice a skater can reach, about 30% cracks, 35% sings and 35% is safe.

Impossible ice is a setting, and off to begin with. Off, the thinnest ice is 2.2 cm, the thinnest that is real, in patches that are on the point of giving way all the way across and crack every second or so. On, those patches go on thinning to 8 mm at their middles and fall silent: about 4% of the ice a skater can reach. They are bright pink, as bright at midnight as at noon: lit from within, in a colour chosen because nothing on a lake is that colour.

Falling through is written and switched off (RULES in src/skater.ts). Switched on, ice under 2.2 cm gives way and the skater is in the water for four seconds. It needs one thing first: a line the autopilot can be sure of, since nothing now keeps thin ice off its path.

Milky grey ice down the lake, with a black patch of the thinnest ice to the left of the middle

How the lake works

The lake parting round a wooded island ahead

A long island of reeds ahead in the middle of the lake

An islet of reeds standing in the ice, with more beyond it

Ice to a hazy horizon with no shore in sight

The sound

The sound is synthesized in the browser. None of it is a recording.

The physics

A sheet of ice floating on water carries bending waves, and they are dispersive: the higher the frequency, the faster it travels. The two sounds of thin ice follow from that (src/ice/acoustics.ts).

The model is from Romeyn, Hanssen, Ruud and Johansen, Sea ice thickness from air-coupled flexural waves, The Cryosphere 15, 2939–2955 (2021), who took the sound of the very film this app is modelled on and read about 725 Hz and 4.3 cm of ice from it. With textbook values for freshwater ice and nothing tuned, the model here gives 4.2 cm for 725 Hz.

What the film measures

npm run analyse-video measures a local copy of the film (it is not in this repository):

Measured Used for
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
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
How fast a ring dies 36 dB a second The ring's decay: to 1/e in 0.24 s

The chirps cannot be checked against the film. Three ways of doing it were tried, and the last is still in the tool: split each burst of sound into frequency bands, time when each band arrives, and see whether higher bands arrive earlier by as much as the model says. Of 122 bursts, 119 show no spread to measure: every band arrives within 30 ms of every other. The other three do not agree with each other or with the model. The likely reason is that the film's microphones were close to the skater, and a sound from close by has had no distance to spread out over. So the film gives the note and not the chirp. The chirps here are the model's, which is the paper's, and the paper measured the spreading directly, with instruments on the ice.

What is synthesized

src/audio/iceSound.ts, from noise and sine waves only:

npm run sound-check renders twelve seconds of it without playing it and draws its spectrogram: six seconds on the film's 4.1 cm, then onto ice thinning to 2.8 cm, with three cracks.

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

Develop

Needs Node 22.

npm install
npm run dev      # dev server on every interface, with a QR code for a phone on the same Wi-Fi
npm test         # vitest
npm run build    # typecheck, then build into dist/
npm run preview  # serve dist/
npm run screenshots  # build, then photograph the app at phone size into docs/screenshots/
npm run sound-check  # render the synthesized sound and draw its spectrogram into docs/sound/
npm run analyse-video  # measure a local copy of the reference film
npm run icons  # remake the home-screen and tab icons from assets/skate-shoes.png

On the address: ?seed=1 asks for lake number 1, ?time=night for a time of day (day, dusk, night, aurora), ?weather=snow for a weather (clear, snow, blizzard), ?pace=olympic for a pace (meander, cruise, brisk, racing, olympic), and ?s=2500 starts that many metres down the lake. The screenshots use all three, so that they are of the same thing every run.

The screenshots and the sound check need Playwright's Chromium once: npx playwright install chromium. The sound check and the analysis need ffmpeg.

Deploy

.gitlab-ci.yml tests and builds every push, and publishes the default branch to GitLab Pages. Asset paths are relative (vite.config.ts), so the build works at /nordic-skating/ or at the root of a unique Pages domain without configuration.

Static files go in static/, not public/: GitLab Pages needs its artifact to be named public, and a public/ already in the repository would collide with it.

Licence

MIT, for everything here but the icons. What it builds on is credited below.

Attribution

Source
1# Nordic Skating
2
3**Live — <https://apps.fuzzyworld.net/nordic-skating/>** · [Source](https://code.fuzzyworld.net/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
14<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
16## Status
17
18**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
21a 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
25the 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.
27
28<img src="docs/screenshots/aurora.png" alt="The lake at night under green curtains of aurora" width="640" />
29
301. ✅ Scaffold: build, tests, deploy to GitLab Pages.
312. ✅ An endless lake, skated on autopilot, with steering.
323. ✅ Measure the reference video, then synthesize the ice sound from the ice's thickness.
334. ✅ Ice zones that fall out of the physics, and falling through.
345. ✅ Forks in the lake, narrow passes, and stretches of open sea.
356. Game controller, then multiplayer. Postponed: [PLAN.md](PLAN.md) has what comes first.
36
37## Controls
38
39The button at the top right turns the sound on and off, as does the M key, and shows
40which it is: a speaker, or a speaker crossed out. Sound cannot start by itself: a
41browser lets a page make sound only after it has been touched, so until then the
42speaker is crossed out, and the first tap or key anywhere starts it. Turning it off is remembered for next time. On an iPhone the
43ring/silent switch also silences it.
44
45Do 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.
47
48| To | Touch or mouse | Keyboard |
49|---|---|---|
50| 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) | |
52| Turn right round, or circle an islet | Hold the steer over | the same |
53| Choose a way at a fork, or a side of an island | A touch towards it on the way up | the same |
54| Sound on or off | The button at the top right | M |
55| 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 |
56| 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
66## 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
77## 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 |
84| Weather | Surprise me, Clear, Snow, Blizzard | Surprise me: whichever the lake comes with |
85| Pace | Meander, Cruise, Brisk, Racing, Olympic | Cruise |
86| 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
91### Tilt to steer
92
93On 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
96left 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
101While 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
108An iPhone asks leave for this, each visit, at the first touch. If it has been refused
109it is not asked again, and the app says so where its hint is: Settings, Safari, Motion &
110Orientation Access turns it back on.
111Browsers 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
114### A different lake every visit
115
116A lake is a number from 1 to 999,999, and everything about it follows from the number:
117its 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.
119Opening 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.
123
124Which 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.
128
129### 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
145### 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
164**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,
173the 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
181**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
184setting up beyond being connected, though a browser shows a page a controller only
185once one of its buttons has been pressed.
186
187Steering turns the skater: the harder the steer, the tighter the curve, up to a circle
18816 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
191is turned to run along it.
192
193The autopilot does not skate a straight line. It carves long, slow S-curves down the
194lake, 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.
202
203The 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.
224
225The screen is kept awake while the page is open, where the browser allows it.
226
227At a fork the lake parts round an island. A touch of steering one way, from 200 m
228before 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
231200 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.
233
234## The ice
235
236How 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).
242
243| Ice | Thickness | Looks | Sings at | What happens |
244|---|---|---|---|---|
245| 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 |
246| 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 |
247| 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
250Of 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
254across and crack every second or so. On, those patches go on thinning to 8 mm at
255their 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.
258
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.
263
264<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" />
265
266## How the lake works
267
268- **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
284- **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
291- **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.
297- **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.
303- **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,
305 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.
311
312<img src="docs/screenshots/fork.png" alt="The lake parting round a wooded island ahead" width="640" />
313
314<img src="docs/screenshots/island.png" alt="A long island of reeds ahead in the middle of the lake" width="640" />
315
316<img src="docs/screenshots/islets.png" alt="An islet of reeds standing in the ice, with more beyond it" width="640" />
317
318<img src="docs/screenshots/sea.png" alt="Ice to a hazy horizon with no shore in sight" width="640" />
319
320- **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
323 skater may point any way at all, back up the lake included; left alone, the autopilot
324 turns them down it again.
325- **The skater is kept on the ice.** They have the width of the channel less 2 m at each
326 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.
329- **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`).
331- **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
333 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
337## The sound
338
339The 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
373**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.
382
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.
390- **The wind**, when there is one: low, loose noise, louder and higher in its gusts.
391- **Snow under the blade**, in a drift: a soft, low shush in place of the ring, the hum
392 and the scrape.
393- **The cracks**: every few seconds the ice cracks somewhere between 40 and 600 m off,
394 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.
400 Impossible ice does not crack at all.
401
402`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.
405
406<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" />
407
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/
418npm run screenshots # build, then photograph the app at phone size into docs/screenshots/
419npm 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
421npm run icons # remake the home-screen and tab icons from assets/skate-shoes.png
422```
423
424On the address: `?seed=1` asks for lake number 1, `?time=night` for a time of day (`day`,
425`dusk`, `night`, `aurora`), `?weather=snow` for a weather (`clear`, `snow`, `blizzard`), `?pace=olympic` for a pace
426(`meander`, `cruise`, `brisk`, `racing`, `olympic`),
427and `?s=2500` starts that many metres down the lake. The
428screenshots use all three, so that they are of the same thing every run.
429
430The screenshots and the sound check need Playwright's Chromium once:
431`npx playwright install chromium`. The sound check and the analysis need ffmpeg.
432
433## 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
442## Licence
443
444[MIT](LICENSE), for everything here but the icons. What it builds on is credited below.
445
446## Attribution
447
448- 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`,
453 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
455 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.
463- [three.js](https://threejs.org/) — MIT licence.
464- [simplex-noise](https://github.com/jwagner/simplex-noise.js) — MIT licence.
465- [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.
468- 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).
471- What ice will carry: Gold, L. W., "Use of ice covers for transportation", *Canadian
472 Geotechnical Journal* 8, 170–181, 1971.
473- 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.