@fungi.computer/sprite-zdog 0.0.0-stage → 0.0.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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -2
  3. package/dist/agent-proof/moth.d.ts +50 -0
  4. package/dist/agent-proof/moth.js +52 -0
  5. package/dist/author.d.ts +217 -0
  6. package/dist/author.js +372 -0
  7. package/dist/avatar.d.ts +69 -0
  8. package/dist/avatar.js +40 -0
  9. package/dist/builtins/base-face.d.ts +290 -0
  10. package/dist/builtins/base-face.js +615 -0
  11. package/dist/builtins/base.d.ts +331 -0
  12. package/dist/builtins/base.js +579 -0
  13. package/dist/builtins/burger-input.d.ts +310 -0
  14. package/dist/builtins/burger-input.js +517 -0
  15. package/dist/builtins/burger.d.ts +305 -0
  16. package/dist/builtins/burger.js +4 -0
  17. package/dist/builtins/cat-input.d.ts +316 -0
  18. package/dist/builtins/cat-input.js +255 -0
  19. package/dist/builtins/cat.d.ts +311 -0
  20. package/dist/builtins/cat.js +4 -0
  21. package/dist/builtins/index.d.ts +4 -0
  22. package/dist/builtins/index.js +4 -0
  23. package/dist/builtins/mushroom-input.d.ts +314 -0
  24. package/dist/builtins/mushroom-input.js +94 -0
  25. package/dist/builtins/mushroom.d.ts +310 -0
  26. package/dist/builtins/mushroom.js +4 -0
  27. package/dist/burger.d.ts +9 -0
  28. package/dist/burger.js +15 -0
  29. package/dist/cat.d.ts +9 -0
  30. package/dist/cat.js +15 -0
  31. package/dist/catalog.d.ts +3 -0
  32. package/dist/catalog.js +63 -0
  33. package/dist/document.d.ts +444 -0
  34. package/dist/document.js +204 -0
  35. package/dist/index.d.ts +10 -0
  36. package/dist/index.js +10 -0
  37. package/dist/internal.d.ts +41 -0
  38. package/dist/internal.js +21 -0
  39. package/dist/mushroom.character.json +1930 -0
  40. package/dist/mushroom.d.ts +4 -0
  41. package/dist/mushroom.js +9 -0
  42. package/dist/palette.d.ts +3 -0
  43. package/dist/palette.js +5 -0
  44. package/dist/portable.d.ts +4 -0
  45. package/dist/portable.js +61 -0
  46. package/dist/renderer.d.ts +4 -0
  47. package/dist/renderer.js +106 -0
  48. package/dist/zdog-runtime.d.ts +21 -0
  49. package/dist/zdog-runtime.js +622 -0
  50. package/package.json +112 -3
package/dist/author.js ADDED
@@ -0,0 +1,372 @@
1
+ import { COLOR_PREFIX, asInternalNode, getDefinitionInternals, registerInternalNode, setDefinitionInternals, } from "./internal.js";
2
+ const sceneNodeBrand = Symbol("Sprite.SceneNode");
3
+ const finite = (value, label) => {
4
+ if (!Number.isFinite(value))
5
+ throw new Error(`Sprite ${label} must be finite`);
6
+ };
7
+ const validateTransform = (value, label) => {
8
+ if (!value)
9
+ return;
10
+ for (const axis of ["x", "y", "z"]) {
11
+ const coordinate = value[axis];
12
+ if (coordinate !== undefined)
13
+ finite(coordinate, `${label}.${axis}`);
14
+ }
15
+ };
16
+ const validateMotion = (motion, label) => {
17
+ if (!motion.target.trim())
18
+ throw new Error(`Sprite ${label}.target cannot be empty`);
19
+ validateTransform(motion.translate, `${label}.translate`);
20
+ validateTransform(motion.rotate, `${label}.rotate`);
21
+ if (motion.durationMs !== undefined) {
22
+ finite(motion.durationMs, `${label}.durationMs`);
23
+ if (motion.durationMs < 0)
24
+ throw new Error(`Sprite ${label}.durationMs cannot be negative`);
25
+ }
26
+ };
27
+ function isSnapshotArray(value) {
28
+ return Array.isArray(value);
29
+ }
30
+ function cloneAndFreeze(value, seen = new WeakMap()) {
31
+ if (value === null ||
32
+ !(value instanceof Object) ||
33
+ value instanceof Function) {
34
+ return value;
35
+ }
36
+ const key = value;
37
+ const prior = seen.get(key);
38
+ if (prior !== undefined)
39
+ return prior;
40
+ if (isSnapshotArray(value)) {
41
+ const copy = value.map((entry) => cloneAndFreeze(entry, seen));
42
+ seen.set(key, copy);
43
+ return Object.freeze(copy);
44
+ }
45
+ const copy = {};
46
+ seen.set(key, copy);
47
+ for (const [property, entry] of Object.entries(value)) {
48
+ copy[property] = cloneAndFreeze(entry, seen);
49
+ }
50
+ return Object.freeze(copy);
51
+ }
52
+ const freezeValue = (value, seen = new Set()) => {
53
+ if (value === null ||
54
+ value === undefined ||
55
+ !isSnapshotObject(value) ||
56
+ seen.has(value))
57
+ return;
58
+ const key = value;
59
+ seen.add(key);
60
+ const entries = Object.values(value);
61
+ for (const entry of entries) {
62
+ freezeValue(entry, seen);
63
+ }
64
+ Object.freeze(value);
65
+ };
66
+ const isSnapshotObject = (value) => Object(value) === value && !(value instanceof Function);
67
+ const freezeScene = (root) => {
68
+ const visited = new Set();
69
+ const visit = (node) => {
70
+ if (visited.has(node))
71
+ return;
72
+ visited.add(node);
73
+ for (const child of node.children)
74
+ visit(child);
75
+ freezeValue(node.options);
76
+ Object.freeze(node.children);
77
+ Object.freeze(node);
78
+ };
79
+ visit(root);
80
+ };
81
+ const validatePath = (path, label) => {
82
+ for (const [index, step] of path.entries()) {
83
+ if ("bezier" in step) {
84
+ for (const [controlIndex, point] of step.bezier.entries()) {
85
+ finite(point.x, `${label}[${String(index)}].bezier[${String(controlIndex)}].x`);
86
+ finite(point.y, `${label}[${String(index)}].bezier[${String(controlIndex)}].y`);
87
+ if (point.z !== undefined)
88
+ finite(point.z, `${label}[${String(index)}].bezier[${String(controlIndex)}].z`);
89
+ }
90
+ }
91
+ else {
92
+ finite(step.x, `${label}[${String(index)}].x`);
93
+ finite(step.y, `${label}[${String(index)}].y`);
94
+ if (step.z !== undefined)
95
+ finite(step.z, `${label}[${String(index)}].z`);
96
+ }
97
+ }
98
+ };
99
+ const valueForBlush = (value) => {
100
+ if (!value)
101
+ return "off";
102
+ if (value <= 0.5)
103
+ return "small";
104
+ if (value <= 0.8)
105
+ return "medium";
106
+ return "big";
107
+ };
108
+ const validateVariant = (variants, channel, variant) => {
109
+ const channelVariants = variants.get(channel);
110
+ if (!channelVariants)
111
+ throw new Error(`Unknown Sprite variant channel: ${channel}`);
112
+ if (!channelVariants.has(variant))
113
+ throw new Error(`Unknown Sprite variant: ${channel}.${variant}`);
114
+ };
115
+ const validateGeometry = (geometry, label, expressions, variants) => {
116
+ if (!geometry)
117
+ return;
118
+ if (geometry.expression !== undefined &&
119
+ !(geometry.expression in expressions)) {
120
+ throw new Error(`Unknown Sprite ${label}.expression: ${geometry.expression}`);
121
+ }
122
+ if (geometry.eyes !== undefined)
123
+ validateVariant(variants, "eyes", geometry.eyes);
124
+ if (geometry.mouth !== undefined)
125
+ validateVariant(variants, "mouth", geometry.mouth);
126
+ if (geometry.blush !== undefined) {
127
+ finite(geometry.blush, `${label}.blush`);
128
+ validateVariant(variants, "blush", valueForBlush(geometry.blush));
129
+ }
130
+ };
131
+ /** The standard presentation activities every Sprite understands. */
132
+ export const STANDARD_ACTIVITIES = [
133
+ "idle",
134
+ "listening",
135
+ "processing",
136
+ "speaking",
137
+ "thinking",
138
+ "error",
139
+ ];
140
+ const standardActivityNames = new Set(STANDARD_ACTIVITIES);
141
+ const createScene = () => {
142
+ const owner = Symbol("sprite-scene");
143
+ const nodes = new Map();
144
+ const variants = new Map();
145
+ const makeNode = (kind, options = {}) => {
146
+ const name = options.name;
147
+ if (options.name !== undefined && (!name || name.trim() === "")) {
148
+ throw new Error("Sprite node name cannot be empty");
149
+ }
150
+ if (name && nodes.has(name))
151
+ throw new Error(`Duplicate Sprite node name: ${name}`);
152
+ const { parent: parentOption, ...nodeOptions } = options;
153
+ const parent = parentOption ? asInternalNode(parentOption) : undefined;
154
+ if (parent && parent.owner !== owner)
155
+ throw new Error("Sprite nodes cannot cross scene definitions");
156
+ validateTransform(options.translate, "translate");
157
+ validateTransform(options.rotate, "rotate");
158
+ validateTransform(options.scale, "scale");
159
+ if (options.stroke !== undefined && options.stroke !== false)
160
+ finite(options.stroke, "stroke");
161
+ const node = {
162
+ owner,
163
+ kind,
164
+ name,
165
+ parent,
166
+ children: [],
167
+ options: {
168
+ ...nodeOptions,
169
+ translate: options.x !== undefined ||
170
+ options.y !== undefined ||
171
+ options.z !== undefined
172
+ ? {
173
+ ...options.translate,
174
+ x: options.x ?? options.translate?.x,
175
+ y: options.y ?? options.translate?.y,
176
+ z: options.z ?? options.translate?.z,
177
+ }
178
+ : options.translate,
179
+ },
180
+ };
181
+ if (parent)
182
+ parent.children.push(node);
183
+ if (name)
184
+ nodes.set(name, node);
185
+ const publicNode = { [sceneNodeBrand]: true };
186
+ registerInternalNode(publicNode, node);
187
+ return publicNode;
188
+ };
189
+ const scene = {
190
+ group: (options = {}) => makeNode("group", options),
191
+ path: (options) => {
192
+ if (options.path)
193
+ validatePath(options.path, "shape.path");
194
+ return makeNode("shape", options);
195
+ },
196
+ ellipse: (options) => {
197
+ finite(options.width, "ellipse.width");
198
+ finite(options.height, "ellipse.height");
199
+ return makeNode("ellipse", options);
200
+ },
201
+ hemisphere: (options) => {
202
+ finite(options.diameter, "hemisphere.diameter");
203
+ return makeNode("hemisphere", options);
204
+ },
205
+ cylinder: (options) => {
206
+ finite(options.diameter, "cylinder.diameter");
207
+ finite(options.length, "cylinder.length");
208
+ return makeNode("cylinder", options);
209
+ },
210
+ };
211
+ return { scene, owner, nodes, variants };
212
+ };
213
+ /** Validate and freeze one renderer-neutral Sprite definition. */
214
+ export function defineSprite(input) {
215
+ if (!Object.is(input.apiVersion, 1))
216
+ throw new Error("Sprite definitions must declare apiVersion: 1");
217
+ if (!input.name.trim())
218
+ throw new Error("Sprite definition name cannot be empty");
219
+ if (!Object.keys(input.palette).length)
220
+ throw new Error("Sprite definitions need a palette");
221
+ for (const [name, value] of Object.entries(input.palette)) {
222
+ if (!name.trim() || !value.trim())
223
+ throw new Error(`Invalid Sprite palette entry: ${name}`);
224
+ }
225
+ if (!Object.keys(input.expressions).length)
226
+ throw new Error("Sprite definitions need an expression");
227
+ const snapshot = cloneAndFreeze({
228
+ ...input,
229
+ palette: input.palette,
230
+ expressions: input.expressions,
231
+ activities: input.activities ?? {},
232
+ });
233
+ const built = createScene();
234
+ const color = (name) => {
235
+ if (!(name in snapshot.palette))
236
+ throw new Error(`Unknown Sprite palette slot: ${name}`);
237
+ return `${COLOR_PREFIX}${name}`;
238
+ };
239
+ const result = snapshot.build({ scene: built.scene, color });
240
+ if (Object.is(result, null) ||
241
+ Object.is(result, undefined) ||
242
+ !Object.hasOwn(result, "root") ||
243
+ Object.is(result.root, null) ||
244
+ Object.is(result.root, undefined))
245
+ throw new Error("Sprite build must return a root node");
246
+ const root = asInternalNode(result.root);
247
+ if (root.owner !== built.owner)
248
+ throw new Error("Sprite root belongs to another definition");
249
+ for (const [channel, values] of Object.entries(result.variants ?? {})) {
250
+ const channelVariants = new Map();
251
+ for (const [variant, node] of Object.entries(values)) {
252
+ const internal = asInternalNode(node);
253
+ if (internal.owner !== built.owner)
254
+ throw new Error(`Sprite variant is from another definition: ${channel}.${variant}`);
255
+ channelVariants.set(variant, internal);
256
+ }
257
+ built.variants.set(channel, channelVariants);
258
+ }
259
+ for (const [expressionName, expression] of Object.entries(snapshot.expressions)) {
260
+ for (const [channel, variant] of Object.entries(expression.variants ?? {})) {
261
+ const channelVariants = built.variants.get(channel);
262
+ if (!channelVariants)
263
+ throw new Error(`Unknown Sprite variant channel: ${channel}`);
264
+ if (!channelVariants.has(variant)) {
265
+ throw new Error(`Unknown Sprite variant: ${channel}.${variant}`);
266
+ }
267
+ }
268
+ for (const [channel, choices] of Object.entries(expression.variantChoices ?? {})) {
269
+ const channelVariants = built.variants.get(channel);
270
+ if (!channelVariants)
271
+ throw new Error(`Unknown Sprite variant channel: ${channel}`);
272
+ if (!choices.length)
273
+ throw new Error(`Sprite expression choices cannot be empty: ${expressionName}.${channel}`);
274
+ for (const variant of choices) {
275
+ if (!channelVariants.has(variant))
276
+ throw new Error(`Unknown Sprite variant: ${channel}.${variant}`);
277
+ }
278
+ }
279
+ if (!expressionName.trim())
280
+ throw new Error("Sprite expression name cannot be empty");
281
+ }
282
+ const activities = snapshot.activities;
283
+ for (const [activityName, activity] of Object.entries(activities)) {
284
+ if (!activityName.trim())
285
+ throw new Error("Sprite activity name cannot be empty");
286
+ if (activity.motion) {
287
+ validateMotion(activity.motion, `activity.${activityName}.motion`);
288
+ if (!built.nodes.has(activity.motion.target)) {
289
+ throw new Error(`Unknown Sprite activity target: ${activity.motion.target}`);
290
+ }
291
+ }
292
+ if (activity.mouseInfluence !== undefined)
293
+ finite(activity.mouseInfluence, `activity.${activityName}.mouseInfluence`);
294
+ validateGeometry(activity.geometry, `activity.${activityName}.geometry`, snapshot.expressions, built.variants);
295
+ for (const [index, step] of (activity.mouthCycle ?? []).entries()) {
296
+ finite(step.durationMs, `activity.${activityName}.mouthCycle[${String(index)}].durationMs`);
297
+ if (step.durationMs < 0)
298
+ throw new Error(`Sprite activity.${activityName}.mouthCycle[${String(index)}].durationMs cannot be negative`);
299
+ }
300
+ }
301
+ for (const [expressionName, expression] of Object.entries(snapshot.expressions)) {
302
+ if (expression.blush !== undefined) {
303
+ finite(expression.blush, `expression.${expressionName}.blush`);
304
+ validateVariant(built.variants, "blush", valueForBlush(expression.blush));
305
+ }
306
+ if (expression.motion) {
307
+ validateMotion(expression.motion, `expression.${expressionName}.motion`);
308
+ if (!built.nodes.has(expression.motion.target))
309
+ throw new Error(`Unknown Sprite expression target: ${expression.motion.target}`);
310
+ }
311
+ }
312
+ const firstExpression = Object.keys(snapshot.expressions)[0];
313
+ const defaultExpression = snapshot.defaultPresentation?.expression ?? firstExpression;
314
+ if (!defaultExpression || !(defaultExpression in snapshot.expressions)) {
315
+ throw new Error(`Unknown Sprite default expression: ${String(defaultExpression)}`);
316
+ }
317
+ const defaultActivity = snapshot.defaultPresentation?.activity ?? "idle";
318
+ if (snapshot.defaultPresentation?.activity !== undefined &&
319
+ !STANDARD_ACTIVITIES.some((activity) => activity === defaultActivity) &&
320
+ !(defaultActivity in activities)) {
321
+ throw new Error(`Unknown Sprite default activity: ${defaultActivity}`);
322
+ }
323
+ for (const node of built.nodes.values())
324
+ freezeScene(node);
325
+ const definition = snapshot;
326
+ const variantNodes = new Set();
327
+ for (const channel of built.variants.values()) {
328
+ for (const node of channel.values())
329
+ variantNodes.add(node);
330
+ }
331
+ setDefinitionInternals(definition, {
332
+ owner: built.owner,
333
+ root,
334
+ nodesByName: built.nodes,
335
+ variants: built.variants,
336
+ variantNodes,
337
+ palette: snapshot.palette,
338
+ activities,
339
+ expressions: snapshot.expressions,
340
+ defaultActivity,
341
+ defaultExpression,
342
+ });
343
+ return definition;
344
+ }
345
+ /** Return a compact, machine-readable receipt for an already authored definition. */
346
+ export const check = (definition) => {
347
+ const internals = getDefinitionInternals(definition);
348
+ const visited = new Set();
349
+ const visit = (node) => {
350
+ if (visited.has(node))
351
+ return;
352
+ visited.add(node);
353
+ for (const child of node.children)
354
+ visit(child);
355
+ };
356
+ visit(internals.root);
357
+ const variants = {};
358
+ for (const [channel, values] of internals.variants)
359
+ variants[channel] = [...values.keys()];
360
+ return {
361
+ valid: true,
362
+ name: definition.name,
363
+ nodeCount: visited.size,
364
+ expressions: Object.keys(internals.expressions),
365
+ activities: [
366
+ ...STANDARD_ACTIVITIES,
367
+ ...Object.keys(internals.activities).filter((name) => !standardActivityNames.has(name)),
368
+ ],
369
+ variants,
370
+ };
371
+ };
372
+ export { toCharacter } from "./portable.js";
@@ -0,0 +1,69 @@
1
+ import * as z from "zod";
2
+ import type { AgentColors, Avatar } from "@fungi.computer/sprite/avatar";
3
+ export { avatarCatalog } from "./catalog.js";
4
+ /**
5
+ * The Avatar v1 schema, refined against every built-in character's facts:
6
+ * the one callers should actually parse untrusted data with. (The unrefined
7
+ * envelope lives at `@fungi.computer/sprite/avatar`'s `avatarEnvelopeSchema`,
8
+ * for a caller building a different catalog.)
9
+ */
10
+ export declare const avatarSchema: z.ZodObject<{
11
+ v: z.ZodLiteral<1>;
12
+ character: z.ZodEnum<{
13
+ mushroom: "mushroom";
14
+ cat: "cat";
15
+ burger: "burger";
16
+ }>;
17
+ appearance: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
18
+ expression: z.ZodString;
19
+ renderStyle: z.ZodEnum<{
20
+ plain: "plain";
21
+ pencil: "pencil";
22
+ manga: "manga";
23
+ riso: "riso";
24
+ halftone: "halftone";
25
+ blueprint: "blueprint";
26
+ crt: "crt";
27
+ watercolor: "watercolor";
28
+ }>;
29
+ }, z.core.$strict>;
30
+ /** Parse an unknown value into a built-in-character-valid Avatar v1 descriptor. */
31
+ export declare function parseAvatar(data: unknown): Avatar;
32
+ /**
33
+ * Derive an Agent's avatar from its creator's: `deriveAgentAvatar` bound to
34
+ * the built-in character catalog, so the Hub, Grove's create preview, the
35
+ * TUI and local Agents all compute the same result without each wiring the
36
+ * catalog through themselves.
37
+ */
38
+ export declare function deriveAgentAvatar(member: Avatar, agentColors: AgentColors, seed: string, avoid?: readonly number[]): Avatar;
39
+ /** A person's saved look and preference for the colours of new Agents. */
40
+ export declare const memberAvatarSchema: z.ZodObject<{
41
+ avatar: z.ZodObject<{
42
+ v: z.ZodLiteral<1>;
43
+ character: z.ZodEnum<{
44
+ mushroom: "mushroom";
45
+ cat: "cat";
46
+ burger: "burger";
47
+ }>;
48
+ appearance: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
49
+ expression: z.ZodString;
50
+ renderStyle: z.ZodEnum<{
51
+ plain: "plain";
52
+ pencil: "pencil";
53
+ manga: "manga";
54
+ riso: "riso";
55
+ halftone: "halftone";
56
+ blueprint: "blueprint";
57
+ crt: "crt";
58
+ watercolor: "watercolor";
59
+ }>;
60
+ }, z.core.$strict>;
61
+ agentColors: z.ZodEnum<{
62
+ shift: "shift";
63
+ keep: "keep";
64
+ }>;
65
+ }, z.core.$strict>;
66
+ /** Catalog-validated account avatar preference. */
67
+ export type MemberAvatar = z.infer<typeof memberAvatarSchema>;
68
+ /** The uncustomized mascot and default colour-shift preference. */
69
+ export declare const defaultMemberAvatar: MemberAvatar;
package/dist/avatar.js ADDED
@@ -0,0 +1,40 @@
1
+ import * as z from "zod";
2
+ import { agentColorsSchema, createAvatarSchema, deriveAgentAvatar as deriveAgentAvatarWithCatalog, } from "@fungi.computer/sprite/avatar";
3
+ import { avatarCatalog } from "./catalog.js";
4
+ export { avatarCatalog } from "./catalog.js";
5
+ /**
6
+ * The Avatar v1 schema, refined against every built-in character's facts:
7
+ * the one callers should actually parse untrusted data with. (The unrefined
8
+ * envelope lives at `@fungi.computer/sprite/avatar`'s `avatarEnvelopeSchema`,
9
+ * for a caller building a different catalog.)
10
+ */
11
+ export const avatarSchema = createAvatarSchema(avatarCatalog);
12
+ /** Parse an unknown value into a built-in-character-valid Avatar v1 descriptor. */
13
+ export function parseAvatar(data) {
14
+ return avatarSchema.parse(data);
15
+ }
16
+ /**
17
+ * Derive an Agent's avatar from its creator's: `deriveAgentAvatar` bound to
18
+ * the built-in character catalog, so the Hub, Grove's create preview, the
19
+ * TUI and local Agents all compute the same result without each wiring the
20
+ * catalog through themselves.
21
+ */
22
+ export function deriveAgentAvatar(member, agentColors, seed, avoid = []) {
23
+ return deriveAgentAvatarWithCatalog(member, agentColors, seed, avatarCatalog, avoid);
24
+ }
25
+ /** A person's saved look and preference for the colours of new Agents. */
26
+ export const memberAvatarSchema = z.strictObject({
27
+ avatar: avatarSchema,
28
+ agentColors: agentColorsSchema,
29
+ });
30
+ /** The uncustomized mascot and default colour-shift preference. */
31
+ export const defaultMemberAvatar = {
32
+ avatar: {
33
+ v: 1,
34
+ character: "mushroom",
35
+ appearance: {},
36
+ expression: "normal",
37
+ renderStyle: "plain",
38
+ },
39
+ agentColors: "shift",
40
+ };