nordic-skating / mistral-medium-35-littlejs / files / src / render / marks.ts
Branch mistral-medium-35-littlejs · default branch · all branches
marks.ts
| 1 | import * as THREE from 'three' |
| 2 | import { crackStar } from '../ice/cracks.js' |
| 3 | |
| 4 | /** How many stretches of line the lake remembers; when there are more, the oldest are forgotten. */ |
| 5 | const STRETCHES = 9000 |
| 6 | /** The line of the skates is drawn on in stretches this long, metres. */ |
| 7 | const TRACK_STEP = 0.5 |
| 8 | /** How fast a crack runs out from where it starts, m/s: quick, and slow enough to see go. */ |
| 9 | const CRACK_SPEED = 28 |
| 10 | /** When the skater is this far from where the marks are measured from, metres, they are measured from nearer. */ |
| 11 | const RECENTRE = 400 |
| 12 | |
| 13 | type Colour = readonly [number, number, number, number] |
| 14 | /** What each kind of mark looks like: its colour, and how solid it is. */ |
| 15 | const TRACK: Colour = [0.82, 0.9, 0.95, 0.5] |
| 16 | const PUSH: Colour = [0.9, 0.95, 0.98, 0.75] |
| 17 | const CRACK: Colour = [0.93, 0.97, 1, 0.95] |
| 18 | |
| 19 | /** |
| 20 | * What the skater leaves on the ice: the lines of their skates, the scratch of each push, |
| 21 | * and, on ice thin enough to crack, the cracks. |
| 22 | * |
| 23 | * Every mark is a hairline: a line with no width of its own, drawn a pixel wide however |
| 24 | * near it is, which is what a scratch or a crack in ice is. A crack runs out from where |
| 25 | * it starts over a moment, not all at once. |
| 26 | * |
| 27 | * The marks stay where they were made, so a skater who turns back, or circles an islet, |
| 28 | * comes on their own tracks, as in the film. The lake remembers the last few thousand |
| 29 | * stretches of line and forgets the ones before. |
| 30 | */ |
| 31 | export class Marks { |
| 32 | readonly mesh: THREE.LineSegments |
| 33 | private readonly places = new Float32Array(STRETCHES * 6) |
| 34 | private readonly colours = new Float32Array(STRETCHES * 8) |
| 35 | private readonly geometry = new THREE.BufferGeometry() |
| 36 | /** How many stretches have ever been drawn; the next goes in the place this leaves over. */ |
| 37 | private drawn = 0 |
| 38 | /** Where on the lake the marks are measured from: across, and along. */ |
| 39 | private fromX = 0 |
| 40 | private fromS = 0 |
| 41 | /** Where the line of the skates was last drawn to; nowhere when the pen is up. */ |
| 42 | private last: [number, number] | null = null |
| 43 | /** The stretches of cracks still to appear, soonest first, and the time by this clock. */ |
| 44 | private coming: { at: number; ax: number; as: number; bx: number; bs: number }[] = [] |
| 45 | private clock = 0 |
| 46 | |
| 47 | constructor() { |
| 48 | this.geometry.setAttribute('position', new THREE.BufferAttribute(this.places, 3)) |
| 49 | this.geometry.setAttribute('color', new THREE.BufferAttribute(this.colours, 4)) |
| 50 | this.geometry.setDrawRange(0, 0) |
| 51 | this.mesh = new THREE.LineSegments( |
| 52 | this.geometry, |
| 53 | new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, depthWrite: false }), |
| 54 | ) |
| 55 | // They are never far from the skater, and are moved with them; nothing is gained by asking. |
| 56 | this.mesh.frustumCulled = false |
| 57 | this.mesh.renderOrder = 1 |
| 58 | } |
| 59 | |
| 60 | /** How brightly the marks show: 1 by day, less by the light of a night. */ |
| 61 | set light(strength: number) { |
| 62 | ;(this.mesh.material as THREE.LineBasicMaterial).color.setScalar(strength) |
| 63 | } |
| 64 | |
| 65 | /** Another lake, or a skater set down somewhere else: nothing has been marked here. */ |
| 66 | clear(x: number, s: number): void { |
| 67 | this.drawn = 0 |
| 68 | this.fromX = x |
| 69 | this.fromS = s |
| 70 | this.last = null |
| 71 | this.coming = [] |
| 72 | this.geometry.setDrawRange(0, 0) |
| 73 | } |
| 74 | |
| 75 | /** One stretch of line, from one place on the lake to another. */ |
| 76 | private stretch(ax: number, as: number, bx: number, bs: number, colour: Colour): void { |
| 77 | const n = this.drawn++ % STRETCHES |
| 78 | // A hair above the ice, so that it is the line that shows and not the ice through it. |
| 79 | this.places.set([ax - this.fromX, 0.02, -(as - this.fromS), bx - this.fromX, 0.02, -(bs - this.fromS)], n * 6) |
| 80 | this.colours.set([...colour, ...colour], n * 8) |
| 81 | this.geometry.getAttribute('position').needsUpdate = true |
| 82 | this.geometry.getAttribute('color').needsUpdate = true |
| 83 | this.geometry.setDrawRange(0, Math.min(this.drawn, STRETCHES) * 2) |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * Keep the marks where they lie on the ice, for a skater at `x` across and `s` along, |
| 88 | * and let the cracks that are opening run on by `dt` seconds. |
| 89 | */ |
| 90 | follow(x: number, s: number, dt: number): void { |
| 91 | if (Math.abs(x - this.fromX) > RECENTRE || Math.abs(s - this.fromS) > RECENTRE) { |
| 92 | // Measured from where the skater now is, so that the numbers stay small. |
| 93 | for (let i = 0; i < this.places.length; i += 3) { |
| 94 | this.places[i] = this.places[i]! + this.fromX - x |
| 95 | this.places[i + 2] = this.places[i + 2]! - (this.fromS - s) |
| 96 | } |
| 97 | this.fromX = x |
| 98 | this.fromS = s |
| 99 | this.geometry.getAttribute('position').needsUpdate = true |
| 100 | } |
| 101 | this.mesh.position.set(this.fromX - x, 0, -(this.fromS - s)) |
| 102 | this.clock += dt |
| 103 | while (this.coming.length > 0 && this.coming[0]!.at <= this.clock) { |
| 104 | const { ax, as, bx, bs } = this.coming.shift()! |
| 105 | this.stretch(ax, as, bx, bs, CRACK) |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | /** The line of the skates, on to where the skater now is. */ |
| 110 | track(x: number, s: number): void { |
| 111 | if (this.last && Math.hypot(x - this.last[0], s - this.last[1]) < TRACK_STEP) return |
| 112 | if (this.last) this.stretch(this.last[0], this.last[1], x, s, TRACK) |
| 113 | this.last = [x, s] |
| 114 | } |
| 115 | |
| 116 | /** |
| 117 | * The scratch of a push: a short line off to one side of the way the skater is going, |
| 118 | * `foot` being -1 for a push off the left skate and 1 for the right. |
| 119 | */ |
| 120 | push(x: number, s: number, heading: number, foot: number): void { |
| 121 | // The skate is a little to that side of the skater, and is pushed out and back: a |
| 122 | // hundred degrees round from the way they are going. |
| 123 | const footX = x + foot * 0.25 * Math.cos(heading) |
| 124 | const footS = s - foot * 0.25 * Math.sin(heading) |
| 125 | const out = heading + foot * 1.75 |
| 126 | this.stretch(footX, footS, footX + 1.1 * Math.sin(out), footS + 1.1 * Math.cos(out), PUSH) |
| 127 | } |
| 128 | |
| 129 | /** |
| 130 | * A crack opening in the ice at `x`, `s`: a few arms running out from there, about |
| 131 | * `length` metres each, forking as they go (ice/cracks.ts), over the moment it takes |
| 132 | * a crack to run that far. |
| 133 | */ |
| 134 | crack(x: number, s: number, length: number): void { |
| 135 | for (const { from, to, along } of crackStar(Math.random, length)) { |
| 136 | this.coming.push({ at: this.clock + along / CRACK_SPEED, ax: x + from[0], as: s + from[1], bx: x + to[0], bs: s + to[1] }) |
| 137 | } |
| 138 | this.coming.sort((a, b) => a.at - b.at) |
| 139 | } |
| 140 | } |