nordic-skating / commit
docs: the README and the plan, brought up to what was built
.gitignore | 3 ++ PLAN.md | 137 ++++++++++++++++++++++++++++++++++++++----------------------- README.md | 126 +++++++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 192 insertions(+), 74 deletions(-) diff --git a/.gitignore b/.gitignore index 546d19a..e2d2c9d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ dist/ # Playwright verification screenshots .playwright-mcp/ +# Left by the test runner. +.vitest/ + # What tools/sound-check.mjs renders, beside the spectrogram of it that is kept. docs/sound/*.wav diff --git a/PLAN.md b/PLAN.md index df269ba..8607453 100644 --- a/PLAN.md +++ b/PLAN.md @@ -8,56 +8,90 @@ Last brought up to date 2026-10-09. ## Where things stand -Built and live: the endless lake, autopilot and steering, the synthesized sound, thin -ice and falling through, forks, narrow passes and open sea, the sound button, and the -links to the README and source. +Built: the endless lake, autopilot and steering, the synthesized sound, ice whose +thickness can be seen and heard, forks, narrow passes and open sea, the sound button, +the About box and the link to the source. Falling through is written and switched off. Postponed until the items below are ironed out: **game controller**, then **multiplayer** (the last step of the original six). ## Next -### 1. Motion - -**The problem.** The motion is awkward. Each stride moves the view from one side to the -other and tilts it, on a sine wave, so the skater seems to be shaken from leg to leg -rather than to glide. The speed never changes. Steering moves the skater sideways at a -fixed 4 m/s the instant it is touched, with no weight to it. - -**Pavel's idea.** Stop alternating the direction of motion; pick a Bezier curve or -similar and follow it, for smoother motion. Speed is part of this too. - -**Proposal** (not yet agreed; see the conversation of 2026-10-09). Four changes, in the -order they are expected to matter: - -1. **A speed that breathes with the stride.** A push speeds the skater up over about - half a second, and the glide after it slows them, by 10 to 15% each way around the - present 7 m/s. This is the main thing a skater feels, and it gives the long strides a - reason to be long. -2. **Steering that turns the skater instead of sliding them.** The skater gets a - direction and a limit on how sharply it can change. Input bends the path; letting go - lets it straighten. Paths become arcs by construction, which is what a curve fitted - through points would give, without the joins between curves. -3. **An autopilot that carves.** Rather than sitting exactly on the middle line, it - aims at a point some way ahead on a line that wanders gently across the channel, and - steers towards it with the same turning limit. Long, lazy S-curves down the lake. -4. **A head that stays level.** Take out the side-to-side sway and the tilt on every - stride: a skater's head is the stillest part of them. Keep a small lean into real - turns, and a slight rise on the push and sink through the glide. - -**Open questions.** -- Should the stride length follow the speed, or stay independent of it? -- Should thin ice or a narrow pass slow the skater down? -- How far may the autopilot's line wander from the middle, given that only the middle - 3 m of a channel is guaranteed to be sound ice? Either the wander stays inside that - strip, or the strip of sound ice follows the wandering line. - -### 2. Screen wake lock - -**Decided: add it.** On autopilot nobody touches the screen, so a phone dims and sleeps. -Ask the browser to keep the screen on while the page is visible, and ask again each time -the page comes back, since the lock is dropped when it is hidden. Earlier project to -copy from: `~/code/galaxy-z-flip7-butterchurn`. +### 1. Motion — built, waiting to be felt + +**The problem was** that each stride swung the view from side to side and tilted it, the +speed never changed, and steering slid the skater sideways at a fixed rate. + +**What was built** (2026-10-09), all four changes proposed: + +1. **A speed that breathes with the stride.** A push adds about 1 m/s over half a + second; drag takes it back through the glide. Around 7 m/s. +2. **Steering that turns the skater.** The skater has a place, a direction and a speed. + Input sets how fast the direction changes, up to a 16 m circle, with a third of a + second to lean into it. Paths are arcs by construction. +3. **An autopilot that carves.** It aims 18 m ahead on a line that wanders up to 6 m + either side of the middle, and steers with the same limits as a person. +4. **A head that stays level.** The side-to-side sway and tilt are gone. What is left is + a 3 cm rise on each push and a lean into real turns of at most 3.5°. + +**Decided by Pavel.** +- The whole lake is safe: falling is suspended, and thickness decides only the sound. +- Impossible ice is allowed: thinner than could hold anyone, down to 8 mm, never cracks. +- More complex paths are wanted later, so nothing here may rule them out. + +**Kept open on purpose.** The skater is a point with a direction on a flat sheet, not an +offset along a one-way line, so turning round is already something the model can do. +One rule stops it: the skater may point at most 50° off the line of the channel +(`MAX_HEADING` in `src/skater.ts`). What would still need doing for a skater going +backwards: forks are entered by passing a distance and are not left by passing back +over it, and only 100 m of shore is kept behind the skater. + +**Still to judge, by feel.** +- Are the push and the glide strong enough to notice, or too strong? +- Is the carving too wide, too tight, or about right? Should it stop in a narrow pass? +- Is the turning limit right for a finger on a phone? +- Should the stride length follow the speed, or thin ice or a pass slow the skater? + +### 2. Screen wake lock — built + +Asked for when the page opens, each time it comes back from being hidden, and at the +first touch, since some browsers give it only then. Where the browser has no such thing +it is silently skipped. Not checked on a phone: leave it on autopilot and see whether +the screen stays on. + +### 2a. Settings, times of day, and a different lake every visit — built + +Asked for by Pavel on 2026-10-09, with the reason: one of the main things about this is +to be reloading the page and finding yourself somewhere slightly different. + +**What was built.** +- **A settings box**, opened from the top left, at the foot of the screen so that what + it changes can be seen changing behind it. It is the control panel new options go in. +- **Impossible ice is a setting, off by default.** +- **Four times of day**: day, dusk, night under the Milky Way, and the aurora. The + setting starts on "Surprise me", which is whichever the lake comes with. +- **Every visit is another lake**: a number from 1 to 999,999 picked on opening, which + decides the water, the ice, the trees and the time of day. `?seed=` asks for one by + number, so a lake can still be come back to, linked to, photographed, and, later, + shared between players. +- **Randomize**, a button beside Settings (and the R key): another lake at once, + without a reload, and put in the browser's history so that Back returns to the lake + left. A reload still gets a new lake; which lake a page shows is kept with its place + in the history, not in the address. +- The Source link left the top bar to make room; it is in the About box. + +**Still to judge.** +- Are the Milky Way and the aurora right? The aurora moves, slowly; a still picture + cannot show whether it moves well. +- Is night too dark to read the ice by, on a phone outdoors? +- Is "slightly different" different enough? Only the lake and the time vary now. + +**Ideas for more variety between lakes**, none started: a moon and its phase, falling +snow, mist that thins and thickens, wind in the sound, dawn, how wooded the banks are, +how hilly, the colour of the aurora, how fast it moves. + +**Ideas for more settings**, none started: speed, how far the autopilot carves, how long +the strides are, how much the ice cracks, falling through, the haze. ### 3. Installable app @@ -81,10 +115,9 @@ channel parts round it and joins again 20 to 40 m later. The autopilot goes roun side; the skater may choose the other, or circle it. **Open questions.** -- Circling it means turning back, and today the lake only runs forward: distance skated - can only grow. Either "a loop" means a wide swerve round it and on, or the lake needs - a way to hold a skater who is, for a moment, going backwards. This depends on how the - motion work (item 1) comes out, so it follows it. +- Circling it means turning back. The skater can now do that in principle (item 1, + "Kept open on purpose"); the lake's forks and the shore kept behind the skater cannot + yet. - The film has not been looked at for this: find the scene and see what the skater actually does, and how big the patch is. - Channels that fork and later rejoin, and three-way forks, would both come from the @@ -118,8 +151,9 @@ before it lands may need giving again. ### Competitive mode -**Decided: low stakes for now.** The aim at present is something fun and relaxing: falling -through costs four seconds and nothing is scored. A competitive mode for people who want +**Decided: low stakes for now.** The aim at present is something fun and relaxing: nobody +falls through and nothing is scored. Falling is written and switched off (`RULES` in +`src/skater.ts`), and would be part of this mode. A competitive mode for people who want one is a maybe, as an option beside the relaxed one and never instead of it. Ideas: time spent on singing ice, distance without falling, a set course against the clock. @@ -146,4 +180,5 @@ Raised, answered, and needing nothing more. | Forks are two-way, not "multiple" | Fine as they are for now; more complex forks are part of item 4 | | Thick ice does not sound "bad"; the bad ice is the thin end | Fine as it is | | Frame rate on a phone, including at sea | Good, by Pavel's testing | -| Falling through has no cost and nothing is scored | Intended for now; see Competitive mode | +| Falling through has no cost and nothing is scored | Falling is switched off altogether for now (2026-10-09); see Competitive mode | +| Only the middle 3 m of a channel was guaranteed sound ice, and the autopilot was to wander | Gone with falling: the strip of sound ice was removed. Turning falling back on needs a safe line for the autopilot first | diff --git a/README.md b/README.md index 0814be1..e36d32b 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,13 @@ in which Mårten Ajne skates a small lake outside Stockholm. **Step 5 of 6: forks, passes and open sea.** The lake runs on for ever between snowy, wooded shores, forking every few kilometres, pinching to narrow passes and opening out until no shore can be seen. 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, cracks under you, -and at its thinnest gives way. +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. + +<img src="docs/screenshots/aurora.png" alt="The lake at night under green curtains of aurora" width="640" /> 1. ✅ Scaffold: build, tests, deploy to GitLab Pages. 2. ✅ An endless lake, skated on autopilot, with steering. @@ -44,12 +49,71 @@ one side of every fork, for as long as you leave it. | Steer left or right | Drag sideways from wherever you touch | ← → or A D | | Choose a way at a fork | Steer towards it before the island begins | the same | | Sound on or off | The button at the top right | M | +| 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. + +## 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 | +| Impossible ice | on or off | off | +| Lake | Another lake (the same as Randomize); a link back to this one | a different lake every visit | + +<img src="docs/screenshots/settings.png" alt="The settings box at the foot of the screen, under a night sky" width="320" /> + +### 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, and the time of day it comes with. +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. -The links at the top left, About and Source, open this README and the repository in a -new tab, so the skate is still going when you come back. +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. -Steering moves the skater across the lake, never into a shore. Let go and the skater -holds that line for a second and a half, then eases back to the middle. +### 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. + +<img src="docs/screenshots/night.png" alt="The lake at night, the Milky Way overhead" width="640" /> + +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. A shore cannot be reached; near one the +skater is turned to run along it. Let go and the skater straightens up where they are, +keeps that line for a second and a half, and then eases back to the autopilot's own. + +The autopilot does not skate a straight line. It carves long, slow S-curves down the +lake, up to 6 m either side of the middle where there is the room. + +The speed breathes with the stride: each push adds about 1 m/s over half a second, and +the glide after it gives that back, around 7 m/s (25 km/h). Strides last 1.4 to 3.2 s +and no two are the same, so neither are the speeds. + +The screen is kept awake while the page is open, where the browser allows it. At a fork the lake parts round an island. Until the island begins, a little over 200 m after the fork, you are in whichever of the two channels you are nearer the middle of, @@ -57,23 +121,33 @@ so steering across takes the other one. After that there is land between. ## The ice -How thick the ice is decides everything about it: how it looks, the note it sings, and -whether it holds. 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](#the-sound). +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](#the-sound). | Ice | Thickness | Looks | Sings at | What happens | |---|---|---|---|---| -| Gives way | under 2.2 cm | black | above 1390 Hz | You go through. Four seconds later you skate on from the middle of the lake | -| Cracks | 2.2 to 3.4 cm | nearly black | 900 to 1390 Hz | It holds, cracking around you: more often and nearer the thinner it gets | +| Impossible, if turned on | 0.8 to 2.2 cm | black, lit faintly green from within | 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 2% gives way, 18% cracks, 45% sings and 35% is -safe. The middle of every channel is never thinner than 4.1 cm, so the autopilot never -falls through: where a thin patch crosses the middle, a strip of sound ice bridges it. +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 several times a second. On, those patches go on thinning to 8 mm at +their middles and fall silent: about 4% of the ice a skater can reach. + +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. -<img src="docs/screenshots/thin-ice.png" alt="Milky grey ice down the lake, with a black patch of thin ice to the left of the middle" width="640" /> +<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" /> ## How the lake works @@ -97,11 +171,15 @@ falls through: where a thin patch crosses the middle, a strip of sound ice bridg <img src="docs/screenshots/sea.png" alt="Ice to a hazy horizon with no shore in sight" width="640" /> -- **The skater is an offset from the middle of a channel** (`src/skater.ts`), not a - position. An offset nobody touches follows every bend, which is all the autopilot is. - Where the lake narrows faster than a skater could steer clear, the room they have - starts closing 200 m early, so a shore never sweeps them sideways. Out at sea they may - go no further than 150 m from the middle. +- **The skater has a place, a direction and a speed** (`src/skater.ts`), and steering is + turning. The autopilot steers by the same means and within the same limits, aiming at + a point 18 m ahead on its wandering line, so every path on the lake is an arc. The + skater may point up to 50° off the line of the channel; that is a rule of its own, + and lifting it would let them turn right round. +- **The skater is kept on the ice.** They have the width of the channel less 2 m at each + shore. Within 8 m of the edge of that they may point less and less towards it. Where + the lake narrows faster than a skater could steer clear, their room starts closing + 200 m early. Out at sea they may go no further than 150 m from the middle. - **Thickness is a function too**, and is painted on the ice from a small map of the 768 m around the skater that is redrawn every 32 m (`src/render/ice.ts`). - **The skater never leaves the origin of what is drawn.** The shores and islands are @@ -160,7 +238,7 @@ against the film. - **The cracks**: every few seconds the ice cracks somewhere between 40 and 600 m off, and the chirp for that distance and that thickness arrives. On ice thin enough to crack under the skater they come from a few metres away, and faster the thinner it is. -- **Going through**: four cracks in a fifth of a second, and the water. + Impossible ice does not crack at all. `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 @@ -183,7 +261,9 @@ npm run sound-check # render the synthesized sound and draw its spectrogram int npm run analyse-video # measure a local copy of the reference film ``` -`?s=2500` on the URL starts that many metres down the lake. +On the address: `?seed=1` asks for lake number 1, `?time=night` for a time of day (`day`, +`dusk`, `night`, `aurora`), 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.