@kvmet/beefwife 0.0.0-stage → 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/README.md CHANGED
@@ -1,3 +1,259 @@
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
+ 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.
6
+
7
+ ```js
8
+ import { Beefwife, Descriptor } from "@kvmet/beefwife";
9
+
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
+ ```
20
+
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).
24
+
25
+ ## Install
26
+
27
+ ```sh
28
+ npm install @kvmet/beefwife pixi.js
29
+ ```
30
+
31
+ Or load the classic script after Pixi:
32
+
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>
36
+ ```
37
+
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. |
43
+
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.
46
+
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.
49
+
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.
52
+
53
+ ### Samples
54
+
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:
58
+
59
+ ```text
60
+ https://cdn.jsdelivr.net/npm/@kvmet/beefwife@0.1.1/samples/slow-guy.json
61
+ ```
62
+
63
+ ## API
64
+
65
+ All coordinates are world pixels and all angles are radians. Invalid input
66
+ throws and changes nothing; no value is clamped.
67
+
68
+ ### `new Beefwife(descriptor, options = {})`
69
+
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.
72
+
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.
120
+
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:
185
+
186
+ ```js
187
+ Descriptor.bounds("legs.pairs");
188
+ // { kind: "number", min: 0, max: 128, integer: true, nullable: false }
189
+ Descriptor.bounds("chain.skin.ornaments[].side");
190
+ // { kind: "choice", values: ["left", "right", "both"], nullable: false }
191
+ Descriptor.bounds("definitions.paints.*.stroke.width");
192
+ // { kind: "number", min: 0, max: 1000, integer: false, nullable: false }
193
+ ```
194
+
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.
200
+
201
+ ## Descriptors are values
202
+
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`.
207
+
208
+ ## Edits keep the creature
209
+
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.
223
+
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:
249
+
250
+ ```sh
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
256
+ ```
257
+
258
+ Beefwife is released under the
259
+ [Mozilla Public License 2.0](https://github.com/kvmet/beefwife/blob/main/beefwife/LICENSE).
@@ -0,0 +1,260 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "name": "beefwife",
4
+ "definitions": {
5
+ "materials": {
6
+ "body": {
7
+ "velocityRetention": 0.06928872574536875,
8
+ "jointCorrection": 0.5,
9
+ "linkCorrection": 0.6,
10
+ "grip": {
11
+ "forward": 0.05,
12
+ "backward": 0.62,
13
+ "lateral": 0.91
14
+ }
15
+ }
16
+ },
17
+ "shapes": {
18
+ "bodyPlate": {
19
+ "path": "M -4 -4 L 0 -5.5 L 4.5 -4 L 6 0 L 4.5 4 L 0 5 L -4 3.5 L -5.5 0 Z"
20
+ },
21
+ "eye": {
22
+ "path": "M -2 -2 L 0 -3 L 2.5 -2 L 2.5 0.5 L 0 1.5 L -2.5 0 Z"
23
+ },
24
+ "foot": {
25
+ "path": "M -1.2 -0.65 L 0.65 -0.9 L 1.2 -0.35 L 1.2 0.35 L 0.65 0.9 L -1.2 0.65 Z"
26
+ },
27
+ "headPlate": {
28
+ "path": "M -5.5 -2 L 2 -2 L 3.5 -1 L 3.5 1 L 2 2 L -5.5 2 Z"
29
+ }
30
+ },
31
+ "paints": {
32
+ "eye": {
33
+ "fill": "#050505",
34
+ "stroke": {
35
+ "colour": "#ffffff",
36
+ "width": 3.1
37
+ }
38
+ },
39
+ "foot": {
40
+ "fill": "#5b6b7a",
41
+ "stroke": {
42
+ "colour": "#ff0000",
43
+ "width": 3.1
44
+ }
45
+ },
46
+ "leg": {
47
+ "fill": "#39424e",
48
+ "stroke": null
49
+ },
50
+ "ribbon": {
51
+ "fill": "#7a1414",
52
+ "stroke": null
53
+ },
54
+ "shell": {
55
+ "fill": "#ff0000",
56
+ "stroke": {
57
+ "colour": "#ff0000",
58
+ "width": 3.1
59
+ }
60
+ }
61
+ }
62
+ },
63
+ "gait": {
64
+ "cyclesPerSecond": 1.35,
65
+ "phaseLagRadiansPerPixel": 0.0391666666667,
66
+ "bend": {
67
+ "amplitude": 0.84,
68
+ "harmonic": 1
69
+ },
70
+ "thrust": {
71
+ "acceleration": 0,
72
+ "harmonic": 1,
73
+ "phaseOffset": 0.36,
74
+ "dutyCycle": 0.5
75
+ },
76
+ "gather": {
77
+ "amplitude": 0.12,
78
+ "harmonic": 2,
79
+ "phaseOffset": 0
80
+ },
81
+ "contact": {
82
+ "amplitude": 0.97,
83
+ "harmonic": 1,
84
+ "phaseOffset": -0.25,
85
+ "dutyCycle": 0.5
86
+ }
87
+ },
88
+ "chain": {
89
+ "physics": {
90
+ "autoLift": {
91
+ "amount": 0.59,
92
+ "share": 0.4,
93
+ "rate": 12
94
+ },
95
+ "steering": {
96
+ "gain": 0.6,
97
+ "limit": 0.5,
98
+ "rate": 6
99
+ }
100
+ },
101
+ "breathing": 0,
102
+ "sections": {
103
+ "head": {
104
+ "chunks": 2,
105
+ "spacing": 12,
106
+ "material": "body",
107
+ "motionScale": {
108
+ "bend": 0,
109
+ "thrust": 1,
110
+ "gather": 1,
111
+ "contact": 1
112
+ },
113
+ "profile": {
114
+ "ribbonWidth": {
115
+ "start": 2.5,
116
+ "end": 4.5
117
+ },
118
+ "plateScale": {
119
+ "start": 1,
120
+ "end": 1
121
+ }
122
+ }
123
+ },
124
+ "trunk": {
125
+ "chunks": 16,
126
+ "spacing": 12,
127
+ "material": "body",
128
+ "motionScale": {
129
+ "bend": 1,
130
+ "thrust": 1,
131
+ "gather": 1,
132
+ "contact": 1
133
+ },
134
+ "profile": {
135
+ "ribbonWidth": {
136
+ "start": 4.5,
137
+ "end": 4.5
138
+ },
139
+ "plateScale": {
140
+ "start": 1,
141
+ "end": 1
142
+ }
143
+ }
144
+ },
145
+ "tail": {
146
+ "chunks": 9,
147
+ "spacing": 12,
148
+ "material": "body",
149
+ "motionScale": {
150
+ "bend": 1,
151
+ "thrust": 1,
152
+ "gather": 1,
153
+ "contact": 1
154
+ },
155
+ "profile": {
156
+ "ribbonWidth": {
157
+ "start": 4.5,
158
+ "end": 0
159
+ },
160
+ "plateScale": {
161
+ "start": 1,
162
+ "end": 0.55
163
+ }
164
+ }
165
+ }
166
+ },
167
+ "skin": {
168
+ "loadScale": 0.35,
169
+ "ribbon": {
170
+ "paint": "ribbon"
171
+ },
172
+ "plates": [
173
+ {
174
+ "id": "head-shell",
175
+ "shape": "headPlate",
176
+ "paint": "shell",
177
+ "at": {
178
+ "section": "head",
179
+ "from": "head",
180
+ "offset": 0
181
+ },
182
+ "repeat": {
183
+ "count": 1,
184
+ "step": 1
185
+ },
186
+ "scale": 1
187
+ },
188
+ {
189
+ "id": "body-shell",
190
+ "shape": "bodyPlate",
191
+ "paint": "shell",
192
+ "at": {
193
+ "section": null,
194
+ "from": "head",
195
+ "offset": 1
196
+ },
197
+ "repeat": {
198
+ "count": null,
199
+ "step": 1
200
+ },
201
+ "scale": 1
202
+ }
203
+ ],
204
+ "ornaments": [
205
+ {
206
+ "id": "eyes",
207
+ "shape": "eye",
208
+ "paint": "eye",
209
+ "at": {
210
+ "section": "head",
211
+ "from": "head",
212
+ "offset": 1
213
+ },
214
+ "repeat": {
215
+ "count": 1,
216
+ "step": 1
217
+ },
218
+ "side": "both",
219
+ "layer": "over",
220
+ "offset": {
221
+ "forward": 0,
222
+ "outward": 8
223
+ },
224
+ "angleDegrees": 52,
225
+ "scale": 1,
226
+ "source": 0.4,
227
+ "react": 1,
228
+ "recover": 30,
229
+ "wobble": 0.85
230
+ }
231
+ ]
232
+ }
233
+ },
234
+ "legs": {
235
+ "section": "trunk",
236
+ "pairs": 0,
237
+ "reach": 17,
238
+ "spread": 0.78,
239
+ "lead": 0.5,
240
+ "fold": 0.5,
241
+ "jointBend": 1,
242
+ "jointLean": 0,
243
+ "jointLeanCenter": 0,
244
+ "sidePhase": 1,
245
+ "liftThreshold": 0.45,
246
+ "swingCycles": 0.21600000000000003,
247
+ "swingArc": 0.3,
248
+ "jitter": 0.3,
249
+ "skin": {
250
+ "limbPaint": "leg",
251
+ "limbWidth": 2.4,
252
+ "foot": {
253
+ "shape": "foot",
254
+ "paint": "foot",
255
+ "scale": 3,
256
+ "plantedScale": 1.35
257
+ }
258
+ }
259
+ }
260
+ }