@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 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
- `src/schema.mjs` holds the schema-v1 tree, the authority on a beefwife's
4
- portable JSON contract, and `src/descriptor.mjs` reads, resizes, and reports
5
- bounds against it. Version 1 deliberately accepts no older descriptor shapes or
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
- `name` is the portable identity and JSON filename stem: a lowercase slug of up
10
- to 64 letters, digits, and hyphens. Schema v1 does not mix display copy into the
11
- runtime descriptor.
7
+ ```js
8
+ import { Beefwife, Descriptor } from "@kvmet/beefwife";
12
9
 
13
- The runtime modules consume only schema v1. Callers use `Beefwife`; the model,
14
- gait, body, legs, and skin modules are implementation boundaries.
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
- `beefwife.js` and `beefwife.min.js` are generated from `src` by `bb
17
- beefwife-build`; edit the sources, never the artifacts.
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
- ## Installing
25
+ ## Install
20
26
 
21
27
  ```sh
22
28
  npm install @kvmet/beefwife pixi.js
23
29
  ```
24
30
 
25
- Pixi is a peer dependency. The module entry imports it, so a bundler resolves
26
- one copy for the page and the library shares it:
31
+ Or load the classic script after Pixi:
27
32
 
28
- ```js
29
- import { Beefwife, Descriptor } from "@kvmet/beefwife";
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
- ## Renderer support
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
- Beefwife supports headless simulation and both Pixi renderers, WebGL and
35
- WebGPU. The pixi.js peer dependency requires at least 8.21.0: in earlier 8.x
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
- The classic-script build reads `window.PIXI` instead and adds one global,
39
- `Beefwife`, with the JSON half hanging off it as `Beefwife.Descriptor`. Where
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` loads the classic-script build and `import` loads the module
45
- entry. The two are separate copies with separate `Beefwife` classes, so
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
- ## Public runtime contract
53
+ ### Samples
49
54
 
50
- The library exposes `Beefwife` for one live Pixi display object and `Descriptor`
51
- for plain JSON. Physics, gait, legs, skin, render snapshots, caches, and
52
- fixed-step state remain private.
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
- ```js
55
- import { Beefwife, Descriptor } from "@kvmet/beefwife";
59
+ ```text
60
+ https://cdn.jsdelivr.net/npm/@kvmet/beefwife@0.1.1/samples/slow-guy.json
61
+ ```
56
62
 
57
- const chevronGuy = Descriptor.parse(text);
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
- beefwife.step(dt, { throttle: 1, direction }); // dt is seconds
76
- app.stage.addChild(beefwife); // Pixi refreshes it when the stage renders
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
- The constructor options object and each of its fields are optional. Position
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
- ```html
215
- <script src="https://cdn.jsdelivr.net/npm/pixi.js@8.23.0/dist/pixi.min.js"></script>
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
- ```js
220
- const chevronGuy = Beefwife.Descriptor.parse(text);
221
- const textAgain = Beefwife.Descriptor.stringify(chevronGuy);
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
- `read(value)` validates an already parsed value. It returns a deep-owned plain
225
- JSON value with fixed fields and definition keys in canonical order. Unknown
226
- keys, omitted fields, non-plain objects, invalid references, and values outside
227
- their declared bounds are errors.
228
-
229
- Feet, plates, and ornaments share one texture sheet per distinct appearance
230
- and renderer. The sheet is capped at 2048 × 2048 texels, with each frame capped
231
- at 1024 × 1024. When the frames together exceed the sheet budget, the frame cap
232
- is reduced until they fit. Shapes below that cap retain their requested detail.
233
- Large or complex creatures can therefore look coarser while keeping their world
234
- size, attachment positions, drawing order, and particle batches. Repeated shapes
235
- with the same paint share a frame.
236
-
237
- With Pixi loaded, construction and `setDescriptor` also validate shape bounds.
238
- A rejected edit leaves the current creature intact. Texture rendering failures
239
- emit `error` on the Beefwife; without a listener, the error is thrown. A failed bake is retried
240
- only after an edit, a resolution change, or a renderer change.
241
-
242
- Descriptor px are world pixels, and `scale(descriptor, factor)` is the one way
243
- to resize a creature. It returns a new descriptor with every
244
- length-dimensioned field transformed (px multiply, radians-per-px divide), so
245
- the pose trace scales by the factor with timing and feel unchanged. A product
246
- outside its field's bounds is an error, not a clamp. Shape outlines grow with
247
- the placement scale. Ribbon and limb outline widths grow directly; a paint
248
- shared with a shape receives a separate shape entry when scaled.
249
-
250
- `bounds(path)` reports what the schema enforces for one field, so an editor can
251
- read a range from the schema that checks it instead of keeping a second copy.
252
- Paths name fields with dots, an array item with `[]`, and a record entry with
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 reports `nullable`. Numbers add `min`, `max`, and `integer`; strings
265
- add `minLength`, `maxLength`, `pattern`, and `blankAllowed`; choices add
266
- `values`; objects add `fields`; records add `minEntries`, `maxEntries`, and
267
- `keyPattern`; arrays add `maxLength`. Each call returns fresh `pattern` and
268
- `keyPattern` objects, so editing one cannot change what `read` enforces.
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
- A channel reads `harmonic * (phase - distance * lag) + phaseOffset`, with
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
- ```js
392
- { section: null, from: "head", offset: 1 } // whole chain
393
- { section: "tail", from: "tail", offset: 0 }
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
- Offsets are zero-based from the named end. Repetition walks away from that end:
208
+ ## Edits keep the creature
397
209
 
398
- ```js
399
- { count: 4, step: 1 }
400
- { count: null, step: 1 } // through the rest of this anchor's scope
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
- Placements that leave their scope are invalid; they are never clamped.
404
-
405
- Only one plate may resolve onto a chunk. `chain.skin.loadScale` then grows
406
- plates with grip: a plate's drawn scale is multiplied by
407
- `1 + loadScale * contact`, so a chunk under full contact draws `1 + loadScale`
408
- times its resting size and a negative value shrinks it instead. `-1` is the
409
- floor because below it a fully gripped plate would draw mirrored.
410
-
411
- Ornaments accumulate, may use the same anchor, and retain descriptor order.
412
- Drawing order is:
413
-
414
- 1. `under` ornaments in descriptor order;
415
- 2. ribbon and plates;
416
- 3. `over` ornaments in descriptor order.
417
-
418
- Mirrored and repeated ornaments share shape and paint definitions, but every
419
- expanded ornament owns independent runtime motion. Schema v1 admits at most 512
420
- expanded ornaments per beefwife.
421
-
422
- ## Legs and secondary motion
423
-
424
- Leg pairs are distributed over their named section by resting distance.
425
- `reach` is px; `spread` and `swingArc` are fractions of `reach`.
426
- `swingCycles` is airborne time per step in gait cycles, so feet keep step with
427
- the shared clock; a stopped clock resolves against a floor of 0.01 cycles per
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 canvas-runtime
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
- Run the complete headless suite with `bb test`.
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
@@ -1,4 +1,4 @@
1
- /*! Beefwife v0.1.0. Generated from beefwife/src; do not edit.
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(pixi_js) {
4
4
 
package/beefwife.min.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! Beefwife v0.1.0. Generated from beefwife/src; do not edit.
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.0",
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",