@kvmet/beefwife 0.0.0-stage → 0.1.0

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/README.md CHANGED
@@ -1,3 +1,510 @@
1
- # Temporary Holding Version
1
+ # Beefwife
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
8
+
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.
12
+
13
+ The runtime modules consume only schema v1. Callers use `Beefwife`; the model,
14
+ gait, body, legs, and skin modules are implementation boundaries.
15
+
16
+ `beefwife.js` and `beefwife.min.js` are generated from `src` by `bb
17
+ beefwife-build`; edit the sources, never the artifacts.
18
+
19
+ ## Installing
20
+
21
+ ```sh
22
+ npm install @kvmet/beefwife pixi.js
23
+ ```
24
+
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:
27
+
28
+ ```js
29
+ import { Beefwife, Descriptor } from "@kvmet/beefwife";
30
+ ```
31
+
32
+ ## Renderer support
33
+
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.
37
+
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.
43
+
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.
47
+
48
+ ## Public runtime contract
49
+
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.
53
+
54
+ ```js
55
+ import { Beefwife, Descriptor } from "@kvmet/beefwife";
56
+
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
+ });
74
+
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
+ ```
82
+
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.
213
+
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
+ ```
218
+
219
+ ```js
220
+ const chevronGuy = Beefwife.Descriptor.parse(text);
221
+ const textAgain = Beefwife.Descriptor.stringify(chevronGuy);
222
+ ```
223
+
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
+ `*`:
254
+
255
+ ```js
256
+ Descriptor.bounds("legs.pairs");
257
+ // { kind: "number", min: 0, max: 128, integer: true, nullable: false }
258
+ Descriptor.bounds("chain.skin.ornaments[].side");
259
+ // { kind: "choice", values: ["left", "right", "both"], nullable: false }
260
+ Descriptor.bounds("definitions.paints.*.stroke.width");
261
+ // { kind: "number", min: 0, max: 1000, integer: false, nullable: false }
262
+ ```
263
+
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.
377
+
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:
390
+
391
+ ```js
392
+ { section: null, from: "head", offset: 1 } // whole chain
393
+ { section: "tail", from: "tail", offset: 0 }
394
+ ```
395
+
396
+ Offsets are zero-based from the named end. Repetition walks away from that end:
397
+
398
+ ```js
399
+ { count: 4, step: 1 }
400
+ { count: null, step: 1 } // through the rest of this anchor's scope
401
+ ```
402
+
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:
498
+
499
+ ```sh
500
+ bb descriptor
501
+ bb beefwife-model
502
+ bb beefwife-api
503
+ bb canvas-runtime
504
+ ```
505
+
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.