@kvmet/beefwife-canvas 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
3
3
  Notable changes to `@kvmet/beefwife-canvas`, newest first. This package follows
4
4
  [semantic versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## 0.1.1 (2026-11-14)
7
+
8
+ Documentation only; the runtime is unchanged.
9
+
10
+ The README is rewritten as an install guide and API reference, with a table
11
+ of every option and its default.
12
+
6
13
  ## 0.1.0 (2026-11-14)
7
14
 
8
15
  First release.
package/README.md CHANGED
@@ -1,252 +1,297 @@
1
1
  # BeefwifeCanvas
2
2
 
3
- `BeefwifeCanvas` mounts the Beefwife runtime into a canvas that belongs to the
4
- page. The canvas can have any ID, class, layout, or stacking rules; the API only
5
- looks for `data-beefwife-canvas` when auto-mounting.
3
+ BeefwifeCanvas fills a `<canvas>` on your page with beefwives: procedurally
4
+ animated creatures that wander the canvas and walk around the elements you
5
+ mark. Add the canvas, name the creatures, and load two scripts:
6
6
 
7
7
  ```html
8
8
  <canvas
9
- id="my-beefwives"
10
9
  data-beefwife-canvas
11
- data-beefwife-manifest="beefwives/manifest.json"
12
- data-beefwife-count="8"
13
- data-beefwife-resolution-scale="0.25"
14
- data-beefwife-image-rendering="pixelated"
15
- data-beefwife-round-vertices="true"
16
- data-beefwife-antialias="false"
17
- data-beefwife-simulation-fps="60"
18
- data-beefwife-draw-fps="24"
19
- data-beefwife-target-mode="wander"
20
- data-beefwife-pointer-input="click"
21
- data-beefwife-wander-delay="4"
22
- data-beefwife-knee-projection-center="canvas"
10
+ data-beefwife-sources="
11
+ https://cdn.jsdelivr.net/npm/@kvmet/beefwife@0.1.1/samples/slow-guy.json,
12
+ https://cdn.jsdelivr.net/npm/@kvmet/beefwife@0.1.1/samples/long-girl.json"
23
13
  ></canvas>
24
- ```
25
-
26
- ## Install
27
-
28
- Load Pixi 8, then the runtime. Beefwife and Terrain ride inside
29
- `beefwife-canvas.js`, so the page needs nothing else and gains one global,
30
- `BeefwifeCanvas`.
31
-
32
- Creatures are separate: the canvas draws whichever descriptors it is pointed
33
- at. `@kvmet/beefwife` ships seven samples under `samples/`, reachable from a
34
- CDN without installing it.
35
14
 
36
- From a CDN:
37
-
38
- ```html
39
15
  <script src="https://cdn.jsdelivr.net/npm/pixi.js@8.23.0/dist/pixi.min.js"></script>
40
- <script src="https://cdn.jsdelivr.net/npm/@kvmet/beefwife-canvas@0.1.0/beefwife-canvas.min.js"></script>
16
+ <script src="https://cdn.jsdelivr.net/npm/@kvmet/beefwife-canvas@0.1.1/beefwife-canvas.min.js"></script>
41
17
  ```
42
18
 
43
- With npm:
19
+ The canvas stays yours. Give it any id, class, size, or stacking order;
20
+ BeefwifeCanvas reads only its `data-beefwife-*` attributes.
21
+
22
+ ## Install
23
+
24
+ From a CDN, use the two scripts above. With npm:
44
25
 
45
26
  ```sh
46
27
  npm install @kvmet/beefwife-canvas pixi.js
47
28
  ```
48
29
 
49
- With a bundler, import the module entry. Pixi stays external, so the page
50
- and the runtime share one copy:
51
-
52
30
  ```js
53
31
  import BeefwifeCanvas from "@kvmet/beefwife-canvas";
54
32
  ```
55
33
 
56
- The import mounts every marked canvas on its own, the same as the script
57
- does, so importing for the side effect alone is enough. Without a bundler,
58
- serve the script instead:
34
+ | File | Use |
35
+ | --- | --- |
36
+ | `beefwife-canvas.min.js` | Classic script. Reads `window.PIXI` and adds one global, `BeefwifeCanvas`. |
37
+ | `beefwife-canvas.js` | The same script, unminified. |
38
+ | `beefwife-canvas.mjs` | Module. Imports `pixi.js`, so a bundler gives the page and the runtime one copy. |
59
39
 
60
- ```html
61
- <script src="/node_modules/pixi.js/dist/pixi.min.js"></script>
62
- <script src="/node_modules/@kvmet/beefwife-canvas/beefwife-canvas.min.js"></script>
63
- ```
40
+ Every entry mounts each `canvas[data-beefwife-canvas]` once the DOM is ready,
41
+ so an import with no other code is enough. `@kvmet/beefwife` and
42
+ `@kvmet/terrain` are bundled in.
43
+
44
+ Pixi 8.21.0 or newer is required. In earlier 8.x releases, destroying one
45
+ WebGPU renderer can break another.
46
+
47
+ ## Creatures
48
+
49
+ A creature is a descriptor: one JSON file that defines its body, gait, and
50
+ look. `@kvmet/beefwife` ships seven under `samples/`: `bounding`,
51
+ `chevron-guy`, `long-girl`, `peristaltic`, `reticulating`, `slow-guy`, and
52
+ `undulating`. The format is in
53
+ [DESCRIPTOR.md](https://github.com/kvmet/beefwife/blob/main/beefwife/DESCRIPTOR.md).
54
+
55
+ Three options supply descriptors. Use any mix; they add together.
56
+
57
+ | Option | Accepts |
58
+ | --- | --- |
59
+ | `sources` | A descriptor URL, a `{ src, weight }` entry, or an array of either. The attribute takes a comma-separated list of URLs. |
60
+ | `manifest` | The URL of a manifest. From JavaScript, also the parsed manifest object. |
61
+ | `descriptors` | JavaScript only. A parsed descriptor, a `{ descriptor, weight }` entry, or an array of either. |
64
62
 
65
- Pixi 8.21.0 is the minimum: in earlier 8.x releases, destroying one WebGPU
66
- renderer can break another. The script entry reads `window.PIXI` and the
67
- module entry imports the peer, so only the script entry runs where no
68
- renderer is present.
69
-
70
- `beefwife-canvas.js` is the readable build and creates the same global;
71
- `beefwife-canvas.mjs` is the module build. `npm run build` writes all three
72
- from [`src`](src/); all are committed. The integrated development example is
73
- in [`examples/beefwife-canvas`](../examples/beefwife-canvas/).
74
-
75
- The bundle auto-mounts every marked canvas after the DOM is ready.
76
- Listen for `beefwifecanvasready` or `beefwifecanvaserror`, await the `ready`
77
- promise on a handle returned by `BeefwifeCanvas.get(canvas)`, or inspect
78
- `data-beefwife-state`, whose values are `loading`, `ready`, `running`, `paused`,
79
- `stopped`, `error`, and `destroyed`. A paused canvas also exposes
80
- `data-beefwife-pause-reason` as `hidden`, `offscreen`, or `zero-size`. A mount
81
- or a texture render that fails dispatches the error event, stops the runtime,
82
- cleans itself up, and leaves the canvas in the `error` state, ready to mount again.
83
- The mounted handle exposes the same information through its read-only `state`
84
- and `pauseReason` getters; `pauseReason` is `null` unless state is `paused`.
85
-
86
- `BeefwifeCanvas.scan()` mounts any currently unmounted
87
- `canvas[data-beefwife-canvas]` elements. The initial document is scanned
88
- automatically; call `scan()` after adding declarative canvases dynamically.
89
- Already mounted canvases are left alone.
90
-
91
- The manifest is deliberately small and versioned. Relative source URLs are
92
- resolved from the manifest URL. Weights affect random spawning, not loading.
63
+ A manifest lists sources in one file:
93
64
 
94
65
  ```json
95
66
  {
96
67
  "schemaVersion": 1,
97
- "sources": ["slug-a.json", { "src": "slug-b.json", "weight": 3 }]
68
+ "sources": ["slow-guy.json", { "src": "long-girl.json", "weight": 3 }]
98
69
  }
99
70
  ```
100
71
 
101
- `manifest`, `sources`, and `descriptors` are additive when more than one is
102
- provided. Programmatic `manifest` may be either a manifest URL or the parsed
103
- manifest object. `sources` accepts one descriptor URL, one `{ src, weight }`
104
- entry, or an array of either. `descriptors` likewise accepts one descriptor,
105
- one `{ descriptor, weight }` entry, or an array. Declarative attributes support
106
- a manifest URL and a comma-separated source URL list; parsed descriptors are
107
- JavaScript-only. Every resulting descriptor name must be unique across all
108
- inputs or the mount fails atomically. When `count` is omitted it defaults to
109
- the number of unique descriptors, with weighted names sampled with replacement.
72
+ Relative URLs in a manifest resolve against the manifest's URL. Other URLs
73
+ resolve against the page.
110
74
 
111
- JavaScript options override attributes and can supply already-parsed
112
- descriptors or arbitrary caller-owned Pixi filters. Omit `data-beefwife-canvas`
113
- when mounting programmatically so auto-mount and programmatic mount do not race.
75
+ `weight` defaults to 1 and sets how often a creature is picked when one
76
+ spawns. It does not change what loads. `count` defaults to the number of
77
+ descriptors. Each descriptor's `name` must be unique across all inputs, or the
78
+ mount fails.
79
+
80
+ ## Mount from JavaScript
81
+
82
+ Leave `data-beefwife-canvas` off a canvas you mount yourself, so the automatic
83
+ mount does not race yours:
114
84
 
115
85
  ```js
116
- const canvas = document.querySelector("#programmatic-beefwives");
117
- const runtime = await BeefwifeCanvas.mount(canvas, {
86
+ const canvas = document.querySelector("#beefwives");
87
+ const beefwives = await BeefwifeCanvas.mount(canvas, {
118
88
  descriptors: [descriptorA, { descriptor: descriptorB, weight: 3 }],
119
89
  filters: [new PIXI.NoiseFilter({ noise: 0.08 })],
120
90
  targetMode: "manual",
121
- pointerInput: "none",
122
91
  });
123
92
 
124
- runtime.setTarget({ x: 300, y: 180 });
125
- runtime.getActors()[0].setTarget({ x: 80, y: 120 });
93
+ beefwives.setTarget({ x: 300, y: 180 });
94
+ beefwives.getActors()[0].setTarget({ x: 80, y: 120 });
126
95
  ```
127
96
 
128
- `mount` resolves a frozen runtime handle. `BeefwifeCanvas.get(canvas)` and the
129
- `detail.controller` of either lifecycle event return the same handle while the
130
- event is being dispatched. A failed handle is released after its error event,
131
- so `get(canvas)` then returns `null` and a new mount can begin.
132
-
133
- The lifecycle methods are `start()`, `stop()`, `destroy()`, `setCount(count)`,
134
- `setTimeScale(scale)`, `setTarget(point)`, `clearTarget()`,
135
- `setTargetMode(mode)`, `setPointerInput(input)`, `setDebug(flags)`,
136
- `refreshTerrain()`, `respawn()`, `getActors()`, `getTerrainView()`, and
137
- `getStats()`.
138
- `getActors()` returns frozen Canvas control handles with stable `id` and `name`
139
- fields plus `getPose()`, `getRoute()`, `getTarget()`, target, mode, and respawn
140
- methods; it does not expose the host's mutable actor or route objects.
141
- Targets and poses use canvas-local CSS pixels. Target policy and DOM input are
142
- independent. `targetMode` is `wander` or `manual`; `pointerInput` is `none`,
143
- `click`, or `move`. A click or pointer movement merely supplies a target and
144
- does not change what happens after arrival. In `manual`, every supplied target
145
- produces one finite route and the beefwife then stops until another target is
146
- supplied. In `wander`, the host waits a random duration from zero through
147
- `wanderDelay` seconds before supplying another target. `move` retains its last
148
- destination after the pointer leaves the canvas. The host chooses goals and
149
- Beefwife only receives its usual direction and throttle controls. Attribute
150
- values are read once. Use the methods for live changes.
151
-
152
- `destroy()` removes the scene and runtime listeners but leaves the author's
153
- canvas in place. While the canvas stays in the page, its Pixi renderer is kept
154
- dormant so the same element can be mounted again without a second GPU context.
155
- `antialias` is fixed for the life of that renderer, so a remount cannot change
156
- it. When a destroyed canvas leaves the page, its renderer and GPU context are
157
- released and the canvas can be garbage collected; mounting that element again
158
- fails, so mount a new canvas instead. A canvas removed without `destroy()`
159
- keeps its renderer and GPU context for the life of the page, so call
160
- `destroy()` when you remove one, before or after removing it.
161
-
162
- Performance and appearance options are `resolutionScale`, `roundVertices`,
163
- `antialias`, `simulationFps`, `drawFps`, `maxPixelRatio`, `imageRendering`,
164
- `kneePerspective`, `maxKneeOffset`, `kneeProjectionCenter`, and programmatic
165
- `filters`. `drawFps` is held at `simulationFps`, because a draw between two
166
- simulation steps repeats the frame before it. `simulationFps` defaults to 60
167
- and `drawFps` to 24; both accept 1 through 240, and a `drawFps` of zero asks
168
- for the `simulationFps` ceiling by name.
169
-
170
- The substep size is fixed, so a simulated second always costs the same number
171
- of substeps however `simulationFps` batches them. Lowering it buys fewer,
172
- larger steps and the routing saved between them, not cheaper physics: 10
173
- against 60 saves 8% of the simulation cost at 400 creatures. It also steers
174
- more coarsely, because a route advances once a tick, so creatures take
175
- visibly different paths. To spend less on physics, lower `timeScale`, which
176
- reduces simulated seconds per real second and says so by moving the creature
177
- slower. `drawFps` is the knob that pays without touching behaviour.
178
-
179
- A host that cannot deliver `simulationFps` is not the same as a lower
180
- `simulationFps`. It plays slower and follows the same path, because each tick
181
- steps once whatever the frame took, so the route still advances one 60th of a
182
- second at a time.
183
-
184
- Rates need not divide each other. A step that is not a whole number of
185
- substeps carries its remainder, so `simulationFps` of 24 or a `timeScale` of
186
- 0.7 stays within one substep of exact for any run length rather than drifting.
187
-
188
- Knee perspective affects
189
- leg-knee rendering only; planted feet and
190
- the simulated body are unchanged. `kneeProjectionCenter` is
191
- `canvas` by default; `viewport` makes separate canvases share one apparent
192
- knee-projection field. `resolutionScale` accepts 0.125 through 1 and defaults
193
- to 0.25, where quarter resolution costs a sixteenth of the fill work. When
194
- vertex rounding is enabled, vertices snap to pixels at the effective renderer
195
- resolution, including device pixel ratio and `resolutionScale`.
196
- `imageRendering` independently selects `pixelated` (the default) or `auto`,
197
- allowing either interpolation style at any resolution. For color effects, pass
198
- a configured `PIXI.ColorMatrixFilter` in `filters`.
199
-
200
- Debug rendering is off by default. The boolean options `debugTargets`,
201
- `debugRoutes`, and `debugTerrain` show destination crosshairs, waypoint paths,
202
- and avoidance bounds, respectively. Their declarative forms are
203
- `data-beefwife-debug-targets`, `data-beefwife-debug-routes`,
204
- and `data-beefwife-debug-terrain`. Toggle layers after mounting with a partial
205
- update:
97
+ JavaScript options override attributes. Attributes are read once, at mount;
98
+ use the handle's methods for live changes.
99
+
100
+ ## Options
101
+
102
+ Each option has an attribute: `data-beefwife-` and the option name in kebab
103
+ case, so `drawFps` is `data-beefwife-draw-fps`. Boolean attributes take `true`
104
+ or `false`. JavaScript-only options are marked. An unknown option or attribute
105
+ fails the mount. Distances are CSS pixels.
106
+
107
+ ### Population and movement
108
+
109
+ | Option | Default | Meaning |
110
+ | --- | --- | --- |
111
+ | `count` | Number of descriptors | Creatures on the canvas, 0 to 1024. |
112
+ | `targetMode` | `"wander"` | `wander`: after each arrival, wait and pick a new target. `manual`: stop at each target until given another. |
113
+ | `pointerInput` | `"none"` | `click`: a click sets the target. `move`: the pointer is the target, and its last position stays the target after it leaves. |
114
+ | `wanderDelay` | `4` | Longest wait in seconds before a wandering creature picks a new target. Each wait is random, from 0 to this. |
115
+ | `timeScale` | `1` | Simulated seconds per real second, 0 to 16. |
116
+ | `autoStart` | `true` | Run once ready. `false` leaves the canvas `stopped` until `start()`. |
117
+ | `random` | `Math.random` | JavaScript only. A function returning numbers from 0 up to 1. Drives spawning, wandering, and creature variation. |
118
+
119
+ ### Avoidance
120
+
121
+ | Option | Default | Meaning |
122
+ | --- | --- | --- |
123
+ | `avoid` | `".beefwife-avoid"` | Selector for the elements creatures route around. |
124
+ | `edgeMargin` | `25` | Clear space kept inside each canvas edge. |
125
+ | `obstaclePadding` | `0` | Clear space kept around each avoided element. |
126
+ | `waypointRadius` | `10` | Distance from a route corner that counts as reaching it. Above 1. |
127
+ | `arrivalRadius` | `10` | Distance from the target that counts as arriving. Above 1. |
128
+ | `throttleEase` | `4` | Rate per second at which a creature's speed settles to what its route asks for. |
129
+ | `stuckReplanSeconds` | `120` | Seconds without reaching a waypoint before a creature drops its route. |
130
+ | `escapeReplanSeconds` | `7` | The same limit for a creature caught inside an avoided area or past an edge. |
131
+
132
+ ### Rendering
133
+
134
+ | Option | Default | Meaning |
135
+ | --- | --- | --- |
136
+ | `resolutionScale` | `0.25` | Render resolution against CSS pixels, 0.125 to 1. At 0.25 the renderer fills a sixteenth of the pixels. |
137
+ | `imageRendering` | `"pixelated"` | How the canvas is scaled up: `pixelated` or `auto`. |
138
+ | `roundVertices` | `true` | Snap vertices to the renderer's pixel grid, device pixel ratio included. |
139
+ | `antialias` | `false` | Fixed for the life of the canvas's renderer. A remount cannot change it. |
140
+ | `maxPixelRatio` | `2` | Upper limit on `devicePixelRatio`. |
141
+ | `filters` | None | JavaScript only. Pixi filters for the whole scene, such as a `PIXI.ColorMatrixFilter`. |
142
+ | `kneePerspective` | `0.002` | Pushes each drawn knee away from the projection center, by distance times knee height times this. `0` turns it off. |
143
+ | `maxKneeOffset` | `256` | Upper limit on that push, in pixels. |
144
+ | `kneeProjectionCenter` | `"canvas"` | `canvas`: center the projection on each canvas. `viewport`: center it on the window, so separate canvases share one. |
145
+ | `simulationFps` | `60` | Simulation updates per second, 1 to 240. |
146
+ | `drawFps` | `24` | Draws per second, 1 to 240, limited to `simulationFps`. `0` means match `simulationFps`. |
147
+
148
+ Knee perspective changes only the drawn knees. Feet and bodies stay where the
149
+ simulation puts them.
150
+
151
+ ### Pausing and debug
152
+
153
+ | Option | Default | Meaning |
154
+ | --- | --- | --- |
155
+ | `pauseHidden` | `true` | Pause while the tab is hidden. |
156
+ | `pauseOffscreen` | `true` | Pause while the canvas is out of view. |
157
+ | `debugTargets` | `false` | Draw a crosshair at each target. |
158
+ | `debugRoutes` | `false` | Draw each route's waypoints. |
159
+ | `debugTerrain` | `false` | Draw the avoided areas, padding included. |
160
+
161
+ Debug layers draw at the canvas's render resolution and do not change routing
162
+ or simulation. To draw your own overlay instead, read `getTerrainView()` and
163
+ each creature handle's `getRoute()` and `getTarget()`.
164
+
165
+ ## API
166
+
167
+ ### `BeefwifeCanvas.mount(canvas, options = {})`
168
+
169
+ Mounts the canvas and returns a promise of its handle. Rejects when the canvas
170
+ already has a handle, an option is invalid, or a descriptor fails to load or
171
+ validate.
172
+
173
+ ### `BeefwifeCanvas.get(canvas)`
174
+
175
+ Returns the canvas's handle, or `null`.
176
+
177
+ ### `BeefwifeCanvas.scan()`
178
+
179
+ Mounts each `canvas[data-beefwife-canvas]` that has no handle. The page is
180
+ scanned once on load; call `scan()` after you add marked canvases.
181
+
182
+ ### `BeefwifeCanvas.Descriptor`
183
+
184
+ The `Descriptor` API from `@kvmet/beefwife`, for reading, checking, and
185
+ resizing descriptors without a second script.
186
+
187
+ ### Handle
188
+
189
+ Coordinates are canvas-local CSS pixels. Methods that change something return
190
+ the handle. After `destroy()`, every method throws.
191
+
192
+ | Member | Does |
193
+ | --- | --- |
194
+ | `start()`, `stop()` | Run or stop the canvas. A stopped canvas does not resume when it comes back into view. |
195
+ | `destroy()` | Remove the scene and listeners. See [Lifecycle](#lifecycle). |
196
+ | `setCount(count)` | Add or remove creatures. |
197
+ | `setTimeScale(scale)` | Change `timeScale`. |
198
+ | `setTarget({ x, y })` | Send every creature to a point. |
199
+ | `clearTarget()` | Drop every creature's target. |
200
+ | `setTargetMode(mode)` | `"wander"` or `"manual"`. |
201
+ | `setPointerInput(input)` | `"none"`, `"click"`, or `"move"`. |
202
+ | `setDebug(flags)` | Turn debug layers on or off: `{ targets, routes, terrain }`. Omitted layers keep their state. |
203
+ | `respawn()` | Place every creature again. |
204
+ | `refreshTerrain()` | Measure the avoided elements now. See [Avoidance and occlusion](#avoidance-and-occlusion). |
205
+ | `getActors()` | One creature handle per creature. |
206
+ | `getTerrainView()` | `{ bounds, rectangles }` from the last measurement: the walkable area inside `edgeMargin`, and each avoided element with its padding. Each is `{ left, top, right, bottom }`. |
207
+ | `getStats()` | See [Performance](#performance). |
208
+ | `state` | The canvas state. See [Lifecycle](#lifecycle). |
209
+ | `pauseReason` | `"hidden"`, `"offscreen"`, or `"zero-size"` while `state` is `"paused"`, else `null`. |
210
+ | `ready` | The promise that `mount` returned. |
211
+
212
+ ### Creature handle
213
+
214
+ `getActors()` returns frozen handles. A creature keeps the same handle, with a
215
+ stable `id` and its descriptor's `name`, for as long as it lives.
216
+
217
+ | Member | Does |
218
+ | --- | --- |
219
+ | `getPose()` | `{ head, center, direction }`, each `{ x, y }`. `direction` is a unit vector. |
220
+ | `getRoute()` | The waypoints left on its route. |
221
+ | `getTarget()` | Its target, or `null`. |
222
+ | `setTarget({ x, y })`, `clearTarget()` | Set or drop its target. |
223
+ | `setTargetMode(mode)` | `"wander"` or `"manual"`, for this creature. |
224
+ | `respawn()` | Place it again. |
225
+ | `setDescriptor(descriptor)` | Change it to another descriptor in place. It keeps its position and motion. |
226
+
227
+ The `get` methods return copies.
206
228
 
207
- ```js
208
- runtime.setDebug({ targets: true, routes: true });
209
- runtime.setDebug({ routes: false, terrain: true });
210
- ```
229
+ ## Lifecycle
211
230
 
212
- Terrain draws below the beefwives; routes and targets draw above them. These
213
- layers are diagnostic and do not affect routing or simulation. Their marks are
214
- sized in renderer pixels, so a mark keeps its shape at every `resolutionScale`
215
- and grows on screen as the renderer coarsens.
231
+ `handle.state` and the canvas's `data-beefwife-state` attribute hold one of
232
+ `loading`, `ready`, `running`, `paused`, `stopped`, `error`, or `destroyed`.
233
+ While paused, `data-beefwife-pause-reason` holds the reason.
234
+
235
+ The canvas dispatches two events, each with the handle as
236
+ `event.detail.controller`:
216
237
 
217
- To draw a debug layer of your own instead, read the same state and draw it at
218
- whatever resolution you like. `getTerrainView()` returns the field as the last
219
- build measured it, `{ bounds, rectangles }` in canvas-local CSS pixels, and each
220
- control handle answers `getRoute()` with the remaining waypoints and
221
- `getTarget()` with the current goal or `null`. All three return copies.
238
+ - `beefwifecanvasready` when the mount succeeds.
239
+ - `beefwifecanvaserror` when the mount or a later render fails. The error is
240
+ `event.detail.error`.
222
241
 
223
- `getStats()` reports the last whole second: `actors`, `steps` and `draws` a
224
- second, and `stepMs` and `drawMs` for the mean cost of one simulation step and
225
- one draw. Those milliseconds are this thread's work alone. A draw returns once
226
- the frame is submitted and the GPU renders it afterwards, so what a resolution
227
- costs to rasterise is not in `drawMs`.
242
+ A failure stops the canvas and cleans it up, leaving it in `error`. After the
243
+ event, `get(canvas)` returns `null` and the canvas can be mounted again.
228
244
 
229
- ```js
230
- const { steps, drawMs } = runtime.getStats();
231
- for (const actor of runtime.getActors())
232
- overlay.plot(actor.getRoute(), actor.getTarget());
245
+ `destroy()` removes the scene and listeners and leaves the canvas element in
246
+ place. While the element stays in the page, its renderer is kept, so mounting
247
+ it again reuses the GPU context. When a destroyed canvas leaves the page, its
248
+ renderer is released and that element cannot be mounted again; mount a new
249
+ canvas. A canvas removed without `destroy()` keeps its renderer and GPU context
250
+ for the life of the page, so call `destroy()` when you remove one.
251
+
252
+ ## Avoidance and occlusion
253
+
254
+ Avoidance and occlusion are separate. `avoid` changes where creatures walk. To
255
+ make them walk behind an element, stack that element above the canvas. An
256
+ element can be avoided, stacked above, both, or neither.
257
+
258
+ Avoided elements are measured again when the window resizes, when an avoided
259
+ element or the canvas resizes, and when a window scroll moves an avoided
260
+ element against the canvas. Call `refreshTerrain()` after a change the runtime
261
+ cannot see: a CSS transform, or a scroll inside a nested scroll container.
262
+
263
+ ## Performance
264
+
265
+ `drawFps` and `resolutionScale` are the main controls. Each lowers cost
266
+ without changing how creatures move.
267
+
268
+ `simulationFps` saves little. The simulation runs fixed steps, so a simulated
269
+ second costs the same however updates are grouped; at 400 creatures, 10 against
270
+ 60 saves 8% of the simulation cost. Fewer updates also steer more coarsely, so
271
+ creatures take different paths. To spend less on simulation, lower
272
+ `timeScale`: creatures move slower.
273
+
274
+ A page that cannot keep up with `simulationFps` plays slower but follows the
275
+ same paths.
276
+
277
+ `getStats()` reports the last whole second: `actors`, `steps` and `draws` per
278
+ second, and `stepMs` and `drawMs`, the mean cost of one simulation update and
279
+ one draw. Both times are main-thread work only; GPU time is not included.
280
+
281
+ ## Development
282
+
283
+ `beefwife-canvas.js`, `beefwife-canvas.min.js`, and `beefwife-canvas.mjs` are
284
+ generated from `src/` by `npm run build`; do not edit them directly. From the
285
+ repository root:
286
+
287
+ ```sh
288
+ bb canvas # public API
289
+ bb canvas-runtime # routing and runtime
290
+ bb canvas-package # build, then check the packed package
233
291
  ```
234
292
 
235
- Simulation and routing options are `timeScale`, `wanderDelay`, `edgeMargin`,
236
- `obstaclePadding`, `waypointRadius`, `arrivalRadius`, `throttleEase`,
237
- `stuckReplanSeconds`, and `escapeReplanSeconds`. `waypointRadius` controls how
238
- closely an intermediate corner must be followed, while `arrivalRadius`
239
- controls final-target satisfaction; both default to 10 CSS pixels.
240
- `wanderDelay` defaults to 4 seconds and may be zero.
241
- `avoid` defaults to `.beefwife-avoid`, `edgeMargin` defaults to 25, and
242
- `obstaclePadding` defaults to zero CSS pixels. Terrain is re-measured when the
243
- window resizes, an obstacle or the canvas resizes, or a window scroll moves an
244
- obstacle relative to the canvas, as with a fixed canvas or a sticky obstacle.
245
- Call `refreshTerrain()` after transform-only obstacle movement, or a scroll
246
- inside a nested scroll container, because the runtime cannot see either.
247
- `pauseHidden` and `pauseOffscreen` default to true.
248
-
249
- Avoidance and visual occlusion are independent. `avoid` changes routing. To
250
- make beefwives appear to walk behind a widget, place that DOM element above the
251
- canvas in normal stacking order. A widget may be an avoid element, an
252
- occluder, both, or neither; no Pixi mask is needed for ordinary DOM occlusion.
293
+ [`examples/beefwife-canvas`](https://github.com/kvmet/beefwife/tree/main/examples/beefwife-canvas)
294
+ is a page with every feature wired up.
295
+
296
+ BeefwifeCanvas is released under the
297
+ [Mozilla Public License 2.0](https://github.com/kvmet/beefwife/blob/main/beefwife-canvas/LICENSE).
@@ -1,4 +1,4 @@
1
- /*! Beefwife Canvas v0.1.0. Generated from beefwife-canvas/src; do not edit.
1
+ /*! Beefwife Canvas v0.1.1. Generated from beefwife-canvas/src; do not edit.
2
2
  (c) Kristen Metcalfe, MPL-2.0.
3
3
  Bundles @kvmet/beefwife (MPL-2.0) and @kvmet/terrain (MIT, (c) Kristen Metcalfe). */
4
4
  var BeefwifeCanvas = (function(pixi_js) {
@@ -1,4 +1,4 @@
1
- /*! Beefwife Canvas v0.1.0. Generated from beefwife-canvas/src; do not edit.
1
+ /*! Beefwife Canvas v0.1.1. Generated from beefwife-canvas/src; do not edit.
2
2
  (c) Kristen Metcalfe, MPL-2.0.
3
3
  Bundles @kvmet/beefwife (MPL-2.0) and @kvmet/terrain (MIT, (c) Kristen Metcalfe). */
4
4
  var BeefwifeCanvas=(function(e){var t=Object.create,n=Object.defineProperty,__name=(e,t)=>n(e,`name`,{value:t,configurable:!0}),r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getPrototypeOf,o=Object.prototype.hasOwnProperty,__exportAll=(e,t)=>{let r=Object.setPrototypeOf({},null);for(var i in e)n(r,i,{get:e[i],enumerable:!0});return t&1||n(r,Symbol.toStringTag,{value:`Module`}),t&2?r:Object.preventExtensions(r)},__copyProps=(e,t,a,s)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var c=i(t),l=0,u=c.length,d;l<u;l++)d=c[l],!o.call(e,d)&&d!==a&&n(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(s=r(t,d))||s.enumerable});return e},__toESM=(e,r,i)=>(i=e==null?{}:t(a(e)),__copyProps(r||!e||!e.__esModule||!o.call(e,`default`)?n(i,`default`,{value:e,enumerable:!0}):i,e));e=__toESM(e,1);var s=e,c=typeof s?.Container==`function`,l=1e9,u=1e-6,d=1e6,f=1e6,p=1/60,m=[`head`,`trunk`,`tail`],h=/^[A-Za-z][A-Za-z0-9_-]{0,63}$/,g=/^[a-z0-9][a-z0-9-]{0,63}$/,_=Object.freeze({name:64,definitions:256,chunks:256,placements:512,instances:512,path:65536,pathTotal:1048576}),number=(e,t,n=!1)=>({kind:`number`,min:e,max:t,integer:n}),px=(e,t)=>({...number(e,t),length:1}),perPx=(e,t)=>({...number(e,t),length:-1}),string=(e,t,n=null)=>({kind:`string`,minLength:e,maxLength:t,pattern:n}),choice=(...e)=>({kind:`choice`,values:e}),literal=e=>({kind:`literal`,value:e}),nullable=e=>({kind:`nullable`,item:e}),object=e=>({kind:`object`,fields:e}),array=(e,t)=>({kind:`array`,item:e,maxLength:t}),record=(e,t=0)=>({kind:`record`,item:e,minLength:t,maxLength:_.definitions}),v=string(1,64,h),y=number(0,1),b=px(.001,1e3),x=number(.001,100),S=px(1e-6,1e4),C=px(-1e4,1e4),nonBlank=e=>({...e,blank:!1}),w=nullable(nonBlank(string(1,256))),T=object({velocityRetention:y,jointCorrection:y,linkCorrection:number(.001,1),grip:object({forward:y,backward:y,lateral:y})}),E=object({path:nonBlank(string(1,_.path))}),D=object({fill:w,stroke:nullable(object({colour:nonBlank(string(1,256)),width:px(0,1e3)}))}),O=object({start:px(0,1e3),end:px(0,1e3)}),sectionOf=e=>object({chunks:number(e,_.chunks,!0),spacing:px(1e-6,1e3),material:v,motionScale:object({bend:number(0,4),thrust:number(0,4),gather:number(0,4),contact:number(0,4)}),profile:object({ribbonWidth:O,plateScale:object({start:number(0,100),end:number(0,100)})})}),k=object({section:nullable(choice(...m)),from:choice(`head`,`tail`),offset:number(0,_.chunks-1,!0)}),A=object({count:nullable(number(1,_.chunks,!0)),step:number(1,_.chunks,!0)}),j=object({id:v,shape:v,paint:v,at:k,repeat:A,scale:b}),M=object({id:v,shape:v,paint:v,at:k,repeat:A,side:choice(`left`,`right`,`both`),layer:choice(`under`,`over`),offset:object({forward:C,outward:C}),angleDegrees:number(-180,180),scale:b,source:y,react:number(-4,4),recover:number(0,1e3),wobble:y}),N=object({schemaVersion:literal(1),name:string(1,_.name,g),definitions:object({materials:record(T,1),shapes:record(E,1),paints:record(D,1)}),gait:object({cyclesPerSecond:number(0,15),phaseLagRadiansPerPixel:perPx(-1e3,1e3),bend:object({amplitude:number(0,10),harmonic:number(1,8,!0)}),thrust:object({acceleration:px(0,1e6),harmonic:number(1,8,!0),phaseOffset:number(-Math.PI,Math.PI),dutyCycle:number(.01,1)}),gather:object({amplitude:number(0,.95),harmonic:number(1,8,!0),phaseOffset:number(-Math.PI,Math.PI)}),contact:object({amplitude:y,harmonic:number(1,8,!0),phaseOffset:number(-Math.PI,Math.PI),dutyCycle:number(.01,1)})}),chain:object({physics:object({autoLift:object({amount:y,share:y,rate:number(0,1e3)}),steering:object({gain:number(0,100),limit:number(0,Math.PI),rate:number(0,1e3)})}),breathing:y,sections:object({head:sectionOf(1),trunk:sectionOf(1),tail:sectionOf(0)}),skin:object({loadScale:number(-1,10),ribbon:object({paint:v}),plates:array(j,_.chunks),ornaments:array(M,_.placements)})}),legs:object({section:choice(...m),pairs:number(0,128,!0),reach:S,spread:number(0,4),lead:y,fold:y,jointBend:number(-1,1),jointLean:number(-1,1),jointLeanCenter:number(-1,1),sidePhase:y,liftThreshold:y,swingCycles:number(.001,60),swingArc:number(0,4),jitter:y,skin:object({limbPaint:v,limbWidth:px(0,1e3),foot:object({shape:v,paint:v,scale:b,plantedScale:x})})})}),ee=/* @__PURE__ */ __exportAll({ID_PATTERN:()=>h,LIMITS:()=>_,NAME_PATTERN:()=>g,VERSION:()=>1,bounds:()=>bounds,parse:()=>parse,read:()=>read,scale:()=>scale,stringify:()=>stringify}),fail=(e,t)=>{throw Error(`${e}: ${t}`)},te=/* @__PURE__ */ __name((e,t)=>{(!e||typeof e!=`object`||Array.isArray(e))&&fail(t,`must be an object`);let n=Object.getPrototypeOf(e);n!==Object.prototype&&n!==null&&fail(t,`must be a plain JSON object`)},`plainObject`),ownKeys=(e,t)=>{let n=Reflect.ownKeys(e);return n.some(e=>typeof e!=`string`)&&fail(t,`must not contain symbol keys`),n.forEach(n=>{let r=Object.getOwnPropertyDescriptor(e,n);r.enumerable&&`value`in r||fail(`${t}.${n}`,`must be an enumerable data property`)}),n},readNode=(e,t,n,r)=>{if(e.kind===`nullable`)return t===null?null:readNode(e.item,t,n,r);if(e.kind===`literal`)return t!==e.value&&fail(n,`must equal ${e.value}`),t;if(e.kind===`choice`)return e.values.includes(t)||fail(n,`must be one of ${e.values.join(`, `)}`),t;if(e.kind===`number`)return typeof t==`number`&&Number.isFinite(t)||fail(n,`must be a finite number`),e.integer&&!Number.isInteger(t)&&fail(n,`must be an integer`),(t<e.min||t>e.max)&&fail(n,`must be between ${e.min} and ${e.max}`),Object.is(t,-0)?0:t;if(e.kind===`string`)return typeof t!=`string`&&fail(n,`must be a string`),(t.length<e.minLength||t.length>e.maxLength)&&fail(n,`must contain ${e.minLength} to ${e.maxLength} characters`),e.pattern&&!e.pattern.test(t)&&fail(n,`contains characters that are not allowed`),t;e.kind===`array`?Array.isArray(t)||fail(n,`must be an array`):te(t,n),r.has(t)&&fail(n,`must not contain a cycle`),r.add(t);try{if(e.kind===`array`){t.length>e.maxLength&&fail(n,`must contain at most ${e.maxLength} entries`);let i=Reflect.ownKeys(t).filter(e=>e!==`length`);i.some(e=>typeof e!=`string`)&&fail(n,`must not contain symbol keys`),i.forEach(e=>{let r=Object.getOwnPropertyDescriptor(t,e);r.enumerable&&`value`in r||fail(`${n}.${String(e)}`,`must be an enumerable data property`)});for(let e=0;e<t.length;e++)Object.hasOwn(t,e)||fail(`${n}[${e}]`,`is missing`);return i.some(e=>!/^(0|[1-9][0-9]*)$/.test(e))&&fail(n,`must not contain named properties`),t.map((t,i)=>readNode(e.item,t,`${n}[${i}]`,r))}let i=ownKeys(t,n);if(e.kind===`record`){(i.length<e.minLength||i.length>e.maxLength)&&fail(n,`must contain ${e.minLength} to ${e.maxLength} definitions`);let a={};return i.sort().forEach(i=>{h.test(i)||fail(`${n}.${i}`,`has an invalid id`),a[i]=readNode(e.item,t[i],`${n}.${i}`,r)}),a}let a=Object.keys(e.fields);i.forEach(t=>{Object.hasOwn(e.fields,t)||fail(`${n}.${t}`,`is unknown`)});let o={};return a.forEach(i=>{Object.hasOwn(t,i)||fail(`${n}.${i}`,`is required`),o[i]=readNode(e.fields[i],t[i],`${n}.${i}`,r)}),o}finally{r.delete(t)}},reference=(e,t,n)=>{Object.hasOwn(e,t)||fail(n,`references missing "${t}"`)},resolvedChunks=(e,t,n)=>{let r=e.chain.sections,i={head:0,trunk:r.head.chunks,tail:r.head.chunks+r.trunk.chunks},a=t.at.section,o=a?r[a].chunks:m.reduce((e,t)=>e+r[t].chunks,0);o||fail(`${n}.at`,`cannot address an empty scope`);let s=t.at.offset;s>=o&&fail(`${n}.at.offset`,`falls outside its scope`);let c=t.at.from===`head`?1:-1,l=c>0?s:o-1-s,u=Math.floor((o-1-s)/t.repeat.step)+1,d=t.repeat.count??u;d>u&&fail(`${n}.repeat.count`,`runs outside its scope`);let f=a?i[a]:0;return Array.from({length:d},(e,n)=>f+l+c*n*t.repeat.step)},validate=e=>{let{definitions:t,chain:n,gait:r,legs:i}=e,a=n.sections,o=m.reduce((e,t)=>e+a[t].chunks,0);(o<2||o>_.chunks)&&fail(`$.chain.sections`,`must contain 2 to ${_.chunks} chunks`),m.forEach(e=>reference(t.materials,a[e].material,`$.chain.sections.${e}.material`)),m.forEach(t=>{let n=a[t].motionScale;e.gait.gather.amplitude*n.gather>=1&&fail(`$.chain.sections.${t}.motionScale.gather`,`makes the gathered link length zero or negative`),e.gait.contact.amplitude*n.contact>1&&fail(`$.chain.sections.${t}.motionScale.contact`,`makes ground contact negative`)}),[[t.paints,n.skin.ribbon.paint,`$.chain.skin.ribbon.paint`],[t.paints,i.skin.limbPaint,`$.legs.skin.limbPaint`],[t.shapes,i.skin.foot.shape,`$.legs.skin.foot.shape`],[t.paints,i.skin.foot.paint,`$.legs.skin.foot.paint`]].forEach(e=>reference(...e));let s=/* @__PURE__ */ new Set,c=/* @__PURE__ */ new Map;n.skin.plates.forEach((n,r)=>{let i=`$.chain.skin.plates[${r}]`;s.has(n.id)&&fail(`${i}.id`,`must be unique`),s.add(n.id),reference(t.shapes,n.shape,`${i}.shape`),reference(t.paints,n.paint,`${i}.paint`),resolvedChunks(e,n,i).forEach(e=>{c.has(e)&&fail(`${i}.at`,`overlaps plate "${c.get(e)}" at chunk ${e}`),c.set(e,n.id)})});let l=0;n.skin.ornaments.forEach((n,r)=>{let i=`$.chain.skin.ornaments[${r}]`;s.has(n.id)&&fail(`${i}.id`,`must be unique`),s.add(n.id),reference(t.shapes,n.shape,`${i}.shape`),reference(t.paints,n.paint,`${i}.paint`),l+=resolvedChunks(e,n,i).length*(n.side===`both`?2:1),l>_.instances&&fail(`$.chain.skin.ornaments`,`must expand to at most ${_.instances} instances`)}),[`bend`,`thrust`,`gather`,`contact`].forEach(e=>{r.cyclesPerSecond*r[e].harmonic>15&&fail(`$.gait.${e}.harmonic`,`times cyclesPerSecond must be at most 15`)}),i.pairs&&!a[i.section].chunks&&fail(`$.legs.section`,`cannot attach legs to an empty section`),Object.entries(t.paints).forEach(([e,t])=>{t.fill!==null&&!t.fill.trim()&&fail(`$.definitions.paints.${e}.fill`,`must not be blank`),t.stroke!==null&&!t.stroke.colour.trim()&&fail(`$.definitions.paints.${e}.stroke.colour`,`must not be blank`),t.fill!==null||t.stroke!==null&&t.stroke.width||fail(`$.definitions.paints.${e}`,`must draw a fill or visible stroke`)});let u=0;return Object.entries(t.shapes).forEach(([e,t])=>{t.path.trim()||fail(`$.definitions.shapes.${e}.path`,`must not be blank`),u+=t.path.length}),u>_.pathTotal&&fail(`$.definitions.shapes`,`paths must total at most ${_.pathTotal} characters`),e},read=e=>validate(readNode(N,e,`$`,/* @__PURE__ */ new WeakSet)),resolve=e=>{let t=N;for(let n of e){for(;t.kind===`nullable`;)t=t.item;if(t.kind===`object`&&Object.hasOwn(t.fields,n))t=t.fields[n];else if(t.kind===`record`&&n===`*`)t=t.item;else if(t.kind===`array`&&n===`[]`)t=t.item;else return null}let n=t.kind===`nullable`;return{node:n?t.item:t,nullable:n}},shapeOf=e=>e.kind===`number`?{kind:`number`,min:e.min,max:e.max,integer:e.integer}:e.kind===`string`?{kind:`string`,minLength:e.minLength,maxLength:e.maxLength,pattern:e.pattern&&new RegExp(e.pattern.source,e.pattern.flags),blankAllowed:e.blank!==!1}:e.kind===`choice`?{kind:`choice`,values:Object.freeze([...e.values])}:e.kind===`literal`?{kind:`literal`,value:e.value}:e.kind===`object`?{kind:`object`,fields:Object.freeze(Object.keys(e.fields))}:e.kind===`record`?{kind:`record`,minEntries:e.minLength,maxEntries:e.maxLength,keyPattern:new RegExp(h.source,h.flags)}:{kind:`array`,maxLength:e.maxLength},bounds=e=>{typeof e==`string`&&e||fail(`$`,`bounds path must be a non-empty string`);let t=resolve(e.replace(/\[\]/g,`.[]`).split(`.`));return t||fail(`$.${e}`,`is not a field in this schema`),Object.freeze({...shapeOf(t.node),nullable:t.nullable})},scaleNode=(e,t,n)=>{if(e.kind===`nullable`)return t===null?null:scaleNode(e.item,t,n);if(e.kind===`number`)return e.length?t*n**e.length:t;if(e.kind===`object`){let r={};return Object.keys(e.fields).forEach(i=>{r[i]=scaleNode(e.fields[i],t[i],n)}),r}if(e.kind===`record`){let r={};return Object.keys(t).forEach(i=>{r[i]=scaleNode(e.item,t[i],n)}),r}return e.kind===`array`?t.map(t=>scaleNode(e.item,t,n)):t},scale=(e,t)=>{(typeof t!=`number`||!Number.isFinite(t)||t<=0)&&fail(`$`,`scale factor must be a finite number greater than 0`);let n=read(e),r=scaleNode(N,n,t),i=/* @__PURE__ */ new Set([n.chain.skin.ribbon.paint,n.legs.skin.limbPaint]),a=[r.legs.skin.foot,...r.chain.skin.plates,...r.chain.skin.ornaments],o=new Set(a.map(({paint:e})=>e));for(let e of o){let o=n.definitions.paints[e];if(!o.stroke||o.stroke.width===0||t===1)continue;let s=e;if(i.has(e)){let t=1;do s=`shape-paint-${t++}`;while(Object.hasOwn(r.definitions.paints,s));for(let t of a)t.paint===e&&(t.paint=s)}r.definitions.paints[s]=o}return read(r)},parse=e=>{typeof e!=`string`&&fail(`$`,`JSON input must be a string`);let t;try{t=JSON.parse(e)}catch(e){fail(`$`,`invalid JSON: ${e.message}`)}return read(t)},stringify=(e,t=2)=>((!Number.isInteger(t)||t<0||t>10)&&fail(`$`,`indentation must be an integer from 0 to 10`),JSON.stringify(read(e),null,t)),ne=[`head`,`trunk`,`tail`],re=.1,ie=.15,P=.4,F=2*re*2*Math.PI*P,ae=14,oe=.01,freeze=e=>!e||typeof e!=`object`||Object.isFrozen(e)?e:(Object.values(e).forEach(freeze),Object.freeze(e)),I=/* @__PURE__ */ __name((e,t,n)=>e+(t-e)*n,`lerp`),se=/* @__PURE__ */ __name((e,t,n)=>Math.max(t,Math.min(n,e)),`clamp`),profileAt=(e,t,n)=>n===1?(e.start+e.end)/2:I(e.start,e.end,t/(n-1)),spatialAngle=(e,t,n)=>t.phaseOffset-t.harmonic*n*e.phaseLagRadiansPerPixel,breathingRateFor=e=>se(ie*Math.sqrt(16/e),.1,P),placementChunks=(e,t,n)=>{let r=e.at.section,i=r?t[r]:{start:0,count:n},a=e.at.from===`head`?1:-1,o=a>0?e.at.offset:i.count-1-e.at.offset,s=Math.floor((i.count-1-e.at.offset)/e.repeat.step)+1,c=e.repeat.count??s;return Array.from({length:c},(t,n)=>i.start+o+a*n*e.repeat.step)},normalizePaints=e=>{let t={};return Object.entries(e).forEach(([e,n])=>{t[e]={fill:n.fill,stroke:n.stroke?n.stroke.colour:null,strokeWidth:n.stroke?n.stroke.width:0}}),t},compile=e=>{let t=read(e),n={...t.gait,bend:{...t.gait.bend,phaseOffset:0}},r=t.chain.sections,i={},a=[];ne.forEach(e=>{let n=r[e],o=a.length,s=t.definitions.materials[n.material];for(let t=0;t<n.chunks;t++)a.push({index:a.length,section:e,localIndex:t,restDistance:0,materialId:n.material,material:s,motionScale:n.motionScale,bendScale:0,ribbonWidth:profileAt(n.profile.ribbonWidth,t,n.chunks),plateScale:profileAt(n.profile.plateScale,t,n.chunks)});i[e]={name:e,start:o,end:a.length,count:n.chunks,spacing:n.spacing,materialId:n.material,material:s,motionScale:n.motionScale,profile:n.profile}});let o={strain:t.chain.breathing*re,cyclesPerSecond:breathingRateFor(i.trunk.count)},s=[],c=0;for(let e=0;e<a.length-1;e++){let t=a[e],n=a[e+1],r=i[t.section],l=i[n.section],u=t.section===n.section?r.spacing:(r.spacing+l.spacing)/2,d=(t.material.linkCorrection+n.material.linkCorrection)/2;c+=u,n.restDistance=c,s.push({index:e,from:e,to:e+1,restLength:u,phaseDistance:(t.restDistance+n.restDistance)/2,linkCorrection:d,gatherScale:(t.motionScale.gather+n.motionScale.gather)/2,breathingScale:t.section===`trunk`&&n.section===`trunk`?o.strain:0})}for(let e=1;e<a.length-1;e++)a[e].bendScale=(s[e-1].restLength+s[e].restLength)/2/i.trunk.spacing;a.forEach(e=>{let t=spatialAngle(n,n.bend,e.restDistance);e.bendPhaseSine=Math.sin(t),e.bendPhaseCosine=Math.cos(t)}),s.forEach(e=>{let t=spatialAngle(n,n.gather,e.phaseDistance);e.gatherPhaseSine=Math.sin(t),e.gatherPhaseCosine=Math.cos(t)});let l=t.definitions.shapes,u=normalizePaints(t.definitions.paints),d=t.chain.skin.plates.flatMap(e=>placementChunks(e,i,a.length).map(t=>({id:e.id,chunk:t,shape:l[e.shape],paint:u[e.paint],scale:e.scale}))),f=t.chain.skin.ornaments.flatMap(e=>{let t=e.side===`both`?[`left`,`right`]:[e.side];return placementChunks(e,i,a.length).flatMap(n=>t.map(t=>{let r=t===`left`?-1:1,i=e.angleDegrees*Math.PI*r/180;return{id:e.id,chunk:n,side:t,sideSign:r,layer:e.layer,shape:l[e.shape],paint:u[e.paint],offset:e.offset,angleDegrees:e.angleDegrees,angleCosine:Math.cos(i),angleSine:Math.sin(i),scale:e.scale,waveGain:e.react*(1-e.source),physGain:e.react*e.source,recover:e.recover,wobble:e.wobble}}))});return freeze({descriptor:t,sections:i,chunks:a,links:s,restLength:c,gait:n,paints:u,physics:t.chain.physics,breathing:o,skin:{lateralRate:ae*i.trunk.spacing,loadScale:t.chain.skin.loadScale,hasRibbon:a.some(e=>e.ribbonWidth>0),ribbonPaintId:t.chain.skin.ribbon.paint,ribbonPaint:u[t.chain.skin.ribbon.paint],plates:d,platesTailFirst:[...d].sort((e,t)=>t.chunk-e.chunk),ornaments:f},legs:{...t.legs,spread:t.legs.spread*t.legs.reach,swingArc:t.legs.swingArc*t.legs.reach,swingSeconds:t.legs.swingCycles/Math.max(t.gait.cyclesPerSecond,oe),start:i[t.legs.section].start,end:i[t.legs.section].end,skin:{limbPaint:u[t.legs.skin.limbPaint],limbWidth:t.legs.skin.limbWidth,foot:{shape:l[t.legs.skin.foot.shape],paint:u[t.legs.skin.foot.paint],scale:t.legs.skin.foot.scale,plantedScale:t.legs.skin.foot.plantedScale}}}})},L=Math.PI*2,positiveModulo=(e,t)=>(e%t+t)%t,Gait=class{constructor(e,t=0){this.gait=e,this.phase=positiveModulo(t,L)}advance(e,t){this.phase=positiveModulo(this.phase+L*this.gait.cyclesPerSecond*t*e,L)}_phaseAt(e,t,n=0){return t.harmonic*(this.phase-e*this.gait.phaseLagRadiansPerPixel)+t.phaseOffset+n}_pulseAt(e,t,n=0){let r=positiveModulo(this._phaseAt(e,t,n),L)/L;return r>=t.dutyCycle?0:Math.sin(Math.PI*r/t.dutyCycle)}bendAt(e,t,n){let r=this.gait.bend;return r.amplitude*n*t*Math.sin(this._phaseAt(e,r))}thrustAt(e,t,n){let r=this.gait.thrust;return r.acceleration*n*t*this._pulseAt(e,r)}restAt(e,t,n){let r=this.gait.gather;return 1+r.amplitude*n*t*Math.cos(this._phaseAt(e,r))}contactAt(e,t,n,r=0){let i=this.gait.contact;return 1-i.amplitude*n*t*this._pulseAt(e,i,r)}},ce=60,le=480,shareAtRate=(e,t,n)=>t===n?e:e>0?e>=1?1:1-(1-e)**(n/t):0,ChainTables=class{constructor(e,t,n,r){let i=e.chunks.length,a=e.links.length;this.retention=new Float64Array(i),this.gripForward=new Float64Array(i),this.gripBackward=new Float64Array(i),this.gripLateral=new Float64Array(i),this.motionThrust=new Float64Array(i),this.motionContact=new Float64Array(i),this.motionBend=new Float64Array(i),this.bendScale=new Float64Array(i),this.bendPhaseSine=new Float64Array(i),this.bendPhaseCosine=new Float64Array(i),this.jointCorrectionHalf=new Float64Array(i),this.phaseLag=new Float64Array(i),this.linkRestLength=new Float64Array(a),this.linkCorrectionHalf=new Float64Array(a),this.gatherScale=new Float64Array(a),this.gatherPhaseSine=new Float64Array(a),this.gatherPhaseCosine=new Float64Array(a),this.linkBreathes=new Uint8Array(a),this.refresh(e,t,n,r)}refresh(e,t,n,r){let i=t.phaseLagRadiansPerPixel,a=1/n,o=r/n,perSubstep=e=>shareAtRate(e,a,ce);for(let t=0;t<e.chunks.length;t++){let r=e.chunks[t],a=r.material.grip;this.retention[t]=r.material.velocityRetention**+n,this.gripForward[t]=perSubstep(a.forward),this.gripBackward[t]=perSubstep(a.backward),this.gripLateral[t]=perSubstep(a.lateral),this.motionThrust[t]=r.motionScale.thrust,this.motionContact[t]=r.motionScale.contact,this.motionBend[t]=r.motionScale.bend,this.bendScale[t]=r.bendScale,this.bendPhaseSine[t]=r.bendPhaseSine,this.bendPhaseCosine[t]=r.bendPhaseCosine,this.jointCorrectionHalf[t]=perSubstep(r.material.jointCorrection)*.5,this.phaseLag[t]=r.restDistance*i}for(let t=0;t<e.links.length;t++){let n=e.links[t];this.linkRestLength[t]=n.restLength,this.linkCorrectionHalf[t]=shareAtRate(n.linkCorrection,o,le)*.5,this.gatherScale[t]=n.gatherScale,this.gatherPhaseSine[t]=n.gatherPhaseSine,this.gatherPhaseCosine[t]=n.gatherPhaseCosine,this.linkBreathes[t]=+!!n.breathingScale}}},ChainState=class{constructor(e){this.count=e,this.x=new Float64Array(e),this.y=new Float64Array(e),this.px=new Float64Array(e),this.py=new Float64Array(e),this.dx=new Float64Array(e),this.dy=new Float64Array(e),this.idle=new Float64Array(e),this.gain=new Float64Array(e),this.gaitContact=new Float64Array(e),this.contact=new Float64Array(e),this.correction={x:0,y:0},this.dx.fill(1),this.gaitContact.fill(1),this.contact.fill(1)}translate(e){let{x:t,y:n,px:r,py:i,count:a}=this;for(let o=0;o<a;o++)t[o]+=e.x,n[o]+=e.y,r[o]+=e.x,i[o]+=e.y}fitsTranslation(e,t){let{x:n,y:r,px:i,py:a,count:o}=this,within=(e,n)=>{let r=e+n;return Number.isFinite(r)&&Math.abs(r)<=t};for(let t=0;t<o;t++)if(!within(n[t],e.x)||!within(r[t],e.y)||!within(i[t],e.x)||!within(a[t],e.y))return!1;return!0}worldCorrection(e){let{x:t,y:n,px:r,py:i,count:a}=this,o=1/0,s=-1/0,c=1/0,l=-1/0;for(let e=0;e<a;e++)o=Math.min(o,t[e],r[e]),s=Math.max(s,t[e],r[e]),c=Math.min(c,n[e],i[e]),l=Math.max(l,n[e],i[e]);let correction=(t,n)=>n>e?e-n:t<-e?-e-t:0;return this.correction.x=correction(o,s),this.correction.y=correction(c,l),this.correction}},nameOf=e=>`${e.section}:${e.localIndex}`,R=[`x`,`y`,`px`,`py`,`dx`,`dy`,`idle`,`gain`],carryChunks=(e,t,n,r)=>{let i=/* @__PURE__ */ new Map;r.chunks.forEach((e,t)=>i.set(nameOf(e),t));let a=t.chunks.map((t,r)=>{let a=i.get(nameOf(t));if(a===void 0)return!1;for(let t of R)e[t][r]=n[t][a];return!0}),{x:o,y:s,px:c,py:l,dx:u,dy:d,idle:f,gain:p,count:m}=e;for(let e=0;e<m;e++){if(a[e])continue;let n=e-1;for(;n>=0&&!a[n];)n--;let r=e+1;for(;r<m&&!a[r];)r++;if(n>=0&&r<m){let t=(e-n)/(r-n);o[e]=o[n]+(o[r]-o[n])*t,s[e]=s[n]+(s[r]-s[n])*t,c[e]=c[n]+(c[r]-c[n])*t,l[e]=l[n]+(l[r]-l[n])*t,u[e]=u[n],d[e]=d[n]}else{let i=n>=0?n:r,a=n>=0?-1:1,f=t.links[n>=0?e-1:e],p=(f?f.restLength:0)*Math.abs(e-i);o[e]=o[i]+u[i]*a*p,s[e]=s[i]+d[i]*a*p,c[e]=o[e],l[e]=s[e],u[e]=u[i],d[e]=d[i]}f[e]=0,p[e]=0}},Bend=class{constructor(e){this.targets=new Float64Array(e),this.wantedX=new Float64Array(e),this.wantedY=new Float64Array(e),this.spanX=new Float64Array(e),this.spanY=new Float64Array(e)}update(e,t,n,r,i,a){let o=e.gait.bend,s=o.harmonic*t.phase,c=Math.sin(s),l=Math.cos(s),u=o.amplitude,d=a*i,{motionBend:f,bendPhaseSine:p,bendPhaseCosine:m,bendScale:h}=n,g=this.targets,_=this.wantedX,v=this.wantedY,y=g.length-1,b=0;_[0]=0,v[0]=0;for(let e=0;e<y;e++){if(e>0){let t=u*f[e]*i*(c*m[e]+l*p[e]);g[e]=(t+d)*h[e],b+=g[e]}_[e+1]=_[e]+Math.cos(b)*r[e],v[e+1]=v[e]+Math.sin(b)*r[e]}}relax(e,t){for(let n=Math.max(1,Math.min(2,e.count>>2));n>=1;n>>=1)this.relaxSpan(e,t,n)}relaxSpan(e,t,n){let r=e.count,i=e.x,a=e.y,o=this.wantedX,s=this.wantedY,c=this.spanX,l=this.spanY;for(let e=n;e+n<r;e+=n){let r=e-n,u=e+n,d=i[e],f=a[e],p=d-i[r],m=f-a[r],h=i[u]-d,g=a[u]-f,_=Math.atan2(p*g-m*h,p*h+m*g),v=o[e]-o[r],y=s[e]-s[r],b=o[u]-o[e],x=s[u]-s[e],S=(Math.atan2(v*x-y*b,v*b+y*x)-_)*t[e],C=Math.cos(S),w=Math.sin(S);for(let t=r;t<=u;t++){let n=i[t]-d,r=a[t]-f,o=t<e?-w:w;c[t]=d+n*C-r*o,l[t]=f+n*o+r*C}let T=u-r+1,E=0,D=0,O=0,k=0;for(let e=r;e<=u;e++)E+=c[e]-i[e],D+=l[e]-a[e],O+=i[e],k+=a[e];E/=T,D/=T,O/=T,k/=T;let A=0,j=0;for(let e=r;e<=u;e++){let t=i[e]-O,n=a[e]-k;A+=t*(l[e]-a[e]-D)-n*(c[e]-i[e]-E),j+=t*t+n*n}let M=j>1e-12?A/j:0;for(let e=r;e<=u;e++){let t=i[e]-O,n=a[e]-k;i[e]=c[e]-E+M*n,a[e]=l[e]-D-M*t}}}response(e,t=[]){let n=this.targets,{x:r,y:i,count:a}=e;t.length=0;for(let e=1;e<a-1;e++){let a=r[e]-r[e-1],o=i[e]-i[e-1],s=r[e+1]-r[e],c=i[e+1]-i[e];t.push({joint:e,commanded:n[e],delivered:Math.atan2(a*c-o*s,a*s+o*c)})}return t}},z=Math.PI*2,B=8,V=1.5,ue=/* @__PURE__ */ __name((e,t)=>Math.sqrt(e*e+t*t),`magnitude`),compareGain=(e,t,n)=>e[t]-e[n]||t-n,selectLowest=(e,t,n)=>{if(n<=0||n>=e.length)return;let r=n-1,i=0,a=e.length-1;for(;i<a;){let n=e[i+a>>1],o=i,s=a;for(;o<=s;){for(;o<=a&&compareGain(t,e[o],n)<0;)o++;for(;s>=i&&compareGain(t,n,e[s])<0;)s--;if(o<=s){let t=e[o];e[o++]=e[s],e[s--]=t}}if(r<=s)a=s;else if(r>=o)i=o;else return}},Body=class{constructor(e,t,n=t.phase){this.model=e,this.gait=t,this.breathingPhase=n,this.breathingScale=0,this.accumulator=0,this.axis={x:1,y:0},this.steeringBias=0,this.chain=new ChainState(e.chunks.length),this.linkTargets=new Float64Array(e.links.length),this.bend=new Bend(e.chunks.length),this.breathingShiftX=new Float64Array(e.chunks.length),this.breathingShiftY=new Float64Array(e.chunks.length),this.liftOrder=e.chunks.map((e,t)=>t),this.liftTargets=new Float64Array(e.chunks.length),this.tables=new ChainTables(e,e.gait,p,B)}reconfigure(e,t,n=1,r=this.breathingPhase){if(e.chunks.length!==this.chain.count)throw Error(`cannot reconfigure a different chunk count`);this.model=e,this.gait=t,this.breathingPhase=r,this.tables.refresh(e,e.gait,p,B),this.refreshContacts(n)}place(e,t){let{x:n,y:r,px:i,py:a,dx:o,dy:s,idle:c,gain:l,count:u}=this.chain;for(let d=0;d<u;d++){let u=this.model.chunks[d].restDistance;n[d]=e.x-t.x*u,r[d]=e.y-t.y*u,i[d]=n[d],a[d]=r[d],o[d]=t.x,s[d]=t.y,c[d]=0,l[d]=0}this.axis={...t},this.steeringBias=0,this.accumulator=0,this.breathingScale=0,this._breathe(this._breathingTarget());let d=e.x-n[0],f=e.y-r[0];for(let e=0;e<u;e++)n[e]+=d,r[e]+=f,i[e]+=d,a[e]+=f;this.refreshContacts(1)}adopt(e){carryChunks(this.chain,this.model,e.chain,e.model),this.axis={...e.axis},this.steeringBias=e.steeringBias,this.accumulator=e.accumulator,this.breathingScale=e.breathingScale}refreshContacts(e){let t=this.model.physics.autoLift,{idle:n,gaitContact:r,contact:i,count:a}=this.chain;for(let o=0;o<a;o++){let a=this.model.chunks[o];r[o]=this.gait.contactAt(a.restDistance,e,a.motionScale.contact),i[o]=Math.max(0,Math.min(1,r[o]*(1-t.amount*n[o]*e)))}}step(e,t,n,r){let i=this.accumulator+e,a=2**-52*Math.max(1,i),o=Math.floor((i+a)/p);this.accumulator=i;for(let e=1;e<=o;e++)this.accumulator=Math.max(0,i-e*p),this._substep(p,t,n),r&&r(p);return o>0}_substep(e,t,n){this.gait.advance(e,t),this.breathingPhase=(this.breathingPhase+z*this.model.breathing.cyclesPerSecond*e)%z,this._updateTangentsAndAxis(e);let r=F*e,i=this._breathingTarget()-this.breathingScale;this._breathe(this.breathingScale+Math.max(-r,Math.min(r,i))),this._integrate(e,t),this._updateLinkTargets(t),this.bend.update(this.model,this.gait,this.tables,this.linkTargets,t,this._steer(n,e)),this.bend.relax(this.chain,this.tables.jointCorrectionHalf);for(let e=0;e<B;e++)this._relaxLinks();this._clampLinks(),this._applyAutoLift(e,t)}_breathingTarget(){return this.model.breathing.strain*Math.sin(this.breathingPhase)}_breathe(e){let t=e-this.breathingScale;if(this.breathingScale=e,Math.abs(t)<1e-15)return;let{start:n,end:r,count:i,spacing:a}=this.model.sections.trunk,o=(i-1)/2,{x:s,y:c,px:l,py:u,dx:d,dy:f,count:p}=this.chain,m=0,h=0;for(let e=0;e<p;e++){let i=e<n?o:e>=r?-o:o-(e-n),s=e<n?n:e>=r?r-1:e,c=i*a*t,l=d[s]*c,u=f[s]*c;this.breathingShiftX[e]=l,this.breathingShiftY[e]=u,m+=l/p,h+=u/p}for(let e=0;e<p;e++){let t=this.breathingShiftX[e]-m,n=this.breathingShiftY[e]-h;s[e]+=t,c[e]+=n,l[e]+=t,u[e]+=n}}_updateTangentsAndAxis(e){let{x:t,y:n,px:r,py:i,dx:a,dy:o,count:s}=this.chain,c=s-1,l=0,u=0;for(let e=0;e<s;e++){let s=e===0?0:e-1,d=e===c?c:e+1,f=t[s]-t[d],p=n[s]-n[d],m=Math.sqrt(f*f+p*p);m>=1e-9&&(a[e]=f/m,o[e]=p/m),l+=t[e]-r[e],u+=n[e]-i[e]}let d=Math.sqrt(l*l+u*u);if(d<1e-9)return;let f=Math.min(1,e*V);this.axis.x+=(l/d-this.axis.x)*f,this.axis.y+=(u/d-this.axis.y)*f;let p=ue(this.axis.x,this.axis.y)||1;this.axis.x/=p,this.axis.y/=p}_integrate(e,t){let n=e*e,r=this.gait.gait,i=this.gait.phase,a=r.contact,o=r.thrust,s=a.harmonic,c=o.harmonic,l=a.phaseOffset,u=o.phaseOffset,d=a.dutyCycle,f=o.dutyCycle,p=a.amplitude,m=o.acceleration,h=this.model.physics.autoLift.amount,g=this.axis.x,_=this.axis.y,{retention:v,gripForward:y,gripBackward:b,gripLateral:x,motionContact:S,motionThrust:C,phaseLag:w}=this.tables,{x:T,y:E,px:D,py:O,dx:k,dy:A,idle:j,gain:M,gaitContact:N,contact:ee,count:te}=this.chain;for(let e=0;e<te;e++){let r=v[e],a=T[e],o=E[e],te=(a-D[e])*r,ne=(o-O[e])*r;D[e]=a,O[e]=o;let re=a+te,ie=o+ne,P=k[e],F=A[e],ae=re-a,oe=ie-o,I=ae*P+oe*F,se=ae*-F+oe*P,L=i-w[e],ce=positiveModulo(s*L+l,z)/z,le=ce>=d?1:1-p*S[e]*t*Math.sin(Math.PI*ce/d);N[e]=le;let R=Math.max(0,Math.min(1,le*(1-h*j[e]*t)));ee[e]=R;let B=I*(1-R*(I<0?b[e]:y[e])),V=se*(1-R*x[e]),ue=a+P*B-F*V,H=o+F*B+P*V;M[e]=-((I-B)*(P*g+F*_)+(se-V)*(-F*g+P*_));let U=positiveModulo(c*L+u,z)/z,de=U>=f?0:m*C[e]*t*Math.sin(Math.PI*U/f);T[e]=ue+P*de*n,E[e]=H+F*de*n}}_steer(e,t){let n=this.model.physics.steering,{dx:r,dy:i}=this.chain,a=Math.atan2(r[0]*e.y-i[0]*e.x,r[0]*e.x+i[0]*e.y),o=-Math.max(-n.limit,Math.min(n.limit,a*n.gain));return this.steeringBias+=(o-this.steeringBias)*Math.min(1,t*n.rate),this.steeringBias}_updateLinkTargets(e){let t=this.model.gait.gather,n=t.harmonic*this.gait.phase,r=Math.sin(n),i=Math.cos(n),a=t.amplitude,o=this.breathingScale,s=this.linkTargets,{linkRestLength:c,gatherScale:l,gatherPhaseSine:u,gatherPhaseCosine:d,linkBreathes:f}=this.tables;for(let t=0;t<s.length;t++){let n=i*d[t]-r*u[t],p=1+a*l[t]*e*n;s[t]=c[t]*p*(f[t]?1+o:1)}}_relaxLinks(){let{x:e,y:t}=this.chain,n=this.linkTargets,r=this.tables.linkCorrectionHalf,i=n.length,a=e[0],o=t[0];for(let s=0;s<i;s++){let i=e[s+1],c=t[s+1],l=i-a,u=c-o,d=Math.sqrt(l*l+u*u)||.001,f=(d-n[s])/d*r[s];e[s]=a+l*f,t[s]=o+u*f,a=i-l*f,o=c-u*f}e[i]=a,t[i]=o}_clampLinks(){let{x:e,y:t}=this.chain,n=this.linkTargets,r=n.length,i=e[0],a=t[0];for(let o=0;o<r;o++){let r=n[o]*3,s=e[o+1]-i,c=t[o+1]-a,l=Math.sqrt(s*s+c*c);if(l>r){let n=r/l;i+=s*n,a+=c*n,e[o+1]=i,t[o+1]=a}else i=e[o+1],a=t[o+1]}}_applyAutoLift(e,t){let n=this.model.physics.autoLift;if(!n.amount)return;let{idle:r,gain:i,gaitContact:a,contact:o,count:s}=this.chain,c=Math.round(n.share*s);selectLowest(this.liftOrder,i,c),this.liftTargets.fill(0);for(let e=0;e<c;e++)this.liftTargets[this.liftOrder[e]]=t;let l=Math.min(1,e*n.rate);for(let e=0;e<s;e++)r[e]+=(this.liftTargets[e]-r[e])*l,o[e]=Math.max(0,Math.min(1,a[e]*(1-n.amount*r[e]*t)))}getPose(e){let{x:t,y:n,dx:r,dy:i,count:a}=this.chain,o=t[0]-t[1],s=n[0]-n[1],c=ue(o,s);c<1e-9?(e.direction.x=r[0],e.direction.y=i[0]):(e.direction.x=o/c,e.direction.y=s/c);let l=0,u=0;for(let e=0;e<a;e++)l+=t[e]/a,u+=n[e]/a;return e.head.x=t[0],e.head.y=n[0],e.center.x=l,e.center.y=u,e}},lerp=(e,t,n)=>e+(t-e)*n,clamp=(e,t,n)=>Math.max(t,Math.min(n,e)),H=/* @__PURE__ */ __name((e,t)=>Math.sqrt(e*e+t*t),`magnitude`),limbLength=(e,t,n)=>{let r=n<=.5?lerp(.9,1.35,n*2):lerp(1.35,1.72,(n-.5)*2);return e*t*r},Legs=class{constructor(e,t,n,r){this.model=e,this.body=t,this.gait=n,this.random=r,this.legs=[],this._build()}reconfigure(e,t,n){this.model=e,this.body=t,this.gait=n;let r=this._anchors();this.legs.forEach(e=>e.anchor=r[e.pair])}_signedRandom(){return this.random()*2-1}_vary(e){return 1+e*this._signedRandom()}_anchors(){let{pairs:e,start:t,end:n}=this.model.legs;if(!e)return[];let r=this.model.chunks[t].restDistance,i=this.model.chunks[n-1].restDistance;return Array.from({length:e},(a,o)=>{let s=e===1?.5:o/(e-1),c=lerp(r,i,s),l=t;for(let e=t+1;e<n;e++)Math.abs(this.model.chunks[e].restDistance-c)<Math.abs(this.model.chunks[l].restDistance-c)&&(l=e);return l})}_build(){let e=this.model.legs;this._anchors().forEach((t,n)=>{[[`left`,-1],[`right`,1]].forEach(([r,i])=>{let a={pair:n,anchor:t,side:r,sideSign:i,character:this._signedRandom(),reachScale:this._vary(e.jitter*.28),spreadScale:this._vary(e.jitter*.3),leadScale:this._vary(e.jitter*.45),dragScale:1,swingScale:1,liftAt:e.liftThreshold,scatter:{x:0,y:0},plantSpan:0,progress:1,contactLow:!1,foot:{x:0,y:0},hold:{x:0,y:0},from:{x:0,y:0}};this._rollStep(a);let o=this._plantAt(a);Object.assign(a.foot,o),Object.assign(a.hold,o),Object.assign(a.from,o),a.plantSpan=H(o.x-this.body.chain.x[t],o.y-this.body.chain.y[t]),this.legs.push(a)})})}_rollStep(e){let t=this.model.legs.jitter;e.dragScale=this._vary(t*.4)*(1+t*.35*e.character),e.swingScale=this._vary(t*.3),e.liftAt=clamp(this.model.legs.liftThreshold+t*.2*e.character,0,1);let n=Math.max(this.model.legs.reach,this.model.legs.spread)*t*.1;e.scatter={x:n*this._signedRandom(),y:n*this._signedRandom()}}_plantAt(e){let t=this.model.legs,{x:n,y:r,dx:i,dy:a}=this.body.chain,o=e.anchor,s=t.reach*e.reachScale*(.5+t.lead*.5)*e.leadScale,c=t.spread*e.spreadScale*e.sideSign;return{x:n[o]+i[o]*s-a[o]*c,y:r[o]+a[o]*s+i[o]*c}}contactFor(e,t){let n=this.model.chunks[e.anchor],r=e.side===`right`?Math.PI*this.model.legs.sidePhase:0;return clamp(this.gait.contactAt(n.restDistance,t,n.motionScale.contact,r)*(1-this.model.physics.autoLift.amount*this.body.chain.idle[e.anchor]*t),0,1)}update(e,t){let n=this.model.legs,r=this.body.chain;for(let i=0;i<this.legs.length;i++){let a=this.legs[i],o=r.x[a.anchor],s=r.y[a.anchor],c=r.dx[a.anchor],l=r.dy[a.anchor],u=this.contactFor(a,t)<a.liftAt,d=u&&!a.contactLow;if(a.contactLow=u,a.progress<1){let t=n.swingSeconds*a.swingScale;a.progress=Math.min(1,a.progress+e/t);let r=Math.sin(Math.PI*a.progress)*n.swingArc;a.foot.x=lerp(a.from.x,a.hold.x,a.progress)-l*r*a.sideSign,a.foot.y=lerp(a.from.y,a.hold.y,a.progress)+c*r*a.sideSign;continue}Object.assign(a.foot,a.hold);let f=a.foot.x-o,p=a.foot.y-s,m=f*c+p*l,h=n.reach*a.reachScale,g=m<-(h*(.5-n.lead*.5))*a.dragScale,_=H(h,n.spread*a.spreadScale),v=H(f,p)>Math.max(_,a.plantSpan)*1.55;if(d||g||v){Object.assign(a.from,a.foot);let e=this._plantAt(a);a.hold.x=e.x+a.scatter.x,a.hold.y=e.y+a.scatter.y,a.plantSpan=H(a.hold.x-o,a.hold.y-s),a.progress=0,this._rollStep(a),a.contactLow=this.contactFor(a,t)<a.liftAt}}}translate(e){this.legs.forEach(t=>{[t.foot,t.hold,t.from].forEach(t=>{t.x+=e.x,t.y+=e.y})})}armLength(e){return limbLength(this.model.legs.reach,e.reachScale,this.model.legs.fold)}},U=Object.freeze({chunkStride:4,legStride:11,ornamentStride:6,plateStride:5}),de=13,W=Math.PI/2,magnitude=(e,t)=>Math.sqrt(e*e+t*t),springCoefficients=(e,t,n,r)=>{let i=t*r;if(i<1e-9){e.positionPosition=1,e.positionVelocity=r,e.velocityPosition=0,e.velocityVelocity=1;return}let a=Math.exp(-n*i);if(n>=1-1e-8){e.positionPosition=a*(1+i),e.positionVelocity=a*r,e.velocityPosition=-a*t*i,e.velocityVelocity=a*(1-i);return}let o=t*Math.sqrt(1-n*n),s=Math.cos(o*r),c=Math.sin(o*r),l=n*t/o;e.positionPosition=a*(s+l*c),e.positionVelocity=a*c/o,e.velocityPosition=-a*t*t*c/o,e.velocityVelocity=a*(s-l*c)},rootFor=(e,t,n)=>{let r=e.chunk,i=t.dx[r],a=t.dy[r];return n.x=t.x[r]+i*e.offset.forward-a*e.offset.outward*e.sideSign,n.y=t.y[r]+a*e.offset.forward+i*e.offset.outward*e.sideSign,n.dx=i*e.angleCosine-a*e.angleSine,n.dy=a*e.angleCosine+i*e.angleSine,n},jointFor=(e,t,n,r,i)=>{let a=t.x-e.x,o=t.y-e.y,s=magnitude(a,o),c=s>.001?-o/s:-e.dy,l=s>.001?a/s:e.dx,u=n/2,d=Math.min(s,n)/2,f=Math.sqrt(Math.max(0,u**2-d**2));return i.x=e.x+a/2+c*f*r,i.y=e.y+o/2+l*f*r,i},Skin=class{constructor(e,t,n){this.model=e,this.body=t,this.legs=n,this.joint={x:0,y:0},this.hip={x:0,y:0,dx:0,dy:0},this.ornaments=[],this._buildOrnaments()}reconfigure(e,t,n){this.model=e,this.body=t,this.legs=n,this.ornaments.forEach((n,r)=>{n.spec=e.skin.ornaments[r],n.coefficientDt=null;let i=rootFor(n.spec,t.chain,n.root),a=Math.cos(n.angle),o=Math.sin(n.angle);n.directionX=i.dx*a-i.dy*o,n.directionY=i.dy*a+i.dx*o})}_buildOrnaments(){this.ornaments=this.model.skin.ornaments.map(e=>{let t=rootFor(e,this.body.chain,{});return{spec:e,root:t,nextRoot:{...t},angle:0,velocity:0,directionX:t.dx,directionY:t.dy,coefficientDt:null,positionPosition:1,positionVelocity:0,velocityPosition:0,velocityVelocity:1}})}_fitRenderState(e){e||={layout:U},e.model=this.model;let t={chunks:this.body.chain.count*U.chunkStride,legs:this.legs.legs.length*U.legStride,ornaments:this.ornaments.length*U.ornamentStride,plates:this.model.skin.platesTailFirst.length*U.plateStride};for(let[n,r]of Object.entries(t))e[n]&&e[n].length===r||(e[n]=new Float64Array(r));return e}writeRenderState(e){e=this._fitRenderState(e);let t=this.body.chain,n=U.chunkStride;for(let r=0;r<t.count;r++){let i=r*n;e.chunks[i]=t.x[r],e.chunks[i+1]=t.y[r],e.chunks[i+2]=t.dx[r],e.chunks[i+3]=t.dy[r]}let r=this.model.legs.skin.foot,i=U.legStride,a=this.legs.legs.length?this.model.chunks[this.model.legs.start].restDistance:0,o=this.legs.legs.length?this.model.chunks[this.model.legs.end-1].restDistance-a:0,{jointBend:s,jointLeanCenter:c}=this.model.legs;for(let n=0;n<this.legs.legs.length;n++){let l=this.legs.legs[n],u=this.hip;u.x=t.x[l.anchor],u.y=t.y[l.anchor],u.dx=t.dx[l.anchor],u.dy=t.dy[l.anchor];let d=this.legs.armLength(l),f=jointFor(u,l.foot,d,l.sideSign*s,this.joint),p=o?2*(this.model.chunks[l.anchor].restDistance-a)/o-1:0,m=n*i;e.legs[m]=u.x,e.legs[m+1]=u.y,e.legs[m+2]=f.x,e.legs[m+3]=f.y,e.legs[m+4]=l.foot.x,e.legs[m+5]=l.foot.y,e.legs[m+6]=u.dx,e.legs[m+7]=u.dy,e.legs[m+8]=r.scale*(l.progress<1?1:r.plantedScale),e.legs[m+9]=l.sideSign,e.legs[m+10]=(p-c)*d}let l=U.ornamentStride;for(let t=0;t<this.ornaments.length;t++){let n=this.ornaments[t],r=t*l;e.ornaments[r]=n.root.x,e.ornaments[r+1]=n.root.y,e.ornaments[r+2]=n.directionX,e.ornaments[r+3]=n.directionY,e.ornaments[r+4]=n.spec.scale,e.ornaments[r+5]=n.spec.sideSign}let u=this.model.skin.platesTailFirst,d=U.plateStride;for(let n=0;n<u.length;n++){let r=u[n],i=r.chunk,a=n*d;e.plates[a]=t.x[i],e.plates[a+1]=t.y[i],e.plates[a+2]=t.dx[i],e.plates[a+3]=t.dy[i],e.plates[a+4]=r.scale*this.model.chunks[i].plateScale*(1+this.model.skin.loadScale*t.contact[i])}return e}update(e){for(let t=0;t<this.ornaments.length;t++){let n=this.ornaments[t],{spec:r}=n,i=n.root,a=rootFor(r,this.body.chain,n.nextRoot);n.coefficientDt!==e&&(n.coefficientDt=e,springCoefficients(n,r.recover,1-r.wobble*.98,e));let o=Math.atan2(i.dx*a.dy-i.dy*a.dx,i.dx*a.dx+i.dy*a.dy)/e,s=((a.x-i.x)*-a.dy+(a.y-i.y)*a.dx)/e,c=Math.min(W,Math.max(-W,r.waveGain*-o/de+r.physGain*-s/this.model.skin.lateralRate)),l=n.angle-c,u=c+l*n.positionPosition+n.velocity*n.positionVelocity,d=l*n.velocityPosition+n.velocity*n.velocityVelocity;u>W?(u=W,d=0):u<-W&&(u=-W,d=0),n.angle=u,n.velocity=d;let f=Math.cos(u),p=Math.sin(u);n.directionX=a.dx*f-a.dy*p,n.directionY=a.dy*f+a.dx*p,n.root=a,n.nextRoot=i}}translate(e){this.ornaments.forEach(t=>{t.root.x+=e.x,t.root.y+=e.y})}},fe=4,pe=14,limbIndicesFor=e=>{let t=new Uint32Array(e*6*6),n=0;for(let r=0;r<e;r++){let e=r*pe,i=e+pe-1;for(let r=0;r<6;r++){let a=e+r,o=i-r;t.set([a,o,a+1,a+1,o,o-1],n),n+=6}}return t},me=/* @__PURE__ */ new Float64Array(13),he=/* @__PURE__ */ new Float64Array(13);for(let e=0;e<=12;e++){let t=Math.PI*e/12;me[e]=Math.cos(t),he[e]=Math.sin(t)}var ribbonPositionsFor=e=>new Float32Array((e*2+28)*2),ribbonIndicesFor=e=>{let t=Math.max(0,e-1),n=new Uint32Array(t*6+72),r=0;for(let e=0;e<t;e++){let t=e*2;n.set([t,t+1,t+2,t+2,t+1,t+3],r),r+=6}for(let t of[e*2,e*2+14])for(let e=0;e<12;e++)n.set([t,t+1+e,t+2+e],r),r+=3;return n},snapCoordinate=(e,t,n)=>t===1?Math.round(e):t>0?Math.round(e*t)*n:e,snapPositions=(e,t,n,r,i)=>{if(r===1)for(let r=t;r<n;r++)e[r]=Math.round(e[r]);else if(r>0)for(let a=t;a<n;a++)e[a]=Math.round(e[a]*r)*i},writeCap=(e,t,n,r,i,a,o,s,c,l,u)=>{e[t]=n,e[t+1]=r;for(let l=0;l<=12;l++){let u=t+2+l*2,d=me[l],f=he[l];e[u]=n+i*(d*a+f*s),e[u+1]=r+i*(d*o+f*c)}snapPositions(e,t,t+28,l,u)},writeLimb=(e,t,n,r,i,a,o,s,c,l=0,u=0)=>{let d=c*.5,f=i-n,p=a-r,m=Math.hypot(f,p)||1,h=o-i,g=s-a,_=Math.hypot(h,g)||1,v=-p/m,y=f/m,b=-g/_,x=h/_,S=1+v*b+y*x,C=v,w=y;if(S>1e-6){C=(v+b)/S,w=(y+x)/S;let e=Math.abs((C*f+w*p)/m*d),t=Math.min(m,_);e>t&&(C=C*t/e,w=w*t/e)}let T=Math.atan2(v*x-y*b,v*b+y*x),E=Math.cos(T/fe),D=Math.sin(T/fe),O=T<=0,k=v,A=y,j=t+12,M=t+28-2;e[t]=n+v*d,e[t+1]=r+y*d,e[j]=o+b*d,e[j+1]=s+x*d,e[j+2]=o-b*d,e[j+3]=s-x*d,e[M]=n-v*d,e[M+1]=r-y*d;for(let n=0;n<5;n++){let r=O?k:C,o=O?A:w,s=O?C:k,c=O?w:A,l=t+(1+n)*2,u=t+(12-n)*2;e[l]=i+r*d,e[l+1]=a+o*d,e[u]=i-s*d,e[u+1]=a-c*d;let f=k*E-A*D;A=k*D+A*E,k=f}snapPositions(e,t,t+28,l,u)},discard=(e,t)=>{t.parent===e&&e.removeChild(t);let n=t.geometry??null;t.destroy(),n&&n.destroy(!0)},contextFor=(e,t,n)=>{let r=new s.GraphicsPath(e.path).transform(new s.Matrix(n,0,0,n,0,0)),i=new s.GraphicsContext().path(r);return t.fill!==null&&i.fill(t.fill),t.stroke!==null&&t.strokeWidth>0&&i.stroke({color:t.stroke,width:t.strokeWidth*n,cap:`butt`,join:`miter`}),i},paintMesh=(e,t)=>{let n=new s.Color(t);e.tint=n.toNumber(),e.alpha=n.alpha},meshFor=(e,t,n)=>{let r=new s.MeshGeometry({positions:e,uvs:new Float32Array(e.length),indices:t,shrinkBuffersToFit:!1}),i=new s.Mesh({geometry:r,texture:s.Texture.WHITE,roundPixels:!1});return paintMesh(i,n),i.dynamicPositions=e,i.positionBuffer=r.getBuffer(`aPosition`),i},ge=1,_e=1,ve=1024,frameKeyFor=(e,t)=>`${e.path}|${t.fill}|${t.stroke}|${t.strokeWidth}`,ye=/* @__PURE__ */ new WeakMap,be=/* @__PURE__ */ new WeakMap,planAtlas=(e,t)=>{let n=t*4,r=/* @__PURE__ */ new Map,claim=(e,t,n)=>{if(!(n>0))return null;let i=frameKeyFor(e,t),a=r.get(i);return a?a.scale=Math.max(a.scale,n):r.set(i,{key:i,shape:e,paint:t,scale:n}),i},i=e.legs.skin.foot,a=e.legs.pairs?claim(i.shape,i.paint,i.scale*Math.max(1,i.plantedScale)):null,o=1+Math.max(0,e.skin.loadScale),s=e.skin.platesTailFirst.map(t=>claim(t.shape,t.paint,t.scale*e.chunks[t.chunk].plateScale*o)),c=e.skin.ornaments.map(e=>claim(e.shape,e.paint,e.scale));return{key:`${n}\n${[...r.values()].map(({key:e,scale:t})=>`${e}|${t}`).sort().join(`
@@ -1,4 +1,4 @@
1
- /*! Beefwife Canvas v0.1.0. Generated from beefwife-canvas/src; do not edit.
1
+ /*! Beefwife Canvas v0.1.1. Generated from beefwife-canvas/src; do not edit.
2
2
  (c) Kristen Metcalfe, MPL-2.0.
3
3
  Bundles @kvmet/beefwife (MPL-2.0) and @kvmet/terrain (MIT, (c) Kristen Metcalfe). */
4
4
  import * as pixi from "pixi.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kvmet/beefwife-canvas",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Drop-in canvas runtime that populates a page with beefwives.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "Kristen Metcalfe",