@kvmet/beefwife 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/DESCRIPTOR.md +221 -0
- package/README.md +214 -465
- package/beefwife.js +1 -1
- package/beefwife.min.js +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
Notable changes to `@kvmet/beefwife`, 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 library is unchanged.
|
|
9
|
+
|
|
10
|
+
The README is rewritten as an install guide and API reference. The descriptor
|
|
11
|
+
field reference moves to `DESCRIPTOR.md`, which now ships with the package.
|
|
12
|
+
|
|
6
13
|
## 0.1.0 (2026-11-14)
|
|
7
14
|
|
|
8
15
|
First release.
|
package/DESCRIPTOR.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Beefwife descriptor
|
|
2
|
+
|
|
3
|
+
A descriptor is one JSON file that defines a beefwife: its body, its gait, its
|
|
4
|
+
legs, and its look. This page covers schema version 1.
|
|
5
|
+
[`beefwife.example.json`](beefwife.example.json) uses every field, and the
|
|
6
|
+
files in [`samples/`](samples/) are complete creatures.
|
|
7
|
+
|
|
8
|
+
| Field | Meaning |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| `schemaVersion` | Always `1`. |
|
|
11
|
+
| `name` | The creature's id and its file name without `.json`. Up to 64 lowercase letters, digits, and hyphens, starting with a letter or digit. |
|
|
12
|
+
| `definitions` | Named materials, shapes, and paints. See [Definitions](#definitions). |
|
|
13
|
+
| `chain` | The body. See [Chain](#chain) and [Skin](#skin). |
|
|
14
|
+
| `gait` | How the body moves. See [Gait](#gait). |
|
|
15
|
+
| `legs` | See [Legs](#legs). |
|
|
16
|
+
|
|
17
|
+
Every field is required, and an unknown field is an error. A value out of
|
|
18
|
+
range is an error, never clamped. `Descriptor.bounds(path)` reports the range
|
|
19
|
+
of any field.
|
|
20
|
+
|
|
21
|
+
## Units
|
|
22
|
+
|
|
23
|
+
- Lengths are world pixels (px).
|
|
24
|
+
- Angles are radians, except `angleDegrees`.
|
|
25
|
+
- Rates are per second, so motion does not depend on frame rate.
|
|
26
|
+
- Fractions and ratios have no unit.
|
|
27
|
+
|
|
28
|
+
## Definitions
|
|
29
|
+
|
|
30
|
+
`definitions` holds three maps of named parts. Ids start with a letter and
|
|
31
|
+
hold up to 64 letters, digits, `_`, and `-`. Every reference names an id in
|
|
32
|
+
the same descriptor.
|
|
33
|
+
|
|
34
|
+
**Materials** set how the chain responds physically.
|
|
35
|
+
|
|
36
|
+
| Field | Range | Meaning |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| `velocityRetention` | 0 to 1 | Share of velocity kept after one second. |
|
|
39
|
+
| `jointCorrection` | 0 to 1 | How strongly bends are pulled toward their target. |
|
|
40
|
+
| `linkCorrection` | 0.001 to 1 | How strongly links are pulled toward their length. |
|
|
41
|
+
| `grip.forward`, `grip.backward`, `grip.lateral` | 0 to 1 | Ground grip against motion in each direction. |
|
|
42
|
+
|
|
43
|
+
Material belongs to chunks. A link between two chunks, including a link across
|
|
44
|
+
a section boundary, uses the mean of their `linkCorrection`. No link stretches
|
|
45
|
+
past three times its target length, however soft its material.
|
|
46
|
+
|
|
47
|
+
**Shapes** are one SVG path each, as `path`. Coordinates are local: positive x
|
|
48
|
+
points toward the head and positive y points outward from the body.
|
|
49
|
+
|
|
50
|
+
**Paints** are a `fill` colour and a `stroke` of `{ colour, width }`. Either may
|
|
51
|
+
be `null`, but not both. Stroke width is in local shape units for feet,
|
|
52
|
+
plates, and ornaments, and in px for the ribbon and limbs.
|
|
53
|
+
|
|
54
|
+
There is no inheritance. To give the tail other physics, define a second
|
|
55
|
+
material and name it in the tail section.
|
|
56
|
+
|
|
57
|
+
## Chain
|
|
58
|
+
|
|
59
|
+
The chain is a line of chunks in three sections: `head`, `trunk`, and `tail`.
|
|
60
|
+
Head and trunk need at least one chunk each; the tail may have none. Together
|
|
61
|
+
they hold 2 to 256 chunks.
|
|
62
|
+
|
|
63
|
+
| Section field | Meaning |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `chunks` | Number of chunks. |
|
|
66
|
+
| `spacing` | Rest distance between chunks, px. |
|
|
67
|
+
| `material` | Material id. |
|
|
68
|
+
| `motionScale` | `{ bend, thrust, gather, contact }`, 0 to 4: how strongly this section follows each gait channel. |
|
|
69
|
+
| `profile.ribbonWidth` | `{ start, end }`: ribbon half-width at the first and last chunk, px. |
|
|
70
|
+
| `profile.plateScale` | `{ start, end }`: plate scale at the first and last chunk. |
|
|
71
|
+
|
|
72
|
+
Values between a section's ends are interpolated. A one-chunk section uses the
|
|
73
|
+
mean of `start` and `end`. Spacing at a section boundary is the mean of the
|
|
74
|
+
two sections.
|
|
75
|
+
|
|
76
|
+
`chain.breathing`, 0 to 1, slowly lengthens and shortens the trunk's inner
|
|
77
|
+
links together, by at most 10% at 1. It runs at zero throttle too, and longer
|
|
78
|
+
trunks breathe more slowly.
|
|
79
|
+
|
|
80
|
+
`chain.physics.autoLift` lifts the chunks that push the creature forward
|
|
81
|
+
least. Each step it ranks chunks by push, lifts the `share` that push least,
|
|
82
|
+
and eases each chunk toward lifted or planted at `rate` per second. `amount` is
|
|
83
|
+
the contact a fully lifted chunk loses: 0 turns lift off, 1 removes all
|
|
84
|
+
contact.
|
|
85
|
+
|
|
86
|
+
`chain.physics.steering` turns the creature toward its requested direction.
|
|
87
|
+
The angle to that direction times `gain` is the wanted bend, limited to
|
|
88
|
+
`limit` radians and approached at `rate` per second. The bend spreads along
|
|
89
|
+
the whole chain, so the creature curves through its body instead of pivoting
|
|
90
|
+
at the neck.
|
|
91
|
+
|
|
92
|
+
## Gait
|
|
93
|
+
|
|
94
|
+
The gait has four channels that read one shared clock.
|
|
95
|
+
|
|
96
|
+
| Field | Meaning |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `cyclesPerSecond` | Clock speed. |
|
|
99
|
+
| `phaseLagRadiansPerPixel` | Phase change per px along the chain. Sets wavelength; positive waves travel head to tail. |
|
|
100
|
+
| `bend.amplitude` | Bend in radians at the trunk's rest spacing. A signed sine. |
|
|
101
|
+
| `thrust.acceleration` | Forward push, px/s². A positive pulse. |
|
|
102
|
+
| `gather.amplitude` | Fraction by which link length grows and shrinks. A signed cosine. |
|
|
103
|
+
| `contact.amplitude` | Fraction of ground contact removed. A positive pulse. |
|
|
104
|
+
|
|
105
|
+
Each channel has an integer `harmonic`, 1 to 8. Thrust, gather, and contact
|
|
106
|
+
also have a `phaseOffset` against bend, −π to π. Thrust and contact have a
|
|
107
|
+
`dutyCycle`: the share of each cycle the pulse is on, as a half sine. A duty
|
|
108
|
+
cycle of 0.5 matches the positive half of a sine.
|
|
109
|
+
|
|
110
|
+
A channel reads `harmonic × (phase − distance × lag) + phaseOffset`.
|
|
111
|
+
|
|
112
|
+
The gait is sampled once per 1/60 s step, so `cyclesPerSecond × harmonic` must
|
|
113
|
+
be at most 15 for each channel: four samples a cycle. Faster waves alias into
|
|
114
|
+
slower or reversed ones.
|
|
115
|
+
|
|
116
|
+
A chunk's grip is its contact pulse, times what auto-lift leaves, times its
|
|
117
|
+
material's grip in the direction it moves.
|
|
118
|
+
|
|
119
|
+
## Legs
|
|
120
|
+
|
|
121
|
+
| Field | Range | Meaning |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| `section` | `head`, `trunk`, `tail` | Section the legs attach to. |
|
|
124
|
+
| `pairs` | 0 to 128 | Number of leg pairs. |
|
|
125
|
+
| `reach` | px | Length of the forward stance window. |
|
|
126
|
+
| `spread` | 0 to 4 | Sideways foot distance, as a fraction of `reach`. |
|
|
127
|
+
| `lead` | 0 to 1 | How far forward the next foot plants. |
|
|
128
|
+
| `fold` | 0 to 1 | How sharply the drawn leg bends. |
|
|
129
|
+
| `jointBend` | −1 to 1 | 1 bows the knee back and out, 0 keeps it on the line from hip to foot, −1 bows it forward and in. |
|
|
130
|
+
| `jointLean` | −1 to 1 | Slides knees along the body, in limb lengths: positive toward the middle of the leg section, negative away. |
|
|
131
|
+
| `jointLeanCenter` | −1 to 1 | Moves the lean's center from the middle of the leg section (0) to its head end (−1) or tail end (1). |
|
|
132
|
+
| `sidePhase` | 0 to 1 | Right-foot timing offset, as a fraction of π. 1 puts the sides opposite. |
|
|
133
|
+
| `liftThreshold` | 0 to 1 | A planted foot lifts when its contact falls below this. |
|
|
134
|
+
| `swingCycles` | 0.001 to 60 | Time in the air per step, in gait cycles. |
|
|
135
|
+
| `swingArc` | 0 to 4 | How far out a foot bows in the air, as a fraction of `reach`. |
|
|
136
|
+
| `jitter` | 0 to 1 | Random variation in leg proportions and step timing. |
|
|
137
|
+
|
|
138
|
+
One pair attaches at the section's middle. Two or more include the section's
|
|
139
|
+
first and last chunks, spaced evenly by rest distance. More pairs than chunks
|
|
140
|
+
share chunks.
|
|
141
|
+
|
|
142
|
+
Knee lean is zero at its center and full at the ends of the leg section; the
|
|
143
|
+
far end can move up to two limb lengths. Jitter scales with the larger of
|
|
144
|
+
`reach` and spread, and a jitter of 0 makes each pair exact mirrors.
|
|
145
|
+
|
|
146
|
+
`legs.skin` sets the look:
|
|
147
|
+
|
|
148
|
+
| Field | Meaning |
|
|
149
|
+
| --- | --- |
|
|
150
|
+
| `limbPaint` | Paint id for the legs. |
|
|
151
|
+
| `limbWidth` | Leg thickness, px. 0 draws feet with no legs. |
|
|
152
|
+
| `foot.shape`, `foot.paint` | Shape and paint ids for the feet. |
|
|
153
|
+
| `foot.scale` | Foot size, px per shape unit. |
|
|
154
|
+
| `foot.plantedScale` | Foot size multiplier while the foot is on the ground. |
|
|
155
|
+
|
|
156
|
+
## Skin
|
|
157
|
+
|
|
158
|
+
`chain.skin` sets the body's look.
|
|
159
|
+
|
|
160
|
+
| Field | Meaning |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| `ribbon.paint` | Paint id for the ribbon along the chain. |
|
|
163
|
+
| `plates` | Shapes drawn on chunks. At most one plate per chunk. |
|
|
164
|
+
| `ornaments` | Shapes that hang from chunks and swing. |
|
|
165
|
+
| `loadScale` | −1 to 10. Plates grow with grip: drawn scale × (1 + `loadScale` × contact). |
|
|
166
|
+
|
|
167
|
+
### Placement
|
|
168
|
+
|
|
169
|
+
Plates and ornaments each have an `id`, a `shape`, a `paint`, a `scale` in px
|
|
170
|
+
per shape unit, and a place given by `at` and `repeat`.
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{ "at": { "section": null, "from": "head", "offset": 1 },
|
|
174
|
+
"repeat": { "count": 4, "step": 1 } }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`at.section` limits the placement to one section; `null` means the whole
|
|
178
|
+
chain. `from` is the end to count from, and `offset` counts chunks from that
|
|
179
|
+
end, from 0. `repeat` places `count` copies, `step` chunks apart, moving away
|
|
180
|
+
from that end. A `count` of `null` fills the rest of the section. A placement
|
|
181
|
+
that runs past its section is an error.
|
|
182
|
+
|
|
183
|
+
### Ornaments
|
|
184
|
+
|
|
185
|
+
| Field | Meaning |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| `side` | `left`, `right`, or `both`. `both` places a mirrored pair. |
|
|
188
|
+
| `layer` | `under` draws below the ribbon and plates, `over` draws above. |
|
|
189
|
+
| `offset` | `{ forward, outward }` from the chunk, px. |
|
|
190
|
+
| `angleDegrees` | Rest angle, −180 to 180. |
|
|
191
|
+
| `source` | 0 to 1. What swings it: 0 is the chunk's turning, which follows the body wave; 1 is its sideways travel, which follows speed and steering. Between them, a blend. |
|
|
192
|
+
| `react` | −4 to 4. Swing size. 0 is rigid, positive trails the motion, negative leads it. |
|
|
193
|
+
| `recover` | 0 to 1000 per second. Swing speed. |
|
|
194
|
+
| `wobble` | 0 to 1. 0 settles without overshoot, 1 overshoots and rings. |
|
|
195
|
+
|
|
196
|
+
An ornament pivots at its shape's origin. Its swing is limited to a quarter
|
|
197
|
+
turn each way. Repeated and mirrored ornaments share definitions, but each
|
|
198
|
+
copy swings on its own. A descriptor may expand to at most 512 ornaments.
|
|
199
|
+
|
|
200
|
+
Ornaments in one layer draw in descriptor order.
|
|
201
|
+
|
|
202
|
+
## Rules that join fields
|
|
203
|
+
|
|
204
|
+
`Descriptor.bounds` cannot show these, so check them here:
|
|
205
|
+
|
|
206
|
+
- The three sections hold 2 to 256 chunks together.
|
|
207
|
+
- Each placement must land on chunks inside its section, and no two plates may
|
|
208
|
+
land on one chunk.
|
|
209
|
+
- Gather and contact amplitudes, scaled by each section's `motionScale`, must
|
|
210
|
+
not make a link or a contact value negative.
|
|
211
|
+
- `cyclesPerSecond × harmonic` must be at most 15 for each gait channel.
|
|
212
|
+
- A descriptor may have at most 256 of each kind of definition, and its paths
|
|
213
|
+
may total at most 1,048,576 characters.
|
|
214
|
+
|
|
215
|
+
## Resizing
|
|
216
|
+
|
|
217
|
+
Px are world pixels. To resize a creature, use `Descriptor.scale(descriptor,
|
|
218
|
+
factor)`; do not edit fields by hand. It multiplies every px field by
|
|
219
|
+
`factor` and divides every per-px field by it, so the creature moves the same
|
|
220
|
+
way at the new size. Ribbon and leg stroke widths grow directly; shape
|
|
221
|
+
strokes grow with the placement scale.
|
package/README.md
CHANGED
|
@@ -1,256 +1,187 @@
|
|
|
1
1
|
# Beefwife
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
field aliases. The other modules build, step, and render beefwives; Beefwife Lab
|
|
7
|
-
is a consumer of this directory.
|
|
3
|
+
Beefwife simulates and draws one procedurally animated chain-body creature, a
|
|
4
|
+
beefwife, as a Pixi display object. A descriptor, plain JSON, defines its body,
|
|
5
|
+
gait, and look.
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
runtime descriptor.
|
|
7
|
+
```js
|
|
8
|
+
import { Beefwife, Descriptor } from "@kvmet/beefwife";
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
const response = await fetch("slow-guy.json");
|
|
11
|
+
const descriptor = Descriptor.read(await response.json());
|
|
12
|
+
const beefwife = new Beefwife(descriptor, { position: { x: 200, y: 150 } });
|
|
13
|
+
app.stage.addChild(beefwife);
|
|
14
|
+
|
|
15
|
+
app.ticker.add((ticker) => {
|
|
16
|
+
const dt = Math.min(ticker.deltaMS / 1000, 1 / 30);
|
|
17
|
+
beefwife.step(dt, { direction: { x: 1, y: 0.2 } });
|
|
18
|
+
});
|
|
19
|
+
```
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
Beefwife has no clock, routing, or page placement. You step it and you steer
|
|
22
|
+
it. To put creatures on a web page with none of that work, use
|
|
23
|
+
[`@kvmet/beefwife-canvas`](https://www.npmjs.com/package/@kvmet/beefwife-canvas).
|
|
18
24
|
|
|
19
|
-
##
|
|
25
|
+
## Install
|
|
20
26
|
|
|
21
27
|
```sh
|
|
22
28
|
npm install @kvmet/beefwife pixi.js
|
|
23
29
|
```
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
one copy for the page and the library shares it:
|
|
31
|
+
Or load the classic script after Pixi:
|
|
27
32
|
|
|
28
|
-
```
|
|
29
|
-
|
|
33
|
+
```html
|
|
34
|
+
<script src="https://cdn.jsdelivr.net/npm/pixi.js@8.23.0/dist/pixi.min.js"></script>
|
|
35
|
+
<script src="https://cdn.jsdelivr.net/npm/@kvmet/beefwife@0.1.1/beefwife.min.js"></script>
|
|
30
36
|
```
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
| Entry | Use |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `import` | `src/beefwife.mjs`. Exports `Beefwife` and `Descriptor`, and imports `pixi.js`, so a bundler gives the page and the library one copy. |
|
|
41
|
+
| `require`, `beefwife.min.js` | Classic script. Reads `window.PIXI` and adds one global, `Beefwife`, with the descriptor API at `Beefwife.Descriptor`. |
|
|
42
|
+
| `beefwife.js` | The classic script, unminified. |
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
WebGPU
|
|
36
|
-
releases, destroying one WebGPU renderer can break another.
|
|
44
|
+
Pixi 8.21.0 or newer is required. In earlier 8.x releases, destroying one
|
|
45
|
+
WebGPU renderer can break another. WebGL and WebGPU are both supported.
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
that global is missing a beefwife still simulates: it builds no display children
|
|
41
|
-
and leaves `onRender` null, so a headless page or a server gets the pose without
|
|
42
|
-
a renderer.
|
|
47
|
+
Without `window.PIXI`, the classic script still simulates. It creates no
|
|
48
|
+
display objects, so a server or a test gets poses without a renderer.
|
|
43
49
|
|
|
44
|
-
In Node, `require`
|
|
45
|
-
|
|
46
|
-
`instanceof` fails across them. Use one form throughout a program.
|
|
50
|
+
In Node, `require` and `import` load separate copies with separate `Beefwife`
|
|
51
|
+
classes, and `instanceof` fails between them. Use one form in a program.
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
### Samples
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
Seven descriptors ship under `samples/`: `bounding`, `chevron-guy`,
|
|
56
|
+
`long-girl`, `peristaltic`, `reticulating`, `slow-guy`, and `undulating`.
|
|
57
|
+
`beefwife.example.json` uses every field. A CDN serves them without an install:
|
|
53
58
|
|
|
54
|
-
```
|
|
55
|
-
|
|
59
|
+
```text
|
|
60
|
+
https://cdn.jsdelivr.net/npm/@kvmet/beefwife@0.1.1/samples/slow-guy.json
|
|
61
|
+
```
|
|
56
62
|
|
|
57
|
-
|
|
58
|
-
const beefwife = new Beefwife(chevronGuy, {
|
|
59
|
-
position: { x: 200, y: 150 }, // world px at the head
|
|
60
|
-
direction: { x: 1, y: 0 },
|
|
61
|
-
phase: 0, // gait phase in radians
|
|
62
|
-
random: Math.random,
|
|
63
|
-
render: {
|
|
64
|
-
roundVertices: false,
|
|
65
|
-
pixelResolution: 1,
|
|
66
|
-
kneeProjection: {
|
|
67
|
-
centerX: 0,
|
|
68
|
-
centerY: 0,
|
|
69
|
-
perspective: 0.002,
|
|
70
|
-
maxOffset: 256,
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
});
|
|
63
|
+
## API
|
|
74
64
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
beefwife.setDescriptor(nextDescriptor);
|
|
78
|
-
beefwife.reset({ position, direction, phase });
|
|
79
|
-
beefwife.translate({ x: 20, y: 0 });
|
|
80
|
-
const pose = beefwife.getPose();
|
|
81
|
-
```
|
|
65
|
+
All coordinates are world pixels and all angles are radians. Invalid input
|
|
66
|
+
throws and changes nothing; no value is clamped.
|
|
82
67
|
|
|
83
|
-
|
|
84
|
-
defaults to `{ x: 0, y: 0 }`, direction to `{ x: 1, y: 0 }`, phase to zero, and
|
|
85
|
-
random to `Math.random`. Coordinates and phase must be finite. Direction must be
|
|
86
|
-
a nonzero finite vector and is normalized by the library. `random` must be a
|
|
87
|
-
function whose samples are finite numbers from zero inclusive to one exclusive;
|
|
88
|
-
it seeds the breathing starting phase and leg variation. Caller-supplied
|
|
89
|
-
positions and translations are limited to
|
|
90
|
-
`±Beefwife.MAX_WORLD_COORDINATE` (one billion px). Placement and translation are
|
|
91
|
-
rejected atomically if any body chunk would leave that supported range. If
|
|
92
|
-
simulation reaches the numeric boundary, the whole beefwife is rebased just
|
|
93
|
-
inside it; current, previous, leg, and ornament positions move together, so
|
|
94
|
-
velocity and secondary motion are preserved.
|
|
95
|
-
|
|
96
|
-
The optional `render` object is a live rendering policy with exactly three
|
|
97
|
-
fields. `roundVertices` is boolean pixel snapping. `pixelResolution` is the
|
|
98
|
-
positive number of output pixels per world pixel from `1e-6` to `1e6`, and
|
|
99
|
-
defaults to one; snapping uses its output-pixel grid, and a host should update
|
|
100
|
-
it when renderer resolution changes. A live value that is not a finite number
|
|
101
|
-
keeps drawing at the last valid one and is reported once, after the render
|
|
102
|
-
pass, the way texture rendering failures are. `kneeProjection` is `null` or an object
|
|
103
|
-
containing `centerX` and `centerY` within `±Beefwife.MAX_WORLD_COORDINATE` and
|
|
104
|
-
a `perspective` from zero to `1e6`, plus an optional `maxOffset` from zero to
|
|
105
|
-
`Beefwife.MAX_WORLD_COORDINATE`. The ranges are wide enough for any real
|
|
106
|
-
viewport and narrow enough that the projection arithmetic stays finite; a
|
|
107
|
-
retained object edited past them is capped rather than rejected, since nothing
|
|
108
|
-
revalidates it. Projection changes only
|
|
109
|
-
the drawn knee shared by the two segments of each limb; simulated hips and
|
|
110
|
-
planted feet do not move. The object is retained so a host may update its center when
|
|
111
|
-
its viewport changes. Unknown render or projection fields are rejected.
|
|
112
|
-
|
|
113
|
-
`step` owns fixed simulation substeps but never owns a clock or animation loop.
|
|
114
|
-
`dt` must be finite and nonnegative seconds, and all of it is simulated: a
|
|
115
|
-
`dt` of one second costs sixty substeps. Only the host can tell a resumed
|
|
116
|
-
background tab from a deliberate fast-forward, so the host bounds `dt` before
|
|
117
|
-
passing it, usually to one frame of its own rate. The controls
|
|
118
|
-
object is optional. Omitted direction retains the last requested direction;
|
|
119
|
-
omitted throttle means full throttle. Throttle must be finite from zero to one;
|
|
120
|
-
inputs are rejected rather than clamped. Throttle linearly scales gait phase
|
|
121
|
-
rate, channel amplitude, and steering. At zero, existing velocity settles under
|
|
122
|
-
full contact but the beefwife produces no new drive.
|
|
123
|
-
|
|
124
|
-
`Beefwife` extends `PIXI.Container`. Its retained display children refresh
|
|
125
|
-
through Pixi's render lifecycle, so hosts only add the beefwife to a stage and
|
|
126
|
-
render that stage. The creature's own parts hold a container of their own one
|
|
127
|
-
level down, so a child a host adds to a beefwife keeps the place the host gave
|
|
128
|
-
it however the creature is edited. Feet sit below the limb mesh. Under-layer
|
|
129
|
-
ornaments retain descriptor order, followed by the ribbon, plates from tail to
|
|
130
|
-
head, and over-layer ornaments in descriptor order. Headless platforms may
|
|
131
|
-
simulate beefwives without creating display children. With Pixi loaded,
|
|
132
|
-
construction and descriptor replacement reject SVG paths and colors that Pixi
|
|
133
|
-
cannot parse.
|
|
134
|
-
|
|
135
|
-
Two inherited container properties belong to the library. `label` carries the
|
|
136
|
-
descriptor's `name` and is rewritten on every `setDescriptor`. `onRender` is
|
|
137
|
-
how a beefwife draws itself, and `destroy` clears it. A host that wants its own
|
|
138
|
-
label or its own per-frame callback puts them on a parent or a sibling.
|
|
139
|
-
|
|
140
|
-
`setDescriptor` validates replacement resources before changing the instance.
|
|
141
|
-
Invalid input changes nothing about the object definition; a rejected call may
|
|
142
|
-
still have drawn from `random`, which seeds state rather than definition.
|
|
143
|
-
Replacement keeps as much of the live creature as the new descriptor allows.
|
|
144
|
-
Chunk positions, previous positions, steering state, fixed-step remainder, and
|
|
145
|
-
gait and breathing phases survive every edit. Changing a section's chunk count
|
|
146
|
-
carries the chain rather than re-placing it: a chunk the descriptor still names
|
|
147
|
-
keeps its exact position and velocity, and an added one is seeded between its
|
|
148
|
-
neighbours, so the creature settles from where it stood. The head tangent then
|
|
149
|
-
follows whichever chunk sits behind the head, which is a real turn when the
|
|
150
|
-
chunk that defined it was removed.
|
|
151
|
-
|
|
152
|
-
Only the number of leg pairs rebuilds foot state, and only a change to the
|
|
153
|
-
expanded ornament list rebuilds ornament swing. Everything else those parts
|
|
154
|
-
read comes from the model each step, so a foot stays planted and a swing stays
|
|
155
|
-
mid-flight while its stance, anchor, colour, or spring settings change under
|
|
156
|
-
it. Enabling breathing samples a starting phase. A change in breathing strength
|
|
157
|
-
eases in over at most 0.2 s rather than resizing the trunk at once.
|
|
158
|
-
|
|
159
|
-
Display objects are retained on the same terms, one at a time rather than all
|
|
160
|
-
together. A replacement repaints the children it can and changes only what the
|
|
161
|
-
new descriptor disagrees with. Feet, plates, and ornaments draw from baked
|
|
162
|
-
frames, so they are rebuilt only when a frame changes (a shape, paint, or
|
|
163
|
-
scale) or a placement does (a count or a layer); until the new frames are
|
|
164
|
-
baked, the old shapes keep moving with the body. A limb or ribbon mesh is
|
|
165
|
-
rebuilt only when its vertex count moves or its paint gains or loses a fill or
|
|
166
|
-
a visible stroke. Adding a leg pair rebuilds the limb mesh; adding a chunk
|
|
167
|
-
rebuilds the ribbon mesh. Nothing else in the scene is discarded, so no edit
|
|
168
|
-
restarts the creature.
|
|
169
|
-
|
|
170
|
-
`reset(options)` deliberately clears physical and secondary state and samples a
|
|
171
|
-
new breathing phase when breathing is enabled. Its optional position defaults
|
|
172
|
-
to the current head position, phase to the current gait phase, and direction to
|
|
173
|
-
the current head tangent. Omitting direction preserves the last requested
|
|
174
|
-
control direction; providing direction uses it for both body placement and
|
|
175
|
-
future omitted-direction steps. Values follow the constructor's validation
|
|
176
|
-
rules.
|
|
177
|
-
|
|
178
|
-
`translate({ x, y })` shifts all current and previous world positions by a
|
|
179
|
-
finite offset. It preserves velocity, gait phase, steering, and secondary
|
|
180
|
-
motion, making it suitable for world wrapping. It does not change the last
|
|
181
|
-
requested direction.
|
|
182
|
-
|
|
183
|
-
`destroy()` takes down every display object the instance owns and detaches it
|
|
184
|
-
from its parent. It is required, not optional: a host running many beefwives
|
|
185
|
-
must call it, because the scene is retained and nothing else frees it.
|
|
186
|
-
Afterwards `step`, `setDescriptor`, `reset`, and `translate` throw. The
|
|
187
|
-
descriptor and the last pose remain readable.
|
|
188
|
-
|
|
189
|
-
`getPose()` returns a stable runtime-owned snapshot in world pixels. The same
|
|
190
|
-
object is refreshed by `step`, `setDescriptor`, `reset`, and `translate`, so a
|
|
191
|
-
host may retain the reference instead of requesting a new object each frame.
|
|
192
|
-
Treat the snapshot as read-only. `head` is chunk zero, `center` is the
|
|
193
|
-
arithmetic centroid of all chain chunks, and `direction` is the normalized head
|
|
194
|
-
tangent pointing headward. Each is `{ x, y }`. It never exposes mutable chunks
|
|
195
|
-
or renderer state, and changing the snapshot cannot change the simulation.
|
|
196
|
-
Render bounds and collision geometry are not part of schema v1. Constructor
|
|
197
|
-
input and the `descriptor` getter cannot be used to mutate the instance. Random
|
|
198
|
-
variation comes only from the injected random function.
|
|
199
|
-
|
|
200
|
-
A descriptor is a value. One model is compiled per descriptor object and
|
|
201
|
-
shared by every creature given that object, so a cast of a thousand pays one
|
|
202
|
-
compile. The object is deep-frozen when it is compiled, which is what makes
|
|
203
|
-
that safe: an edit in place would otherwise leave every creature drawing the
|
|
204
|
-
model built before it, silently. Pass a copy to change a descriptor, and pass
|
|
205
|
-
the copy to `setDescriptor`. A host that loads its creatures once and never
|
|
206
|
-
edits them, which is the ordinary case, need do nothing.
|
|
207
|
-
|
|
208
|
-
`restLength` is the chain's resting arc length in world pixels, summed over
|
|
209
|
-
every link and averaged across section boundaries. It follows `setDescriptor`.
|
|
210
|
-
A host that compares `getPose()` against its own bounds needs it: the centroid
|
|
211
|
-
trails the head by up to this distance, so a viewport test without it reads a
|
|
212
|
-
long creature as gone while it is still on screen.
|
|
68
|
+
### `new Beefwife(descriptor, options = {})`
|
|
213
69
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
<script src="beefwife.js"></script>
|
|
217
|
-
```
|
|
70
|
+
Builds a creature from a descriptor. With Pixi present, this also rejects SVG
|
|
71
|
+
paths and colours that Pixi cannot parse, and shapes too large to draw.
|
|
218
72
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
73
|
+
| Option | Default | Meaning |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| `position` | `{ x: 0, y: 0 }` | Where the head starts. |
|
|
76
|
+
| `direction` | `{ x: 1, y: 0 }` | Which way the head points. Any nonzero vector; it is normalized. |
|
|
77
|
+
| `phase` | `0` | Starting gait phase. |
|
|
78
|
+
| `random` | `Math.random` | A function returning numbers from 0 up to 1. The only source of variation, in leg proportions, step timing, and breathing. |
|
|
79
|
+
| `render` | See below | How the creature draws. |
|
|
80
|
+
|
|
81
|
+
`render` is kept, not copied, so a host can change its fields while the
|
|
82
|
+
creature runs.
|
|
83
|
+
|
|
84
|
+
| `render` field | Default | Meaning |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| `roundVertices` | `false` | Snap vertices to the output pixel grid. |
|
|
87
|
+
| `pixelResolution` | `1` | Output pixels per world pixel, `1e-6` to `1e6`. Set it from the renderer resolution and update it when that changes. |
|
|
88
|
+
| `kneeProjection` | `null` | `{ centerX, centerY, perspective, maxOffset }`. Pushes each drawn knee away from the center by distance times knee height times `perspective`, up to `maxOffset` (default 256). Changes only the drawn knees. |
|
|
89
|
+
|
|
90
|
+
A `pixelResolution` that is changed to an invalid value is reported once,
|
|
91
|
+
after the render pass, and drawing continues at the last valid value.
|
|
92
|
+
|
|
93
|
+
### `step(dt, controls = {})`
|
|
94
|
+
|
|
95
|
+
Simulates `dt` seconds in fixed steps of 1/60 s. `dt` is never capped: one
|
|
96
|
+
second costs sixty steps. Cap it yourself, usually at one frame, so a tab that
|
|
97
|
+
resumes after a minute does not simulate the whole minute.
|
|
98
|
+
|
|
99
|
+
| Control | Default | Meaning |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| `direction` | The last one given | The way the creature turns toward. |
|
|
102
|
+
| `throttle` | `1` | From 0 to 1. Scales gait speed, gait strength, and steering. At 0 the creature coasts to a stop. |
|
|
103
|
+
|
|
104
|
+
### `setDescriptor(descriptor)`
|
|
105
|
+
|
|
106
|
+
Changes the creature to a new descriptor and keeps as much of it as the new
|
|
107
|
+
descriptor allows. See [Edits keep the creature](#edits-keep-the-creature).
|
|
108
|
+
|
|
109
|
+
### `reset(options = {})`
|
|
110
|
+
|
|
111
|
+
Places the creature again with no motion. Takes `position`, `direction`, and
|
|
112
|
+
`phase`, which default to the current head, head direction, and gait phase. A
|
|
113
|
+
given `direction` also becomes the direction `step` uses when its control is
|
|
114
|
+
omitted.
|
|
115
|
+
|
|
116
|
+
### `translate({ x, y })`
|
|
117
|
+
|
|
118
|
+
Moves the whole creature and keeps its motion, gait, and steering. Use it to
|
|
119
|
+
wrap a creature around a world edge.
|
|
223
120
|
|
|
224
|
-
`
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
121
|
+
### `getPose()`
|
|
122
|
+
|
|
123
|
+
Returns `{ head, center, direction }`, each `{ x, y }`. `center` is the mean of
|
|
124
|
+
all chain points; `direction` is the unit vector the head points along. The
|
|
125
|
+
same object is refreshed after each change, so you can keep the reference.
|
|
126
|
+
Treat it as read-only.
|
|
127
|
+
|
|
128
|
+
### `restLength`
|
|
129
|
+
|
|
130
|
+
The chain's length at rest. `center` can trail the head by up to this
|
|
131
|
+
distance, so add it to the margin when you test whether a creature is in view.
|
|
132
|
+
|
|
133
|
+
### `descriptor`
|
|
134
|
+
|
|
135
|
+
The descriptor the creature is built from.
|
|
136
|
+
|
|
137
|
+
### `getBendResponse(into = [])`
|
|
138
|
+
|
|
139
|
+
Returns one `{ joint, commanded, delivered }` per inner chain joint: the bend
|
|
140
|
+
the gait asks for and the bend the body makes, positive to the left. Use it to
|
|
141
|
+
see what a `gait.bend` amplitude produces on a given body. Pass an array to
|
|
142
|
+
fill it instead of allocating a new one.
|
|
143
|
+
|
|
144
|
+
### `destroy()`
|
|
145
|
+
|
|
146
|
+
Frees every display object the creature owns and removes it from its parent.
|
|
147
|
+
Call it for each creature you drop; nothing else frees them. Afterwards `step`,
|
|
148
|
+
`setDescriptor`, `reset`, `translate`, and `getBendResponse` throw.
|
|
149
|
+
`descriptor` and the last pose stay readable.
|
|
150
|
+
|
|
151
|
+
### `error` event
|
|
152
|
+
|
|
153
|
+
A texture that fails to render is emitted as `error` on the creature. With no
|
|
154
|
+
listener, the error is thrown. The texture is tried again only after an edit,
|
|
155
|
+
a resolution change, or a renderer change.
|
|
156
|
+
|
|
157
|
+
### `Beefwife.MAX_WORLD_COORDINATE`
|
|
158
|
+
|
|
159
|
+
`1e9`. Positions, translations, and projection centers must be within this
|
|
160
|
+
distance of the origin on each axis. A creature that walks to the limit is
|
|
161
|
+
moved back inside it, motion included.
|
|
162
|
+
|
|
163
|
+
## Descriptor
|
|
164
|
+
|
|
165
|
+
`Descriptor` reads, checks, resizes, and writes descriptors. Every function
|
|
166
|
+
throws an `Error` whose message starts with the path of the bad field, such as
|
|
167
|
+
`$.legs.pairs: must be an integer`. The format is in
|
|
168
|
+
[DESCRIPTOR.md](https://github.com/kvmet/beefwife/blob/main/beefwife/DESCRIPTOR.md).
|
|
169
|
+
|
|
170
|
+
| Member | Does |
|
|
171
|
+
| --- | --- |
|
|
172
|
+
| `parse(text)` | Parses JSON text and checks it. Returns a new descriptor. |
|
|
173
|
+
| `read(value)` | Checks a parsed value. Returns a new plain copy with keys in canonical order. |
|
|
174
|
+
| `stringify(value, space = 2)` | Checks a descriptor and returns canonical JSON text. |
|
|
175
|
+
| `scale(descriptor, factor)` | Returns a copy resized by `factor`, with the same timing and feel. A field the result pushes out of range is an error. |
|
|
176
|
+
| `bounds(path)` | What the schema allows for one field. |
|
|
177
|
+
| `VERSION` | `1`, the schema version this release reads. |
|
|
178
|
+
| `LIMITS` | Size limits: `chunks`, `definitions`, `placements`, `instances`, `name`, `path`, and `pathTotal`. |
|
|
179
|
+
| `ID_PATTERN` | Pattern for definition ids. |
|
|
180
|
+
| `NAME_PATTERN` | Pattern for `name`. |
|
|
181
|
+
|
|
182
|
+
`bounds` names a field with dots, an array item with `[]`, and a record entry
|
|
183
|
+
with `*`. It lets an editor take a range from the schema instead of keeping a
|
|
184
|
+
second copy:
|
|
254
185
|
|
|
255
186
|
```js
|
|
256
187
|
Descriptor.bounds("legs.pairs");
|
|
@@ -261,250 +192,68 @@ Descriptor.bounds("definitions.paints.*.stroke.width");
|
|
|
261
192
|
// { kind: "number", min: 0, max: 1000, integer: false, nullable: false }
|
|
262
193
|
```
|
|
263
194
|
|
|
264
|
-
Every kind
|
|
265
|
-
add `minLength`, `maxLength`, `pattern`, and `blankAllowed
|
|
266
|
-
`values
|
|
267
|
-
`
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
A few fields answer to more than themselves, and no per-field bound can say so:
|
|
271
|
-
the three section chunk counts must total from 2 to 256, a placement's anchor
|
|
272
|
-
must land on a chunk in scope that no other plate claims, the contact and
|
|
273
|
-
gather amplitudes are rejected where their section scale and the gait channel
|
|
274
|
-
together would drive a link or a footfall negative, and each gait channel's
|
|
275
|
-
harmonic times `cyclesPerSecond` must be at most 15. A path the schema does
|
|
276
|
-
not define is an error. A consumer may narrow a range to suit its own users,
|
|
277
|
-
but a value outside the reported bound is one `read` rejects.
|
|
278
|
-
|
|
279
|
-
See [beefwife.example.json](beefwife.example.json) for the complete shape.
|
|
280
|
-
|
|
281
|
-
## Definitions and links
|
|
282
|
-
|
|
283
|
-
Every reference stays inside one descriptor:
|
|
284
|
-
|
|
285
|
-
- A material is physical chain response: velocity retention per second, joint
|
|
286
|
-
and link correction, and directional grip.
|
|
287
|
-
- A shape is one SVG path in chunk-local pixels. Positive x points toward the
|
|
288
|
-
head and positive y points outward.
|
|
289
|
-
- A paint is a nullable fill colour and a nullable `{ colour, width }` stroke.
|
|
290
|
-
At least one must be visible. Stroke width uses local shape units for feet,
|
|
291
|
-
plates, and ornaments, and world pixels for ribbons and limbs.
|
|
292
|
-
|
|
293
|
-
Each chain section names one complete material. Sharing a material id links the
|
|
294
|
-
sections; giving the tail different physics means defining another material and
|
|
295
|
-
changing its reference. There is no material inheritance or partial override.
|
|
296
|
-
|
|
297
|
-
Material response belongs to chunks. The link joining two chunks averages their
|
|
298
|
-
`linkCorrection`, including links across section boundaries. Boundary resting
|
|
299
|
-
spacing and gather response are likewise the averages of their adjacent chunks.
|
|
300
|
-
Link correction is always positive, and every link is held to at most three
|
|
301
|
-
times its current target length however soft its material, so separation cannot
|
|
302
|
-
grow without bound. The ceiling sits well above ordinary motion and engages
|
|
303
|
-
only on a chain that would otherwise run away.
|
|
304
|
-
|
|
305
|
-
Visual placements name one shape and one paint. Geometry and paint never refer
|
|
306
|
-
to each other, and definitions cannot refer outside their descriptor.
|
|
307
|
-
|
|
308
|
-
## Chain regions
|
|
309
|
-
|
|
310
|
-
The chain has exactly `head`, `trunk`, and `tail` sections. Head and trunk must
|
|
311
|
-
contain at least one chunk; the tail may be empty. Together they contain 2 to
|
|
312
|
-
256 chunks.
|
|
313
|
-
|
|
314
|
-
Each section declares:
|
|
315
|
-
|
|
316
|
-
- `chunks`: number of points in the section;
|
|
317
|
-
- `spacing`: resting point spacing in px;
|
|
318
|
-
- `material`: physical material id;
|
|
319
|
-
- `motionScale`: response to each gait channel;
|
|
320
|
-
- `profile.ribbonWidth`: half-width in local px at both section ends;
|
|
321
|
-
- `profile.plateScale`: plate scale at both section ends.
|
|
322
|
-
|
|
323
|
-
Adjacent section spacing is resolved at build time. Profiles may meet smoothly
|
|
324
|
-
or make an intentional discontinuity.
|
|
325
|
-
|
|
326
|
-
Profile endpoints are sampled at the first and last chunk of a section. A
|
|
327
|
-
one-chunk section uses the average of its two endpoint values.
|
|
328
|
-
|
|
329
|
-
`chain.breathing` is a normalized intensity for subtle, simultaneous expansion
|
|
330
|
-
and contraction of internal trunk links. At full intensity it changes their
|
|
331
|
-
target length by at most 10%. Its independent cycle continues at zero throttle;
|
|
332
|
-
trunks with more chunks breathe more slowly. Head, tail, and section-boundary
|
|
333
|
-
links retain their ordinary target lengths.
|
|
334
|
-
|
|
335
|
-
## Chain physics
|
|
336
|
-
|
|
337
|
-
`chain.physics` holds the two responses that are not gait channels.
|
|
338
|
-
|
|
339
|
-
`autoLift` is adaptive ground lift. Each substep it ranks chunks by how far
|
|
340
|
-
their grip pushed the creature along its travel axis, picks up the `share`
|
|
341
|
-
fraction that pushed the least, and eases every chunk toward its lifted or
|
|
342
|
-
planted target at `rate` per second. `amount` is the contact a fully lifted
|
|
343
|
-
chunk loses: `0` disables the mechanism outright and `1` frees the chunk. Lift
|
|
344
|
-
multiplies the contact rhythm rather than replacing it.
|
|
345
|
-
|
|
346
|
-
`steering` turns the head toward the requested direction. The angle between
|
|
347
|
-
them times `gain` is the wanted bend bias in radians, clamped to `limit`; the
|
|
348
|
-
applied bias eases toward that at `rate` per second. The bias adds to the bend
|
|
349
|
-
channel along the whole chain, so a turning beefwife curves through its body
|
|
350
|
-
instead of pivoting at the neck. Throttle scales it.
|
|
351
|
-
|
|
352
|
-
## Gait
|
|
353
|
-
|
|
354
|
-
All channels read one phase clock and one spatial lag:
|
|
355
|
-
|
|
356
|
-
- `cyclesPerSecond` advances the shared clock. The gait is sampled once per
|
|
357
|
-
1/60 s physics step, so each channel's rate, `cyclesPerSecond * harmonic`,
|
|
358
|
-
must be at most 15: four steps a cycle. Faster waves alias into slower or
|
|
359
|
-
reversed ones.
|
|
360
|
-
- `phaseLagRadiansPerPixel` controls wavelength and direction along resting
|
|
361
|
-
chain distance. Positive values travel from head to tail.
|
|
362
|
-
- Bend is a signed sine. Its amplitude is radians of curvature at the trunk's
|
|
363
|
-
resting spacing.
|
|
364
|
-
- Thrust is a positive pulse whose acceleration is px/s^2.
|
|
365
|
-
- Gather is a signed cosine and changes resting spacing by a fraction.
|
|
366
|
-
- Contact is a positive pulse whose amplitude removes a fraction of contact.
|
|
367
|
-
|
|
368
|
-
Each channel declares an integer harmonic. Bend is the phase reference; the
|
|
369
|
-
other channels declare a phase offset in radians relative to it. Thrust and
|
|
370
|
-
contact also declare a duty cycle. Section motion scales may attenuate or
|
|
371
|
-
amplify channels, but validation prevents non-positive gathered lengths and
|
|
372
|
-
negative contact.
|
|
373
|
-
|
|
374
|
-
The grip a chunk feels is its contact rhythm times what the adaptive ground
|
|
375
|
-
lift leaves, gated by its material's directional grip; contact declares the
|
|
376
|
-
rhythm, while ground lift picks up whichever chunks push the least.
|
|
195
|
+
Every result has `kind` and `nullable`. Numbers add `min`, `max`, and
|
|
196
|
+
`integer`. Strings add `minLength`, `maxLength`, `pattern`, and `blankAllowed`.
|
|
197
|
+
Choices add `values`, objects add `fields`, arrays add `maxLength`, and records
|
|
198
|
+
add `minEntries`, `maxEntries`, and `keyPattern`. A few rules join several
|
|
199
|
+
fields and cannot show in one field's bounds; DESCRIPTOR.md lists them.
|
|
377
200
|
|
|
378
|
-
|
|
379
|
-
bend's offset fixed at zero. Thrust and
|
|
380
|
-
contact use a half-sine pulse inside the declared fraction of that channel's
|
|
381
|
-
cycle and zero outside it. A duty cycle of `0.5` therefore matches the positive
|
|
382
|
-
half of a sine.
|
|
383
|
-
|
|
384
|
-
Solver timing, relaxation pass count, mass, and waveform implementations are
|
|
385
|
-
library rules rather than descriptor parameters.
|
|
386
|
-
|
|
387
|
-
## Placements
|
|
388
|
-
|
|
389
|
-
An anchor addresses either the whole chain or one section:
|
|
201
|
+
## Descriptors are values
|
|
390
202
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
203
|
+
A creature freezes the descriptor object it is given, and every creature given
|
|
204
|
+
the same object shares one compiled model. A cast of a thousand compiles once.
|
|
205
|
+
To change a descriptor, make a copy, change the copy, and pass it to
|
|
206
|
+
`setDescriptor`.
|
|
395
207
|
|
|
396
|
-
|
|
208
|
+
## Edits keep the creature
|
|
397
209
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
210
|
+
`setDescriptor` checks the new descriptor first. If it fails, the creature is
|
|
211
|
+
unchanged. A failed call may still have drawn from `random`.
|
|
212
|
+
|
|
213
|
+
A successful edit keeps position, motion, steering, and gait and breathing
|
|
214
|
+
phase. When a section gains or loses chunks, the chunks that remain keep their
|
|
215
|
+
place and motion, and new chunks start between their neighbours. Feet rebuild
|
|
216
|
+
only when the number of leg pairs changes, and ornament motion rebuilds only
|
|
217
|
+
when the list of ornaments changes. A change in breathing strength eases in
|
|
218
|
+
over at most 0.2 s.
|
|
219
|
+
|
|
220
|
+
Display objects are kept the same way. Only the ones the edit changes are
|
|
221
|
+
rebuilt, and old shapes keep moving with the body until their replacements are
|
|
222
|
+
ready.
|
|
402
223
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
Drawing order
|
|
413
|
-
|
|
414
|
-
1.
|
|
415
|
-
2.
|
|
416
|
-
3.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
`
|
|
427
|
-
|
|
428
|
-
second. Other leg timing and geometry values are bounded ratios.
|
|
429
|
-
|
|
430
|
-
Two or more pairs include the first and last chunks of their section and choose
|
|
431
|
-
nearest chunks at even resting-distance intervals. One pair uses the section
|
|
432
|
-
midpoint. More pairs than chunks may share anchors. `reach` controls the
|
|
433
|
-
forward stance window, `spread` the lateral foot offset, `lead` shifts the next
|
|
434
|
-
plant forward, and `fold` controls the drawn two-segment limb bend. `swingArc`
|
|
435
|
-
bows an airborne foot farther outward in the body plane.
|
|
436
|
-
|
|
437
|
-
Three signed ratios place the drawn joint. `jointBend` scales how far the joint
|
|
438
|
-
leaves the line between hip and foot: `1` bows it behind and outward, `0` puts
|
|
439
|
-
it on the line, and `-1` mirrors the bow ahead and inward. `jointLean` then
|
|
440
|
-
slides the joint longitudinally by that ratio of one limb length: positive
|
|
441
|
-
values lean joints toward the middle of the leg section, negative values away,
|
|
442
|
-
and zero preserves the bowed position. The lean grows from nothing at its
|
|
443
|
-
center to its full travel at the ends of the leg section, taken from where each
|
|
444
|
-
anchor sits, so pairs sharing an anchor lean alike and no body length scales
|
|
445
|
-
it. `jointLeanCenter` moves that center from the middle of the leg section
|
|
446
|
-
(`0`) to its head end (`-1`) or tail end (`1`); the far end then travels up to
|
|
447
|
-
twice one limb length.
|
|
448
|
-
|
|
449
|
-
`sidePhase` offsets right-foot contact by that fraction of `Math.PI`; `1` makes
|
|
450
|
-
the two sides opposite. `liftThreshold` releases a planted foot when its contact
|
|
451
|
-
falls below the threshold. Jitter samples permanent proportions and per-step
|
|
452
|
-
timing from the instance's injected random function. A jitter of zero makes each
|
|
453
|
-
pair exactly mirrored and consumes randomness without changing its geometry.
|
|
454
|
-
All positional jitter scales with the larger of `reach` and the resolved
|
|
455
|
-
spread; it never introduces a fixed world-pixel displacement.
|
|
456
|
-
|
|
457
|
-
`legs.skin.foot.scale` is the drawn foot size in px. `plantedScale` multiplies
|
|
458
|
-
it while the foot is on the ground, so feet may swell as they take weight and
|
|
459
|
-
shrink through the swing. It is a ratio, so a resized creature keeps it.
|
|
460
|
-
|
|
461
|
-
A limb is one closed outline, not a stroked line: hip, knee, foot down one side
|
|
462
|
-
and back up the other, with both sides sharing each knee vertex so a bend leaves
|
|
463
|
-
no gap. `legs.skin.limbWidth` sets its thickness in px. A fill-only `limbPaint`
|
|
464
|
-
draws it as a tinted mesh; any paint that also asks for a stroke moves the limbs
|
|
465
|
-
to a Graphics path and draws both passes, as the ribbon does. A `limbWidth` of
|
|
466
|
-
zero draws no limbs at all, leaving the feet on their own.
|
|
467
|
-
|
|
468
|
-
Ornament offsets are px, `angleDegrees` is degrees, and `recover` is per
|
|
469
|
-
second. Rates are time-based so visual motion does not depend on frame rate.
|
|
470
|
-
|
|
471
|
-
An ornament swings about its root, pivoting at its shape path's origin, so an
|
|
472
|
-
off-center path pivots off-center. Two root motions can drive the swing, and
|
|
473
|
-
`source` fades between them: at `0` the root's turning, which carries the body
|
|
474
|
-
wave; at `1` its sideways travel through the world, which carries speed,
|
|
475
|
-
thrust, and steering; between them a linear blend. Sideways travel is measured
|
|
476
|
-
against the creature's own body length, so a resized creature keeps the same
|
|
477
|
-
ornament feel.
|
|
478
|
-
|
|
479
|
-
Each remaining control owns one quality of the swing. `react` is its size:
|
|
480
|
-
`0` rides the root rigidly, positive trails the drive, negative leads it.
|
|
481
|
-
`recover` is its tempo: how fast the deflection chases the drive and returns
|
|
482
|
-
to rest, changing speed without changing size. `wobble` is its shape: `0`
|
|
483
|
-
settles without overshoot, `1` overshoots and rings. The deflection is
|
|
484
|
-
clamped to a quarter turn either side of rest after every fixed simulation
|
|
485
|
-
step.
|
|
486
|
-
|
|
487
|
-
## Rendering extensions
|
|
488
|
-
|
|
489
|
-
Schema v1 has no generic shader, effect, or extension-options object. Pixel
|
|
490
|
-
filters are renderer post-processing; particles are stateful anchored emitters.
|
|
491
|
-
Both need concrete runtime contracts before entering the descriptor. The local
|
|
492
|
-
shape, paint, and anchor rules are reusable when a typed effect is introduced
|
|
493
|
-
in a later schema version.
|
|
494
|
-
|
|
495
|
-
## Check
|
|
496
|
-
|
|
497
|
-
Run the permanent adversarial tests:
|
|
224
|
+
## Rendering
|
|
225
|
+
|
|
226
|
+
`Beefwife` extends `PIXI.Container`. Add it to a stage and render the stage;
|
|
227
|
+
the creature updates its display objects in Pixi's render pass. Its own parts
|
|
228
|
+
sit in one child container, so children you add to it keep the order you give
|
|
229
|
+
them. The library owns two inherited properties: `label` holds the
|
|
230
|
+
descriptor's `name`, and `onRender` draws the creature. Put your own label or
|
|
231
|
+
render callback on a parent.
|
|
232
|
+
|
|
233
|
+
Drawing order, back to front:
|
|
234
|
+
|
|
235
|
+
1. Feet, then legs.
|
|
236
|
+
2. `under` ornaments, in descriptor order.
|
|
237
|
+
3. The ribbon, then plates from tail to head.
|
|
238
|
+
4. `over` ornaments, in descriptor order.
|
|
239
|
+
|
|
240
|
+
Feet, plates, and ornaments are drawn from one texture sheet per look and
|
|
241
|
+
renderer. The sheet is at most 2048 × 2048 texels and each shape at most
|
|
242
|
+
1024 × 1024. When a creature's shapes do not fit, all of them draw at lower
|
|
243
|
+
detail; size and position do not change.
|
|
244
|
+
|
|
245
|
+
## Development
|
|
246
|
+
|
|
247
|
+
`beefwife.js` and `beefwife.min.js` are generated from `src/` by `npm run
|
|
248
|
+
build`; do not edit them directly. From the repository root:
|
|
498
249
|
|
|
499
250
|
```sh
|
|
500
|
-
bb descriptor
|
|
501
|
-
bb beefwife-model
|
|
502
|
-
bb beefwife-api
|
|
503
|
-
bb
|
|
251
|
+
bb descriptor # schema
|
|
252
|
+
bb beefwife-model # descriptor compilation
|
|
253
|
+
bb beefwife-api # public API
|
|
254
|
+
bb beefwife-package # build, then check the packed package
|
|
255
|
+
bb test # every headless suite
|
|
504
256
|
```
|
|
505
257
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
Together they mutate every descriptor field and nesting level, check references,
|
|
509
|
-
scope bounds, ownership, lifecycle replacement, solver invariants, rendering
|
|
510
|
-
composition, hostile JavaScript values, canonical JSON, and the canvas boundary.
|
|
258
|
+
Beefwife is released under the
|
|
259
|
+
[Mozilla Public License 2.0](https://github.com/kvmet/beefwife/blob/main/beefwife/LICENSE).
|
package/beefwife.js
CHANGED
package/beefwife.min.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Beefwife v0.1.
|
|
1
|
+
/*! Beefwife v0.1.1. Generated from beefwife/src; do not edit.
|
|
2
2
|
(c) Kristen Metcalfe, MPL-2.0. */
|
|
3
3
|
var Beefwife=(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`],P=.1,re=.15,F=.4,I=2*P*2*Math.PI*F,L=14,ie=.01,freeze=e=>!e||typeof e!=`object`||Object.isFrozen(e)?e:(Object.values(e).forEach(freeze),Object.freeze(e)),R=/* @__PURE__ */ __name((e,t,n)=>e+(t-e)*n,`lerp`),z=/* @__PURE__ */ __name((e,t,n)=>Math.max(t,Math.min(n,e)),`clamp`),profileAt=(e,t,n)=>n===1?(e.start+e.end)/2:R(e.start,e.end,t/(n-1)),spatialAngle=(e,t,n)=>t.phaseOffset-t.harmonic*n*e.phaseLagRadiansPerPixel,breathingRateFor=e=>z(re*Math.sqrt(16/e),.1,F),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*P,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:L*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,ie),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}}}})},B=Math.PI*2,positiveModulo=(e,t)=>(e%t+t)%t,Gait=class{constructor(e,t=0){this.gait=e,this.phase=positiveModulo(t,B)}advance(e,t){this.phase=positiveModulo(this.phase+B*this.gait.cyclesPerSecond*t*e,B)}_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),B)/B;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)}},V=60,H=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,V);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,H)*.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}`,U=[`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 U)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}},W=Math.PI*2,G=8,K=1.5,q=/* @__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,G)}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,G),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+W*this.model.breathing.cyclesPerSecond*e)%W,this._updateTangentsAndAxis(e);let r=I*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<G;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*K);this.axis.x+=(l/d-this.axis.x)*f,this.axis.y+=(u/d-this.axis.y)*f;let p=q(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 P=a+te,re=o+ne,F=k[e],I=A[e],L=P-a,ie=re-o,R=L*F+ie*I,z=L*-I+ie*F,B=i-w[e],V=positiveModulo(s*B+l,W)/W,H=V>=d?1:1-p*S[e]*t*Math.sin(Math.PI*V/d);N[e]=H;let U=Math.max(0,Math.min(1,H*(1-h*j[e]*t)));ee[e]=U;let G=R*(1-U*(R<0?b[e]:y[e])),K=z*(1-U*x[e]),q=a+F*G-I*K,J=o+I*G+F*K;M[e]=-((R-G)*(F*g+I*_)+(z-K)*(-I*g+F*_));let Y=positiveModulo(c*B+u,W)/W,X=Y>=f?0:m*C[e]*t*Math.sin(Math.PI*Y/f);T[e]=q+F*X*n,E[e]=J+I*X*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=q(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)),J=/* @__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=J(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,_=J(h,n.spread*a.spreadScale),v=J(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=J(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)}},Y=Object.freeze({chunkStride:4,legStride:11,ornamentStride:6,plateStride:5}),X=13,Z=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:Y},e.model=this.model;let t={chunks:this.body.chain.count*Y.chunkStride,legs:this.legs.legs.length*Y.legStride,ornaments:this.ornaments.length*Y.ornamentStride,plates:this.model.skin.platesTailFirst.length*Y.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=Y.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=Y.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=Y.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=Y.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(Z,Math.max(-Z,r.waveGain*-o/X+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>Z?(u=Z,d=0):u<-Z&&(u=-Z,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})}},ae=4,oe=14,limbIndicesFor=e=>{let t=new Uint32Array(e*6*6),n=0;for(let r=0;r<e;r++){let e=r*oe,i=e+oe-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},se=/* @__PURE__ */ new Float64Array(13),ce=/* @__PURE__ */ new Float64Array(13);for(let e=0;e<=12;e++){let t=Math.PI*e/12;se[e]=Math.cos(t),ce[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=se[l],f=ce[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/ae),D=Math.sin(T/ae),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},le=1,ue=1,de=1024,frameKeyFor=(e,t)=>`${e.path}|${t.fill}|${t.stroke}|${t.strokeWidth}`,Q=/* @__PURE__ */ new WeakMap,fe=/* @__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(`
|
|
4
4
|
`)}`,resolution:n,frames:[...r.values()],feet:a,plates:s,ornaments:c}},arrangeEntries=e=>{e.sort((e,t)=>t.height-e.height||e.key.localeCompare(t.key));let t=0,n=0,r=0,i=0;for(let a of e)t>0&&t+a.width>2048&&(t=0,n+=r,r=0),a.x=t,a.y=n,t+=a.width,r=Math.max(r,a.height),i=Math.max(i,t);return{width:i,height:n+r}},packAtlas=e=>{let t=e.resolution,n=[];try{for(let t of e.frames){let e=contextFor(t.shape,t.paint,t.scale),r={...t,context:e,requestedScale:t.scale};n.push(r);let i=e.bounds;if(r.bounds={minX:i.minX,minY:i.minY,width:i.width,height:i.height},!Object.values(r.bounds).every(Number.isFinite))throw RangeError(`atlas shape bounds must be finite`)}let r=de,i;for(;;){for(let e of n){let{bounds:n,paint:i,requestedScale:a}=e,o=i.strokeWidth*a*le*t,s=Math.max(n.width,n.height)*t,c=Math.ceil(s)+2*Math.max(ue,Math.ceil(o))<=r?1:(r-3)/(s+2*o);e.scale=a*c,e.pad=Math.max(ue,Math.ceil(o*c)),e.originX=e.pad+Math.ceil(-n.minX*c*t),e.originY=e.pad+Math.ceil(-n.minY*c*t),e.width=Math.ceil(n.width*c*t)+e.pad*2,e.height=Math.ceil(n.height*c*t)+e.pad*2}if(i=arrangeEntries(n),i.width<=2048&&i.height<=2048)break;if(r===4)throw RangeError(`too many atlas frames for the texture budget`);r=Math.max(4,Math.floor(r*.75))}for(let e of n)e.scale!==e.requestedScale&&(e.context.destroy(),e.context=null,e.context=contextFor(e.shape,e.paint,e.scale));return{resolution:t,...i,entries:n}}catch(e){for(let e of n)e.context?.destroy();throw e}},prepareAtlas=(e,t)=>{let n=fe.get(e);if(n?.renderResolution===t)return n.plan;if(!Number.isFinite(t)||t<=0)throw RangeError(`pixelResolution must be finite and positive`);let r=planAtlas(e,t),i=packAtlas(r);for(let e of i.entries)e.context.destroy();return fe.set(e,{renderResolution:t,plan:r}),r},bakeAtlas=(e,t)=>{let n=packAtlas(e),r=1/n.resolution,i=s.RenderTexture.create({width:n.width*r,height:n.height*r,resolution:n.resolution,antialias:!1,scaleMode:`nearest`}),a=/* @__PURE__ */ new Map,o=!1;try{let e=!0;for(let o of n.entries){let n=new s.Graphics(o.context);try{t.render({container:n,target:i,clear:e,transform:new s.Matrix(1,0,0,1,(o.x+o.originX)*r,(o.y+o.originY)*r)})}finally{n.destroy()}e=!1,a.set(o.key,{texture:new s.Texture({source:i.source,frame:new s.Rectangle(o.x*r,o.y*r,o.width*r,o.height*r)}),scale:o.scale,anchorX:o.originX/o.width,anchorY:o.originY/o.height})}o=!0}finally{for(let e of n.entries)e.context.destroy();if(!o){for(let e of a.values())e.texture.destroy();i.destroy(!0)}}return{key:e.key,renderer:t,target:i,frames:a}},acquireAtlas=(e,t)=>{if(!e.frames.length)return null;let n=Q.get(t);n||Q.set(t,n=/* @__PURE__ */ new Map);let r=n.get(e.key);return r||(r={atlas:bakeAtlas(e,t),uses:0},n.set(e.key,r)),r.uses++,r.atlas},releaseAtlas=e=>{let t=e&&Q.get(e.renderer),n=t&&t.get(e.key);if(!(!n||--n.uses>0)){t.delete(e.key);for(let t of e.frames.values())t.texture.destroy();e.target.source.removeAllListeners(`change`),e.target.destroy(!0)}},pe=[`feet`,`ornaments-under`,`plates`,`ornaments-over`],me={position:!0,vertex:!0,rotation:!0,uvs:!1,color:!1},HeadlessGraphics=class{static available=!1;static prepare(){}static prepareAtlas(){}},he=l*2,pixelResolutionOf=e=>{let t=e?.pixelResolution;if(t===void 0)return 1;if(!Number.isFinite(t))throw RangeError(`pixelResolution must be finite and positive`);return Math.max(u,Math.min(d,t))},afterPass=e=>queueMicrotask(e),Graphics=class{static available=!0;static prepare(e){for(let[t,n]of Object.entries(e.descriptor.definitions.shapes))try{new s.GraphicsPath(n.path)}catch(e){throw Error(`$.definitions.shapes.${t}.path: ${e.message}`)}for(let[t,n]of Object.entries(e.paints))for(let e of[`fill`,`stroke`])if(n[e]!==null)try{new s.Color(n[e])}catch(n){throw Error(`$.definitions.paints.${t}.${e===`stroke`?`stroke.colour`:e}: ${n.message}`)}}static prepareAtlas(e,t){return prepareAtlas(e,pixelResolutionOf(t))}constructor(e,t,n=null){this.host=e,this.failedBake=null,this.parent=e.addChild(new s.Container),this.model=t.model,this.options=n||{},this.footParticles=[],this.ornamentParticles=[],this.plateParticles=[],this.shapeContainers=[],this.atlas=null,this.atlasRenderer=null,this.bakeRenderer=null,this.plan=null,this.legCount=0,this.limbPositions=null,this.limbFill=null,this.limbStroke=null,this.limbCount=-1,this.ribbonPositions=null,this.ribbonFill=null,this.ribbonStroke=null,this.ribbonCount=-1,this.reportedResolution=null,this.baking=!1,this.state=null,this.adopt(t)}adopt(e){let t=pixelResolutionOf(this.options),n=prepareAtlas(e.model,t),r=e.legs.length/e.layout.legStride;this._samePlacements(n,e.model,r)?this._sameFrames(n)&&(this.plan=n):(this.plan=null,this.footParticles=[],this.ornamentParticles=[],this.plateParticles=[]),this.nextPlan=n,this.nextResolution=t,this.failedBake=null,this.model=e.model,this.legCount=r;let i=this._syncLimbParts(this.legCount);i=this._syncRibbonParts()||i,i&&this._arrange(),this.sync(e)}_samePlacements(e,t,n){let r=this.plan;if(!r||n!==this.legCount)return!1;let drawn=(e,t)=>e===null==(t===null);return drawn(r.feet,e.feet)&&r.plates.length===e.plates.length&&r.plates.every((t,n)=>drawn(t,e.plates[n]))&&r.ornaments.length===e.ornaments.length&&r.ornaments.every((n,r)=>drawn(n,e.ornaments[r])&&this.model.skin.ornaments[r].layer===t.skin.ornaments[r].layer)}_sameFrames(e){let t=this.plan;return t.key===e.key&&t.feet===e.feet&&t.plates.every((t,n)=>t===e.plates[n])&&t.ornaments.every((t,n)=>t===e.ornaments[n])}_drop(e){discard(this.parent,e)}_syncLimbParts(e){let t=this.model.legs.skin.limbPaint,n=e>0&&t.fill!==null,r=e>0&&t.stroke!==null&&t.strokeWidth>0,i=e!==this.limbCount,a=!1;return i&&(this.limbCount=e,this.limbPositions=new Float32Array(e*28)),this.limbFill&&(i||!n)&&(this._drop(this.limbFill),this.limbFill=null,a=!0),n&&!this.limbFill?(this.limbFill=meshFor(this.limbPositions,limbIndicesFor(e),t.fill),a=!0):this.limbFill&&paintMesh(this.limbFill,t.fill),this.limbStroke&&!r?(this._drop(this.limbStroke),this.limbStroke=null,a=!0):r&&!this.limbStroke&&(this.limbStroke=new s.Graphics,a=!0),a}_syncRibbonParts(){let e=this.model.skin.ribbonPaint,t=this.model.chunks.length,n=e.fill!==null,r=e.stroke!==null&&e.strokeWidth>0,i=t!==this.ribbonCount,a=!1;return i&&(this.ribbonCount=t,this.ribbonPositions=ribbonPositionsFor(t)),this.ribbonFill&&(i||!n)&&(this._drop(this.ribbonFill),this.ribbonFill=null,a=!0),n&&!this.ribbonFill?(this.ribbonFill=meshFor(this.ribbonPositions,ribbonIndicesFor(t),e.fill),a=!0):this.ribbonFill&&paintMesh(this.ribbonFill,e.fill),this.ribbonStroke&&!r?(this._drop(this.ribbonStroke),this.ribbonStroke=null,a=!0):r&&!this.ribbonStroke&&(this.ribbonStroke=new s.Graphics,a=!0),a}_arrange(){let[e,t,n,r]=this.shapeContainers;for(let i of[e,this.limbFill,this.limbStroke,t,this.ribbonFill,this.ribbonStroke,n,r])i&&this.parent.addChild(i)}_buildParticles(e){let t=[null,null,null,null],bandFor=e=>(t[e]||(t[e]=new s.ParticleContainer({label:pe[e],dynamicProperties:me})),t[e]),place=(e,t)=>{let n=t===null?null:this.atlas.frames.get(t);if(!n)return null;let r=new s.Particle({texture:n.texture,anchorX:n.anchorX,anchorY:n.anchorY});return r.bakeScale=n.scale,bandFor(e).addParticle(r),r};this.footParticles=Array.from({length:this.legCount},()=>place(0,e.feet)),this.ornamentParticles=e.ornaments.map((e,t)=>place(this.model.skin.ornaments[t].layer===`under`?1:3,e)),this.plateParticles=e.plates.map(e=>place(2,e)),this.shapeContainers=t,this._arrange()}_syncAtlas(e){let t=this._liveResolution();if(t!==this.nextResolution&&(this.nextPlan=prepareAtlas(this.model,t),this.nextResolution=t),e&&(this.plan!==this.nextPlan||this.atlasRenderer!==e)&&(this.failedBake?.plan!==this.nextPlan||this.failedBake.renderer!==e)){if(this.atlasRenderer&&this.atlasRenderer!==e)for(let e of this.shapeContainers)e&&(e.visible=!1);this.bakeRenderer=e,this.baking||(this.baking=!0,afterPass(()=>{if(this.baking=!1,!this.parent.destroyed)try{this._bake(this.bakeRenderer)}catch(e){if(this.failedBake={plan:this.nextPlan,renderer:this.bakeRenderer},!this.host.emit(`error`,e))throw e}}))}}_liveResolution(){let e=this.options.pixelResolution;try{let e=pixelResolutionOf(this.options);return this.reportedResolution=null,e}catch(t){if(!(t instanceof RangeError))throw t;let n=this.reportedResolution;return n&&Object.is(n.value,e)||(this.reportedResolution={value:e},afterPass(()=>{if(!this.parent.destroyed&&!this.host.emit(`error`,t))throw t})),this.nextResolution}}_bake(e){let t=this.nextPlan,n=acquireAtlas(t,e),r=this.atlas,i=this.shapeContainers;this.atlas=n,this.atlasRenderer=e,this.plan=t,this._buildParticles(t);let a=this._snapResolution();this._placeParticles(this.state,a,a>0?1/a:0);for(let e of i)e&&e.destroy();releaseAtlas(r)}_place(e,t,n,r,i,a,o,s,c){if(!e)return;e.x=snapCoordinate(t,s,c),e.y=snapCoordinate(n,s,c),e.rotation=Math.atan2(i,r);let l=a/e.bakeScale;e.scaleX=l,e.scaleY=l*o}_syncLimbs(e,t,n){let r=e.legs,i=this.model.legs.skin.limbWidth,a=this.limbPositions;if(this.limbStroke&&this.limbStroke.clear(),i<=0){a.fill(0),this.limbFill&&this.limbFill.positionBuffer.update();return}let o=e.layout.legStride,s=this.options.kneeProjection??null,c=s?.centerX,u=s?.centerY,d=s?.perspective;Number.isFinite(c)&&Number.isFinite(u)&&Number.isFinite(d)?(c=Math.max(-l,Math.min(l,c)),u=Math.max(-l,Math.min(l,u)),d=Math.max(0,Math.min(f,d))):d=0;let p=s?.maxOffset,m=Number.isFinite(p)?Math.max(0,Math.min(l,p)):he,h=this.model.legs.jointLean;for(let e=0;e<r.length;e+=o){let s=e/o*28,l=r[e+2],f=r[e+3];if(d>0){let t=Math.hypot(l-c,f-u);if(t>0){let n=Math.hypot(l-(r[e]+r[e+4])/2,f-(r[e+1]+r[e+5])/2),i=Math.min(m,t*n*d);l+=(l-c)/t*i,f+=(f-u)/t*i}}if(h!==0){let t=r[e+10]*h;l+=r[e+6]*t,f+=r[e+7]*t}writeLimb(a,s,r[e],r[e+1],l,f,r[e+4],r[e+5],i,t,n)}if(this.limbFill&&this.limbFill.positionBuffer.update(),!this.limbStroke)return;for(let e=0;e<a.length;e+=28){let t=a[e],n=a[e+1];this.limbStroke.moveTo(t,n);for(let r=2;r<28;r+=2){let i=a[e+r],o=a[e+r+1];(i!==t||o!==n)&&(this.limbStroke.lineTo(i,o),t=i,n=o)}this.limbStroke.closePath()}let g=this.model.legs.skin.limbPaint;this.limbStroke.stroke({color:g.stroke,width:g.strokeWidth,cap:`butt`,join:`bevel`})}_syncRibbon(e,t,n){let r=e.chunks,i=e.layout.chunkStride,a=this.model.chunks.length,o=a-1,s=this.ribbonPositions;for(let e=0;e<a;e++){let a=e*i,o=e*4,c=this.model.chunks[e].ribbonWidth;s[o]=r[a]-r[a+3]*c,s[o+1]=r[a+1]+r[a+2]*c,s[o+2]=r[a]+r[a+3]*c,s[o+3]=r[a+1]-r[a+2]*c,snapPositions(s,o,o+4,t,n)}let c=o*i,l=r[c+2],u=r[c+3];writeCap(s,a*4,r[c],r[c+1],this.model.chunks[o].ribbonWidth,-u,l,-l,-u,t,n);let d=r[2],f=r[3];if(writeCap(s,a*4+28,r[0],r[1],this.model.chunks[0].ribbonWidth,f,-d,d,f,t,n),this.ribbonFill&&this.ribbonFill.positionBuffer.update(),!this.ribbonStroke||(this.ribbonStroke.clear(),!this.model.skin.hasRibbon))return;let p=a*4+2,m=a*4+28+2;this.ribbonStroke.moveTo(s[0],s[1]);for(let e=1;e<a;e++)this.ribbonStroke.lineTo(s[e*4],s[e*4+1]);for(let e=1;e<12;e++)this.ribbonStroke.lineTo(s[p+e*2],s[p+e*2+1]);for(let e=o;e>=0;e--)this.ribbonStroke.lineTo(s[e*4+2],s[e*4+3]);for(let e=1;e<12;e++)this.ribbonStroke.lineTo(s[m+e*2],s[m+e*2+1]);this.ribbonStroke.closePath();let h=this.model.skin.ribbonPaint;this.ribbonStroke.stroke({color:h.stroke,width:h.strokeWidth})}sync(e,t=null,n=!0){if(e.model!==this.model)throw Error(`render state model does not match Beefwife graphics`);if(this.state=e,this._syncAtlas(t),!n)return;let r=this._snapResolution(),i=r>0?1/r:0;this._syncLimbs(e,r,i),this._syncRibbon(e,r,i),this._placeParticles(e,r,i)}_snapResolution(){return this.options.roundVertices===!0?this.nextResolution:0}_placeParticles(e,t,n){let r=e.legs;for(let i=0;i<this.footParticles.length;i++){let a=i*e.layout.legStride;this._place(this.footParticles[i],r[a+4],r[a+5],r[a+6],r[a+7],r[a+8],r[a+9],t,n)}let i=e.ornaments;for(let r=0;r<this.ornamentParticles.length;r++){let a=r*e.layout.ornamentStride;this._place(this.ornamentParticles[r],i[a],i[a+1],i[a+2],i[a+3],i[a+4],i[a+5],t,n)}let a=e.plates;for(let r=0;r<this.plateParticles.length;r++){let i=r*e.layout.plateStride;this._place(this.plateParticles[r],a[i],a[i+1],a[i+2],a[i+3],a[i+4],1,t,n)}}destroy(){let e=[...this.shapeContainers,this.limbFill,this.limbStroke,this.ribbonFill,this.ribbonStroke];for(let t of e)t&&discard(this.parent,t);releaseAtlas(this.atlas),this.atlas=null,this.parent.destroy()}},$=c?Graphics:HeadlessGraphics,HeadlessContainer=class{destroy(){this.destroyed=!0}},ge=c?s.Container:HeadlessContainer,_e=/* @__PURE__ */ new WeakMap,freezeDeep=e=>!e||typeof e!=`object`?e:(Object.values(e).forEach(freezeDeep),Object.freeze(e)),modelFor=e=>{let t=_e.get(e);if(t)return t;let n=compile(e);return $.prepare(n),_e.set(e,n),freezeDeep(e),n},ve=Math.PI*2,ye=/* @__PURE__ */ new Set([`position`,`direction`,`phase`,`random`,`render`]),be=/* @__PURE__ */ new Set([`roundVertices`,`pixelResolution`,`kneeProjection`]),xe=/* @__PURE__ */ new Set([`centerX`,`centerY`,`perspective`,`maxOffset`]),Se=/* @__PURE__ */ new Set([`position`,`direction`,`phase`]),Ce=/* @__PURE__ */ new Set([`throttle`,`direction`]),plainObject=(e,t)=>{if(!e||typeof e!=`object`||Array.isArray(e))throw TypeError(`${t} must be an object`);let n=Object.getPrototypeOf(e);if(n!==Object.prototype&&n!==null)throw TypeError(`${t} must be a plain object`);return e},optionsOf=(e,t,n)=>{if(e===void 0)return{};let r=plainObject(e,n);for(let e in r)if(!t.has(e))throw TypeError(`${n}.${e} is unknown`);return r},defaulted=(e,t)=>e===void 0?t:e,finite=(e,t)=>{if(typeof e!=`number`||!Number.isFinite(e))throw TypeError(`${t} must be a finite number`);return e},renderOptionsOf=e=>{if(e===void 0)return null;let t=optionsOf(e,be,`options.render`);if(t.roundVertices!==void 0&&typeof t.roundVertices!=`boolean`)throw TypeError(`options.render.roundVertices must be a boolean`);if(t.pixelResolution!==void 0){let e=finite(t.pixelResolution,`options.render.pixelResolution`);if(e<=0)throw RangeError(`options.render.pixelResolution must be positive`);if(e<1e-6||e>1e6)throw RangeError(`options.render.pixelResolution must be from ${u} to ${d}`)}let n=t.kneeProjection;if(n!=null){optionsOf(n,xe,`options.render.kneeProjection`),worldPoint({x:n.centerX,y:n.centerY},null,`options.render.kneeProjection.center`);let e=finite(n.perspective,`options.render.kneeProjection.perspective`);if(e<0)throw RangeError(`options.render.kneeProjection.perspective must be nonnegative`);if(e>1e6)throw RangeError(`options.render.kneeProjection.perspective must be at most ${f}`);if(n.maxOffset!==void 0){let e=finite(n.maxOffset,`options.render.kneeProjection.maxOffset`);if(e<0)throw RangeError(`options.render.kneeProjection.maxOffset must be nonnegative`);if(e>1e9)throw RangeError(`options.render.kneeProjection.maxOffset must be at most ${l}`)}}return t},point=(e,t,n)=>{if(e===void 0){if(t===null)throw TypeError(`${n} is required`);return{...t}}let r=plainObject(e,n);for(let e in r)if(e!==`x`&&e!==`y`)throw TypeError(`${n}.${e} is unknown`);return{x:finite(r.x,`${n}.x`),y:finite(r.y,`${n}.y`)}},worldPoint=(e,t,n)=>{let r=point(e,t,n);if(Math.abs(r.x)>1e9||Math.abs(r.y)>1e9)throw RangeError(`${n} coordinates must be from ${-l} to ${l}`);return r},directionInto=(e,t,n,r)=>{let i=e===void 0?t:plainObject(e,n);if(e!==void 0){for(let e in i)if(e!==`x`&&e!==`y`)throw TypeError(`${n}.${e} is unknown`)}let a=finite(i.x,`${n}.x`),o=finite(i.y,`${n}.y`),s=Math.max(Math.abs(a),Math.abs(o));if(!s)throw RangeError(`${n} must be nonzero`);let c=a/s,l=o/s,u=Math.hypot(c,l);return r.x=c/u,r.y=l/u,r},direction=(e,t,n)=>directionInto(e,t,n,{}),newPose=()=>({head:{x:0,y:0},center:{x:0,y:0},direction:{x:0,y:0}}),bodyFitsWorld=e=>e.chain.fitsTranslation({x:0,y:0},l),sameTopology=(e,t)=>[`head`,`trunk`,`tail`].every(n=>e.sections[n].count===t.sections[n].count),legCountKey=e=>e.descriptor.legs.pairs,ornamentKey=e=>e.skin.ornaments.map(e=>`${e.id}:${e.side}`).join(`|`),Beefwife=class extends ge{#e;#t;#n;#r;#i;#a;#o;#s=null;#c=!1;#l=null;#u=null;#d=newPose();#f;#p;#m=e=>{this.#f=this.#p,this.#a.update(e,this.#p),this.#o.update(e)};constructor(e,t){super();let n=optionsOf(t,ye,`options`),r=worldPoint(n.position,{x:0,y:0},`options.position`),i=direction(n.direction,{x:1,y:0},`options.direction`),a=finite(defaulted(n.phase,0),`options.phase`);if(n.random!==void 0&&typeof n.random!=`function`)throw TypeError(`options.random must be a function`);this.#e=n.random??Math.random,this.#l=renderOptionsOf(n.render),this.#t={...i},this.#n=modelFor(e),$.prepareAtlas(this.#n,this.#l),this.#r=new Gait(this.#n.gait,a);let o=this.#n.breathing.strain?ve*this.#g():this.#r.phase;if(this.#i=new Body(this.#n,this.#r,o),this.#i.place(r,i),!bodyFitsWorld(this.#i))throw RangeError(`options.position places the body outside the world`);this.#a=new Legs(this.#n,this.#i,this.#r,()=>this.#g()),this.#f=1,this.#p=1,this.#o=new Skin(this.#n,this.#i,this.#a),this.#_(),this.label=this.#n.descriptor.name,this.onRender=$.available?e=>this.#y(e):null,this.#v()}get descriptor(){return this.#n.descriptor}get restLength(){return this.#n.restLength}step(e,t){this.#h(`step`);let n=finite(e,`dt`);if(n<0)throw RangeError(`dt must be nonnegative`);let r=optionsOf(t,Ce,`controls`),i=finite(defaulted(r.throttle,1),`controls.throttle`);if(i<0||i>1)throw RangeError(`controls.throttle must be from 0 to 1`);let a=directionInto(r.direction,this.#t,`controls.direction`,this.#t);if(this.#p=i,this.#i.step(n,i,a,this.#m)){let e=this.#i.chain.worldCorrection(l);(e.x||e.y)&&(this.#i.chain.translate(e),this.#a.translate(e),this.#o.translate(e)),this.#_()}}setDescriptor(e){this.#h(`setDescriptor`);let t=modelFor(e);$.prepareAtlas(t,this.#l);let n=new Gait(t.gait,this.#r.phase),r=!this.#n.breathing.strain&&t.breathing.strain?ve*this.#g():this.#i.breathingPhase,i=sameTopology(this.#n,t),a=this.#i;if(!i&&(a=new Body(t,n,r),a.adopt(this.#i),a.refreshContacts(this.#f),!bodyFitsWorld(a)))throw RangeError(`descriptor places the body outside the world`);let o=legCountKey(this.#n)===legCountKey(t)?null:new Legs(t,a,n,()=>this.#g()),s=ornamentKey(this.#n)===ornamentKey(t)?null:new Skin(t,a,o||this.#a);i?this.#i.reconfigure(t,n,this.#f,r):this.#i=a,o?this.#a=o:this.#a.reconfigure(t,this.#i,n),s?this.#o=s:this.#o.reconfigure(t,this.#i,this.#a),this.#n=t,this.#r=n,this.#_(),this.label=t.descriptor.name,this.#v()}reset(e){this.#h(`reset`);let t=optionsOf(e,Se,`options`),n=this.#i.getPose(this.#d),r=worldPoint(t.position,n.head,`options.position`),i=direction(t.direction,n.direction,`options.direction`),a=finite(defaulted(t.phase,this.#r.phase),`options.phase`),o=new Gait(this.#n.gait,a),s=this.#n.breathing.strain?ve*this.#g():o.phase,c=new Body(this.#n,o,s);if(c.place(r,i),!bodyFitsWorld(c))throw RangeError(`options.position places the body outside the world`);let l=new Legs(this.#n,c,o,()=>this.#g()),u=new Skin(this.#n,c,l);t.direction!==void 0&&(this.#t={...i}),this.#r=o,this.#i=c,this.#a=l,this.#o=u,this.#f=1,this.#_(),this.#s&&this.#y()}translate(e){this.#h(`translate`);let t=worldPoint(e,null,`offset`);if(!this.#i.chain.fitsTranslation(t,1e9))throw RangeError(`offset places the body outside the world`);this.#i.chain.translate(t),this.#a.translate(t),this.#o.translate(t),this.#_()}getPose(){return this.#d}getBendResponse(e){return this.#h(`getBendResponse`),this.#i.bend.response(this.#i.chain,e)}destroy(e){this.onRender=null,this.#s&&=(this.#s.destroy(),null),this.#c=!0,super.destroy(e)}#h(e){if(this.#c)throw Error(`${e} was called on a destroyed beefwife`)}#g(){let e=this.#e();if(typeof e!=`number`||!Number.isFinite(e))throw TypeError(`random() must return a finite number`);if(e<0||e>=1)throw RangeError(`random() must return a number from 0 to 1`);return e}#_(){this.#i.getPose(this.#d)}#v(){this.#u=this.#o.writeRenderState(this.#u),this.#s?this.#s.adopt(this.#u):$.available&&(this.#s=new $(this,this.#u,this.#l))}#y(e=null){let t=this.visible&&this.renderable;t&&(this.#u=this.#o.writeRenderState(this.#u)),this.#s.sync(this.#u,e,t)}};return Object.defineProperty(Beefwife,"MAX_WORLD_COORDINATE",{value:l,enumerable:!0}),Object.defineProperty(Beefwife,"Descriptor",{value:ee,enumerable:!0}),Beefwife})(globalThis.PIXI);typeof module<`u`&&module.exports&&(module.exports=Beefwife);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kvmet/beefwife",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Procedurally animated chain-body creatures for the web.",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"author": "Kristen Metcalfe",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"files": [
|
|
41
41
|
"CHANGELOG.md",
|
|
42
|
+
"DESCRIPTOR.md",
|
|
42
43
|
"LICENSE",
|
|
43
44
|
"README.md",
|
|
44
45
|
"beefwife.example.json",
|