@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 +7 -0
- package/README.md +251 -206
- package/beefwife-canvas.js +1 -1
- package/beefwife-canvas.min.js +1 -1
- package/beefwife-canvas.mjs +1 -1
- package/package.json +1 -1
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
16
|
+
<script src="https://cdn.jsdelivr.net/npm/@kvmet/beefwife-canvas@0.1.1/beefwife-canvas.min.js"></script>
|
|
41
17
|
```
|
|
42
18
|
|
|
43
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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": ["
|
|
68
|
+
"sources": ["slow-guy.json", { "src": "long-girl.json", "weight": 3 }]
|
|
98
69
|
}
|
|
99
70
|
```
|
|
100
71
|
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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("#
|
|
117
|
-
const
|
|
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
|
-
|
|
125
|
-
|
|
93
|
+
beefwives.setTarget({ x: 300, y: 180 });
|
|
94
|
+
beefwives.getActors()[0].setTarget({ x: 80, y: 120 });
|
|
126
95
|
```
|
|
127
96
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
`
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
`
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
`
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
`
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
`
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
the
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
208
|
-
runtime.setDebug({ targets: true, routes: true });
|
|
209
|
-
runtime.setDebug({ routes: false, terrain: true });
|
|
210
|
-
```
|
|
229
|
+
## Lifecycle
|
|
211
230
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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).
|
package/beefwife-canvas.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Beefwife Canvas v0.1.
|
|
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) {
|
package/beefwife-canvas.min.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Beefwife Canvas v0.1.
|
|
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(`
|
package/beefwife-canvas.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Beefwife Canvas v0.1.
|
|
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";
|