@neta-art/cohub 5.9.0 → 6.0.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.
Files changed (55) hide show
  1. package/README.md +42 -46
  2. package/dist/board/animation.d.ts +156 -67
  3. package/dist/board/animation.js +162 -277
  4. package/dist/board/codec.js +1 -1
  5. package/dist/board/export/index.d.ts +6 -0
  6. package/dist/board/export/index.js +6 -3
  7. package/dist/board/export/scene.d.ts +6 -0
  8. package/dist/board/export/scene.js +26 -2
  9. package/dist/board/geometry.d.ts +27 -1
  10. package/dist/board/geometry.js +92 -1
  11. package/dist/board/headless/index.d.ts +7 -1
  12. package/dist/board/headless/index.js +6 -2
  13. package/dist/board/index.d.ts +8 -6
  14. package/dist/board/index.js +7 -6
  15. package/dist/board/mutation.d.ts +15 -0
  16. package/dist/board/mutation.js +67 -0
  17. package/dist/board/render/css-color.d.ts +4 -0
  18. package/dist/board/render/css-color.js +36 -0
  19. package/dist/board/render/index.d.ts +2 -1
  20. package/dist/board/render/index.js +2 -1
  21. package/dist/board/render/renderers/base-card-renderer.js +3 -20
  22. package/dist/board/render/themes/clean-theme.js +6 -3
  23. package/dist/chunks/http.d.ts +30 -7
  24. package/dist/chunks/http.js +785 -289
  25. package/dist/chunks/websocket.d.ts +2675 -187
  26. package/dist/http.d.ts +3 -3
  27. package/dist/index.d.ts +158 -299
  28. package/dist/index.js +164 -279
  29. package/dist/protocol/dist/board-authoring.d.ts +1 -0
  30. package/dist/protocol/dist/board-authoring.js +217 -0
  31. package/dist/protocol/dist/board-capability-registry.d.ts +2 -0
  32. package/dist/protocol/dist/board-capability-registry.js +74 -0
  33. package/dist/protocol/dist/board-codec.d.ts +2 -0
  34. package/dist/protocol/dist/board-codec.js +4 -0
  35. package/dist/protocol/dist/board-composition.d.ts +258 -0
  36. package/dist/protocol/dist/board-composition.js +318 -0
  37. package/dist/protocol/dist/board-constants.d.ts +2 -1
  38. package/dist/protocol/dist/board-constants.js +40 -10
  39. package/dist/protocol/dist/board-document.d.ts +28 -2
  40. package/dist/protocol/dist/board-document.js +16 -3
  41. package/dist/protocol/dist/board-node.d.ts +1 -11
  42. package/dist/protocol/dist/board-node.js +1 -81
  43. package/dist/protocol/dist/board-upgrade.d.ts +1 -0
  44. package/dist/protocol/dist/board-upgrade.js +2 -0
  45. package/dist/protocol/dist/board-url.d.ts +2 -1
  46. package/dist/protocol/dist/board-url.js +8 -3
  47. package/dist/protocol/dist/board.d.ts +135 -76
  48. package/dist/protocol/dist/board.js +50 -69
  49. package/dist/protocol/dist/index.d.ts +9 -4
  50. package/dist/protocol/dist/index.js +9 -4
  51. package/dist/types.d.ts +3 -2
  52. package/docs/work-runtime-guide.md +19 -3
  53. package/package.json +3 -2
  54. package/dist/board/nodes.d.ts +0 -113
  55. package/dist/board/nodes.js +0 -154
@@ -1,267 +1,148 @@
1
1
  import { BOARD_BUILTIN_CAPABILITIES, DEFAULT_BOARD_RENDER_LIMITS } from "../protocol/dist/board-constants.js";
2
+ import { BOARD_ANIMATION_CHANNELS, BoardCompositionSchema, BoardProceduralClipSchema, BoardTrackSchema } from "../protocol/dist/board-composition.js";
3
+ import { estimateBuiltinBoardClipCost, validateBuiltinBoardClip } from "../protocol/dist/board-capability-registry.js";
4
+ import "../protocol/dist/index.js";
2
5
  //#region src/board/animation.ts
3
- const ZERO_COST = {
4
- particles: 0,
5
- vertices: 0,
6
- dynamicVertices: 0,
7
- drawCalls: 0,
8
- filterPasses: 0,
9
- renderTexturePixels: 0,
10
- textureBytes: 0,
11
- bufferBytes: 0,
12
- simulationSteps: 0
13
- };
14
- const DEFAULT_BOARD_LIMITS = DEFAULT_BOARD_RENDER_LIMITS;
15
- const BUILTIN_CAPABILITIES = BOARD_BUILTIN_CAPABILITIES;
16
- function numericParam(params, key, fallback) {
17
- const value = params[key];
18
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
6
+ function track(input) {
7
+ return BoardTrackSchema.parse(input);
19
8
  }
20
- function builtinDefinition(capability) {
21
- return {
22
- ...capability,
23
- validate(params) {
24
- const diagnostics = [];
25
- if (capability.id === "effects.particles") {
26
- const count = params.count;
27
- const bounds = params.bounds;
28
- if (!Number.isSafeInteger(count) || count < 1 || count > DEFAULT_BOARD_LIMITS.particles) diagnostics.push({
29
- severity: "error",
30
- code: "INVALID_PARTICLE_COUNT",
31
- message: `count must be an integer between 1 and ${DEFAULT_BOARD_LIMITS.particles}`,
32
- path: "params.count"
33
- });
34
- if (!bounds || typeof bounds !== "object" || Array.isArray(bounds)) diagnostics.push({
35
- severity: "error",
36
- code: "PARTICLE_BOUNDS_REQUIRED",
37
- message: "particles require finite bounds",
38
- path: "params.bounds"
39
- });
40
- else {
41
- const value = bounds;
42
- if (![
43
- value.x,
44
- value.y,
45
- value.width,
46
- value.height
47
- ].every((item) => typeof item === "number" && Number.isFinite(item)) || value.width <= 0 || value.height <= 0) diagnostics.push({
48
- severity: "error",
49
- code: "INVALID_PARTICLE_BOUNDS",
50
- message: "particle bounds must have a positive finite size",
51
- path: "params.bounds"
52
- });
53
- }
54
- }
55
- if (capability.id === "motion.path") {
56
- const points = params.points;
57
- if (!Array.isArray(points) || points.length < 2 || points.length > 1e4) diagnostics.push({
58
- severity: "error",
59
- code: "INVALID_MOTION_PATH",
60
- message: "motion path must contain 2 to 10000 points",
61
- path: "params.points"
62
- });
63
- }
64
- return diagnostics;
9
+ function proceduralClip(input) {
10
+ return BoardProceduralClipSchema.parse({
11
+ ...input,
12
+ kindVersion: input.kindVersion ?? 1
13
+ });
14
+ }
15
+ function composition(input) {
16
+ return BoardCompositionSchema.parse(input);
17
+ }
18
+ function compileComposition(input) {
19
+ return composition({
20
+ id: input.id,
21
+ name: input.name,
22
+ timeline: {
23
+ duration: input.duration,
24
+ tracks: (input.tracks ?? []).map(track),
25
+ clips: (input.clips ?? []).map(proceduralClip),
26
+ markers: input.markers ?? []
65
27
  },
66
- getBounds(params) {
67
- const bounds = params.bounds;
68
- if (!bounds || typeof bounds !== "object" || Array.isArray(bounds)) return null;
69
- const value = bounds;
70
- return [
71
- value.x,
72
- value.y,
73
- value.width,
74
- value.height
75
- ].every((item) => typeof item === "number" && Number.isFinite(item)) ? value : null;
28
+ playback: input.playback ?? {
29
+ loop: false,
30
+ endBehavior: "hold",
31
+ reducedMotion: { mode: "base" }
76
32
  },
77
- estimateCost(params) {
78
- switch (capability.id) {
79
- case "effects.particles": {
80
- const particles = Math.max(0, Math.floor(numericParam(params, "count", 0)));
81
- return {
82
- particles,
83
- vertices: particles * 4,
84
- dynamicVertices: particles * 4,
85
- drawCalls: 1,
86
- bufferBytes: particles * 48,
87
- simulationSteps: particles
88
- };
89
- }
90
- case "effects.trail": return {
91
- vertices: 32,
92
- dynamicVertices: 32,
93
- drawCalls: 1,
94
- bufferBytes: 1024,
95
- simulationSteps: 16
96
- };
97
- case "effects.impact":
98
- case "effects.flash": return {
99
- vertices: 64,
100
- drawCalls: 1
101
- };
102
- case "effects.color": return {
103
- drawCalls: 1,
104
- filterPasses: 1
105
- };
106
- case "draw.reveal":
107
- case "draw.handwrite": return {
108
- drawCalls: 1,
109
- dynamicVertices: 1
110
- };
111
- default: return {};
112
- }
113
- }
114
- };
33
+ metadata: input.metadata ?? {},
34
+ revision: 0
35
+ });
115
36
  }
116
- function stableHash(value) {
117
- let hash = 2166136261;
118
- for (let index = 0; index < value.length; index += 1) {
119
- hash ^= value.charCodeAt(index);
120
- hash = Math.imul(hash, 16777619);
37
+ const clamp01 = (value) => Math.max(0, Math.min(1, value));
38
+ function sampleEasing(easing, value) {
39
+ const t = clamp01(value);
40
+ switch (easing) {
41
+ case "ease-in-quad": return t * t;
42
+ case "ease-out-quad": return 1 - (1 - t) ** 2;
43
+ case "ease-in-out-quad": return t < .5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
44
+ case "ease-in-cubic": return t ** 3;
45
+ case "ease-out-cubic": return 1 - (1 - t) ** 3;
46
+ case "ease-in-out-cubic": return t < .5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2;
47
+ case "ease-out-quart": return 1 - (1 - t) ** 4;
48
+ case "ease-out-expo": return t === 1 ? 1 : 1 - 2 ** (-10 * t);
49
+ default: return t;
121
50
  }
122
- return (hash >>> 0).toString(36).padStart(7, "0");
123
- }
124
- function stableId(prefix, seed, path) {
125
- return `${prefix}_${stableHash(`${seed}:${path}`)}`;
126
- }
127
- function assertDuration(value, field) {
128
- if (!Number.isFinite(value) || value < 0) throw new TypeError(`${field} must be a finite non-negative number`);
129
51
  }
130
- const timeline = {
131
- clip(clip) {
132
- return {
133
- type: "clip",
134
- clip
135
- };
136
- },
137
- parallel(...children) {
138
- return {
139
- type: "parallel",
140
- children
141
- };
142
- },
143
- sequence(...children) {
144
- return {
145
- type: "sequence",
146
- children
147
- };
148
- },
149
- stagger(each, ...children) {
150
- assertDuration(each, "stagger each");
151
- return {
152
- type: "stagger",
153
- each,
154
- children
155
- };
156
- },
157
- delay(duration, child) {
158
- assertDuration(duration, "delay duration");
159
- return {
160
- type: "delay",
161
- duration,
162
- child
163
- };
164
- },
165
- repeat(count, child) {
166
- if (!Number.isSafeInteger(count) || count < 1 || count > 1e3) throw new TypeError("repeat count must be an integer between 1 and 1000");
167
- return {
168
- type: "repeat",
169
- count,
170
- child
171
- };
52
+ function interpolate(left, right, progress) {
53
+ if (typeof left === "number" && typeof right === "number") return left + (right - left) * progress;
54
+ if (left && right && typeof left === "object" && typeof right === "object" && !Array.isArray(left) && !Array.isArray(right)) {
55
+ const a = left;
56
+ const b = right;
57
+ const result = {};
58
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) result[key] = interpolate(a[key], b[key], progress);
59
+ return result;
172
60
  }
173
- };
174
- function clip(input) {
175
- assertDuration(input.duration, "clip duration");
176
- if (input.duration === 0) throw new TypeError("clip duration must be greater than zero");
177
- return timeline.clip({
178
- id: input.id,
179
- kind: input.kind,
180
- kindVersion: input.kindVersion ?? 1,
181
- target: input.target,
182
- duration: input.duration,
183
- layer: input.layer ?? "content",
184
- fill: input.fill ?? "none",
185
- easing: input.easing ?? "linear",
186
- params: input.params ?? {},
187
- keyframes: input.keyframes ?? [],
188
- assetRefs: input.assetRefs ?? [],
189
- seed: input.seed,
190
- metadata: input.metadata ?? {}
191
- });
61
+ return progress < 1 ? left : right;
192
62
  }
193
- function flatten(input, offset, sequenceSeed, path) {
194
- if (input.type === "clip") {
195
- const clipSeed = input.clip.seed ?? `${sequenceSeed}:${path}`;
63
+ /** Deterministically sample one validated Track without touching scene state. */
64
+ function sampleTrack(trackValue, time) {
65
+ const frames = trackValue.keyframes;
66
+ const first = frames[0];
67
+ const last = frames.at(-1);
68
+ if (!first || !last) return null;
69
+ if (time < first.time) {
70
+ if (trackValue.fill !== "backwards" && trackValue.fill !== "both") return null;
196
71
  return {
197
- duration: input.clip.duration,
198
- clips: [{
199
- ...input.clip,
200
- id: input.clip.id ?? stableId("clip", sequenceSeed, path),
201
- start: offset,
202
- seed: clipSeed
203
- }]
72
+ target: trackValue.target,
73
+ channel: trackValue.channel,
74
+ value: first.value
204
75
  };
205
76
  }
206
- if (input.type === "delay") {
207
- const nested = flatten(input.child, offset + input.duration, sequenceSeed, `${path}.child`);
77
+ if (time > last.time) {
78
+ if (trackValue.fill !== "forwards" && trackValue.fill !== "both") return null;
208
79
  return {
209
- duration: input.duration + nested.duration,
210
- clips: nested.clips
80
+ target: trackValue.target,
81
+ channel: trackValue.channel,
82
+ value: last.value
211
83
  };
212
84
  }
213
- if (input.type === "repeat") {
214
- const clips = [];
215
- let cursor = offset;
216
- let total = 0;
217
- for (let index = 0; index < input.count; index += 1) {
218
- const nested = flatten(input.child, cursor, sequenceSeed, `${path}.${index}`);
219
- clips.push(...nested.clips);
220
- cursor += nested.duration;
221
- total += nested.duration;
222
- }
223
- return {
224
- duration: total,
225
- clips
226
- };
227
- }
228
- const clips = [];
229
- let duration = 0;
230
- for (const [index, child] of input.children.entries()) {
231
- const childOffset = input.type === "sequence" ? duration : input.type === "stagger" ? input.each * index : 0;
232
- const nested = flatten(child, offset + childOffset, sequenceSeed, `${path}.${index}`);
233
- clips.push(...nested.clips);
234
- duration = input.type === "sequence" ? duration + nested.duration : Math.max(duration, childOffset + nested.duration);
85
+ let low = 0;
86
+ let high = frames.length;
87
+ while (low < high) {
88
+ const middle = low + high >>> 1;
89
+ if ((frames[middle]?.time ?? Number.POSITIVE_INFINITY) <= time) low = middle + 1;
90
+ else high = middle;
235
91
  }
92
+ const left = frames[Math.max(0, low - 1)] ?? first;
93
+ const right = frames[Math.min(frames.length - 1, low)] ?? last;
94
+ if (left === right || trackValue.interpolation === "step") return {
95
+ target: trackValue.target,
96
+ channel: trackValue.channel,
97
+ value: left.value
98
+ };
99
+ const span = Math.max(Number.EPSILON, right.time - left.time);
100
+ const progress = sampleEasing(right.easing ?? "linear", (time - left.time) / span);
236
101
  return {
237
- duration,
238
- clips
102
+ target: trackValue.target,
103
+ channel: trackValue.channel,
104
+ value: interpolate(left.value, right.value, progress)
239
105
  };
240
106
  }
241
- function compileSequence(input) {
242
- const flattened = flatten(input.timeline, 0, input.seed, "root");
243
- flattened.clips.sort((left, right) => left.start - right.start || left.id.localeCompare(right.id));
244
- const refs = /* @__PURE__ */ new Map();
245
- for (const item of flattened.clips) for (const ref of item.assetRefs) refs.set(`${ref.type}:${ref.ref}:${ref.digest ?? ""}`, ref);
107
+ function sampleCompositionTracks(value, time) {
108
+ const position = Math.max(0, Math.min(value.timeline.duration, time));
109
+ return value.timeline.tracks.flatMap((item) => {
110
+ const sampled = sampleTrack(item, position);
111
+ return sampled ? [sampled] : [];
112
+ });
113
+ }
114
+ const ZERO_COST = {
115
+ particles: 0,
116
+ vertices: 0,
117
+ dynamicVertices: 0,
118
+ drawCalls: 0,
119
+ filterPasses: 0,
120
+ renderTexturePixels: 0,
121
+ textureBytes: 0,
122
+ bufferBytes: 0,
123
+ simulationSteps: 0
124
+ };
125
+ const DEFAULT_BOARD_LIMITS = DEFAULT_BOARD_RENDER_LIMITS;
126
+ function builtinDefinition(capability) {
246
127
  return {
247
- sequence: {
248
- id: input.id,
249
- name: input.name,
250
- duration: flattened.duration,
251
- seed: input.seed,
252
- restPose: input.restPose ?? {},
253
- metadata: input.metadata ?? {}
128
+ ...capability,
129
+ validate() {
130
+ return [];
254
131
  },
255
- clips: flattened.clips,
256
- assetRefs: [...refs.values()]
132
+ estimateCost(params) {
133
+ return estimateBuiltinBoardClipCost({
134
+ kind: capability.id,
135
+ params
136
+ });
137
+ }
257
138
  };
258
139
  }
259
- function addCost(target, source) {
260
- for (const key of Object.keys(target)) target[key] += source[key] ?? 0;
261
- }
262
140
  var BoardExtensionRegistry = class {
263
141
  #extensions = /* @__PURE__ */ new Map();
264
142
  #presets = /* @__PURE__ */ new Map();
143
+ constructor() {
144
+ for (const capability of BOARD_BUILTIN_CAPABILITIES) this.register(builtinDefinition(capability));
145
+ }
265
146
  register(definition) {
266
147
  const key = `${definition.kind}:${definition.id}@${definition.version}`;
267
148
  const target = definition.kind === "preset" ? this.#presets : this.#extensions;
@@ -270,9 +151,15 @@ var BoardExtensionRegistry = class {
270
151
  return this;
271
152
  }
272
153
  capabilities() {
273
- const extensions = [...this.#extensions.values()].map(({ validate: _validate, getBounds: _bounds, getAssetRefs: _refs, estimateCost: _cost, ...definition }) => definition);
274
- const presets = [...this.#presets.values()].map(({ compile: _compile, ...definition }) => definition);
275
- return [...extensions, ...presets];
154
+ return [...this.#extensions.values(), ...this.#presets.values()].map((definition) => ({
155
+ kind: definition.kind,
156
+ id: definition.id,
157
+ version: definition.version,
158
+ ...definition.digest ? { digest: definition.digest } : {},
159
+ ...definition.renderers ? { renderers: definition.renderers } : {},
160
+ ...definition.fallbackId ? { fallbackId: definition.fallbackId } : {},
161
+ ...definition.schema ? { schema: definition.schema } : {}
162
+ }));
276
163
  }
277
164
  compilePreset(id, version, params) {
278
165
  const preset = this.#presets.get(`preset:${id}@${version}`);
@@ -280,74 +167,72 @@ var BoardExtensionRegistry = class {
280
167
  return preset.compile(params);
281
168
  }
282
169
  validate(input) {
170
+ const parsed = BoardCompositionSchema.safeParse(input.composition);
171
+ if (!parsed.success) return {
172
+ valid: false,
173
+ diagnostics: parsed.error.issues.map((issue) => ({
174
+ severity: "error",
175
+ code: "INVALID_COMPOSITION",
176
+ message: issue.message,
177
+ path: issue.path.join(".")
178
+ })),
179
+ peakCost: { ...ZERO_COST }
180
+ };
283
181
  const diagnostics = [];
182
+ const composition = parsed.data;
183
+ const cost = { ...ZERO_COST };
284
184
  const events = [];
285
- const profile = input.profile ?? "high";
286
- const persistentCost = { ...ZERO_COST };
287
- for (const [index, effect] of (input.effects ?? []).entries()) {
288
- const definition = this.#extensions.get(`effect:${effect.kind}@${effect.kindVersion}`);
289
- if (!definition) {
290
- diagnostics.push({
291
- severity: "warning",
292
- code: "UNKNOWN_EFFECT",
293
- message: `No renderer is registered for ${effect.kind}@${effect.kindVersion}`,
294
- path: `effects.${index}`
295
- });
296
- continue;
297
- }
298
- diagnostics.push(...definition.validate?.(effect.params) ?? []);
299
- addCost(persistentCost, definition.estimateCost(effect.params, profile));
300
- }
301
- for (const [index, clip] of input.clips.entries()) {
185
+ for (const clip of composition.timeline.clips) {
302
186
  const definition = this.#extensions.get(`clip:${clip.kind}@${clip.kindVersion}`);
303
187
  if (!definition) {
304
188
  diagnostics.push({
305
189
  severity: "warning",
306
190
  code: "UNKNOWN_CLIP",
307
- message: `No renderer is registered for ${clip.kind}@${clip.kindVersion}`,
308
- path: `clips.${index}`
191
+ message: `No renderer is registered for ${clip.kind}@${clip.kindVersion}`
309
192
  });
310
193
  continue;
311
194
  }
195
+ diagnostics.push(...validateBuiltinBoardClip(clip, `composition.timeline.clips.${clip.id}`));
312
196
  diagnostics.push(...definition.validate?.(clip.params) ?? []);
313
- const cost = { ...ZERO_COST };
314
- addCost(cost, definition.estimateCost(clip.params, profile));
197
+ const estimate = definition.estimateCost(clip.params, input.profile ?? "high");
198
+ const clipCost = { ...ZERO_COST };
199
+ for (const key of Object.keys(clipCost)) clipCost[key] = estimate[key] ?? 0;
315
200
  events.push({
316
201
  at: clip.start,
317
202
  direction: 1,
318
- cost
319
- }, {
203
+ cost: clipCost
204
+ });
205
+ events.push({
320
206
  at: clip.start + clip.duration,
321
207
  direction: -1,
322
- cost
208
+ cost: clipCost
323
209
  });
324
210
  }
325
211
  events.sort((left, right) => left.at - right.at || left.direction - right.direction);
326
- const current = { ...persistentCost };
327
- const peak = { ...persistentCost };
328
- for (const event of events) for (const key of Object.keys(current)) {
329
- current[key] += event.cost[key] * event.direction;
330
- peak[key] = Math.max(peak[key], current[key]);
212
+ const active = { ...ZERO_COST };
213
+ for (const event of events) for (const key of Object.keys(active)) {
214
+ active[key] += event.cost[key] * event.direction;
215
+ cost[key] = Math.max(cost[key], active[key]);
331
216
  }
217
+ for (const effect of input.effects ?? []) if (effect.kind === "effects.pulse" || effect.kind === "effects.float") cost.drawCalls += 1;
332
218
  const limits = input.limits ?? DEFAULT_BOARD_LIMITS;
333
- for (const key of Object.keys(limits)) if (peak[key] > limits[key]) diagnostics.push({
219
+ for (const key of Object.keys(cost)) if (cost[key] > limits[key]) diagnostics.push({
334
220
  severity: "warning",
335
221
  code: "RENDER_BUDGET_EXCEEDED",
336
- message: `${key} peaks at ${peak[key]}, above ${limits[key]}`,
222
+ message: `${key} peaks at ${cost[key]}, above ${limits[key]}`,
337
223
  path: key,
338
224
  adaptation: { quality: "lower" }
339
225
  });
340
226
  return {
341
227
  valid: !diagnostics.some((item) => item.severity === "error"),
342
228
  diagnostics,
343
- peakCost: peak
229
+ peakCost: cost
344
230
  };
345
231
  }
346
232
  };
347
- function createBoardExtensionRegistry(input = {}) {
348
- const registry = new BoardExtensionRegistry();
349
- if (input.builtins !== false) for (const capability of BUILTIN_CAPABILITIES) registry.register(builtinDefinition(capability));
350
- return registry;
233
+ function createBoardExtensionRegistry() {
234
+ return new BoardExtensionRegistry();
351
235
  }
236
+ const BOARD_CHANNELS = BOARD_ANIMATION_CHANNELS;
352
237
  //#endregion
353
- export { BoardExtensionRegistry, DEFAULT_BOARD_LIMITS, clip, compileSequence, createBoardExtensionRegistry, timeline };
238
+ export { BOARD_CHANNELS, BoardExtensionRegistry, DEFAULT_BOARD_LIMITS, compileComposition, composition, createBoardExtensionRegistry, proceduralClip, sampleCompositionTracks, sampleEasing, sampleTrack, track };
@@ -1,7 +1,7 @@
1
1
  import { BOARD_DOCUMENT_KIND, InvalidBoardFileError, isBoardPath, parseBoardManifest, serializeBoardManifest } from "../protocol/dist/board.js";
2
2
  import { BoardAppearanceSchema, BoardFileSnapshotSchema, BoardTaskSnapshotSchema, UNKNOWN_BOARD_ITEM_TYPE, parseBoardDocument } from "../protocol/dist/board-document.js";
3
- import { TEXT_FONT_SIZE, clampBoardTextFontSize } from "./core/text-metrics.js";
4
3
  import "../protocol/dist/index.js";
4
+ import { TEXT_FONT_SIZE, clampBoardTextFontSize } from "./core/text-metrics.js";
5
5
  //#region src/board/codec.ts
6
6
  const DEFAULT_BOARD_APPEARANCE = BoardAppearanceSchema.parse({
7
7
  theme: "clean",
@@ -23,6 +23,12 @@ type BoardExportOptions = {
23
23
  textures?: Map<string, Texture>;
24
24
  /** Preview-key strategy supplied by the host; defaults to still images only. */
25
25
  assetKey?: (item: BoardItem) => string | null;
26
+ backgroundImage?: {
27
+ texture: Texture;
28
+ fit: "cover" | "contain" | "repeat";
29
+ position: "center" | "top" | "bottom" | "left" | "right";
30
+ opacity: number;
31
+ };
26
32
  maxEdge?: number;
27
33
  maxPixels?: number;
28
34
  };
@@ -1,12 +1,14 @@
1
1
  import { normalizeBoardDocument, planBoardExport } from "../core/export-plan.js";
2
+ import { parseBoardCssColor } from "../render/css-color.js";
2
3
  import { defaultBoardPalette } from "../render/palette.js";
3
4
  import { createBoardExportScene } from "./scene.js";
4
5
  import { Rectangle } from "pixi.js";
5
6
  //#region src/board/export/index.ts
6
- function resolveBackground(background, palette) {
7
+ function resolveBackground(background, palette, document) {
7
8
  if (background === "transparent") return null;
8
9
  if (typeof background === "number") return background;
9
- return palette.bg;
10
+ const declared = document.appearance.background.color;
11
+ return declared ? parseBoardCssColor(declared) ?? palette.bg : palette.bg;
10
12
  }
11
13
  /**
12
14
  * Capture a board region as a canvas.
@@ -42,7 +44,8 @@ function renderBoardExport(renderer, input, options = {}) {
42
44
  colors: options.colors,
43
45
  textures: options.textures,
44
46
  assetKey: options.assetKey,
45
- background: resolveBackground(options.background, palette)
47
+ background: resolveBackground(options.background, palette, document),
48
+ backgroundImage: options.backgroundImage
46
49
  });
47
50
  try {
48
51
  const canvas = renderer.extract.canvas({
@@ -25,6 +25,12 @@ type BoardExportSceneInput = {
25
25
  assetKey?: (item: BoardItem) => string | null;
26
26
  /** Opaque paper behind the content, or null for transparency. */
27
27
  background?: number | null;
28
+ backgroundImage?: {
29
+ texture: Texture;
30
+ fit: "cover" | "contain" | "repeat";
31
+ position: "center" | "top" | "bottom" | "left" | "right";
32
+ opacity: number;
33
+ };
28
34
  };
29
35
  type BoardExportScene = {
30
36
  /** Root to hand to the renderer; already positioned and scaled. */
@@ -3,7 +3,7 @@ import { buildFallbackShapeColors } from "../core/palette.js";
3
3
  import { createConnectionLayer } from "../render/connection-layer.js";
4
4
  import { defaultBoardPalette } from "../render/palette.js";
5
5
  import { getBoardCardRenderer } from "../render/renderers/board-renderer-registry.js";
6
- import { Container, Graphics } from "pixi.js";
6
+ import { Container, Graphics, Sprite, TilingSprite } from "pixi.js";
7
7
  //#region src/board/export/scene.ts
8
8
  /**
9
9
  * Render context for an export.
@@ -45,10 +45,34 @@ function buildContext(input) {
45
45
  function createBoardExportScene(input) {
46
46
  const { context, missing } = buildContext(input);
47
47
  const root = new Container({ label: "board-export-root" });
48
- if (input.background != null) root.addChild(new Graphics().rect(0, 0, input.world.width * input.scale, input.world.height * input.scale).fill({
48
+ const outputWidth = input.world.width * input.scale;
49
+ const outputHeight = input.world.height * input.scale;
50
+ if (input.background != null) root.addChild(new Graphics().rect(0, 0, outputWidth, outputHeight).fill({
49
51
  color: input.background,
50
52
  alpha: 1
51
53
  }));
54
+ if (input.backgroundImage) {
55
+ const { texture, fit, position, opacity } = input.backgroundImage;
56
+ if (fit === "repeat") root.addChild(new TilingSprite({
57
+ texture,
58
+ width: outputWidth,
59
+ height: outputHeight,
60
+ alpha: opacity
61
+ }));
62
+ else {
63
+ const sprite = new Sprite({
64
+ texture,
65
+ alpha: opacity
66
+ });
67
+ const scale = fit === "cover" ? Math.max(outputWidth / texture.width, outputHeight / texture.height) : Math.min(outputWidth / texture.width, outputHeight / texture.height);
68
+ sprite.width = texture.width * scale;
69
+ sprite.height = texture.height * scale;
70
+ const x = position === "left" ? 0 : position === "right" ? outputWidth - sprite.width : (outputWidth - sprite.width) / 2;
71
+ const y = position === "top" ? 0 : position === "bottom" ? outputHeight - sprite.height : (outputHeight - sprite.height) / 2;
72
+ sprite.position.set(x, y);
73
+ root.addChild(sprite);
74
+ }
75
+ }
52
76
  const world = new Container({
53
77
  isRenderGroup: true,
54
78
  label: "board-export-world"