fuzzyworld

nordic-skating / mistral-medium-35-littlejs / files / src / render / marks.ts

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

marks.ts

6.1 KB · history · blame

1import * as THREE from 'three'
2import { crackStar } from '../ice/cracks.js'
3
4/** How many stretches of line the lake remembers; when there are more, the oldest are forgotten. */
5const STRETCHES = 9000
6/** The line of the skates is drawn on in stretches this long, metres. */
7const TRACK_STEP = 0.5
8/** How fast a crack runs out from where it starts, m/s: quick, and slow enough to see go. */
9const CRACK_SPEED = 28
10/** When the skater is this far from where the marks are measured from, metres, they are measured from nearer. */
11const RECENTRE = 400
12
13type Colour = readonly [number, number, number, number]
14/** What each kind of mark looks like: its colour, and how solid it is. */
15const TRACK: Colour = [0.82, 0.9, 0.95, 0.5]
16const PUSH: Colour = [0.9, 0.95, 0.98, 0.75]
17const 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 */
31export 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}