@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.
- package/README.md +42 -46
- package/dist/board/animation.d.ts +156 -67
- package/dist/board/animation.js +162 -277
- package/dist/board/codec.js +1 -1
- package/dist/board/export/index.d.ts +6 -0
- package/dist/board/export/index.js +6 -3
- package/dist/board/export/scene.d.ts +6 -0
- package/dist/board/export/scene.js +26 -2
- package/dist/board/geometry.d.ts +27 -1
- package/dist/board/geometry.js +92 -1
- package/dist/board/headless/index.d.ts +7 -1
- package/dist/board/headless/index.js +6 -2
- package/dist/board/index.d.ts +8 -6
- package/dist/board/index.js +7 -6
- package/dist/board/mutation.d.ts +15 -0
- package/dist/board/mutation.js +67 -0
- package/dist/board/render/css-color.d.ts +4 -0
- package/dist/board/render/css-color.js +36 -0
- package/dist/board/render/index.d.ts +2 -1
- package/dist/board/render/index.js +2 -1
- package/dist/board/render/renderers/base-card-renderer.js +3 -20
- package/dist/board/render/themes/clean-theme.js +6 -3
- package/dist/chunks/http.d.ts +30 -7
- package/dist/chunks/http.js +785 -289
- package/dist/chunks/websocket.d.ts +2675 -187
- package/dist/http.d.ts +3 -3
- package/dist/index.d.ts +158 -299
- package/dist/index.js +164 -279
- package/dist/protocol/dist/board-authoring.d.ts +1 -0
- package/dist/protocol/dist/board-authoring.js +217 -0
- package/dist/protocol/dist/board-capability-registry.d.ts +2 -0
- package/dist/protocol/dist/board-capability-registry.js +74 -0
- package/dist/protocol/dist/board-codec.d.ts +2 -0
- package/dist/protocol/dist/board-codec.js +4 -0
- package/dist/protocol/dist/board-composition.d.ts +258 -0
- package/dist/protocol/dist/board-composition.js +318 -0
- package/dist/protocol/dist/board-constants.d.ts +2 -1
- package/dist/protocol/dist/board-constants.js +40 -10
- package/dist/protocol/dist/board-document.d.ts +28 -2
- package/dist/protocol/dist/board-document.js +16 -3
- package/dist/protocol/dist/board-node.d.ts +1 -11
- package/dist/protocol/dist/board-node.js +1 -81
- package/dist/protocol/dist/board-upgrade.d.ts +1 -0
- package/dist/protocol/dist/board-upgrade.js +2 -0
- package/dist/protocol/dist/board-url.d.ts +2 -1
- package/dist/protocol/dist/board-url.js +8 -3
- package/dist/protocol/dist/board.d.ts +135 -76
- package/dist/protocol/dist/board.js +50 -69
- package/dist/protocol/dist/index.d.ts +9 -4
- package/dist/protocol/dist/index.js +9 -4
- package/dist/types.d.ts +3 -2
- package/docs/work-runtime-guide.md +19 -3
- package/package.json +3 -2
- package/dist/board/nodes.d.ts +0 -113
- package/dist/board/nodes.js +0 -154
package/dist/board/animation.js
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
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
|
|
21
|
-
return {
|
|
22
|
-
...
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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 (
|
|
207
|
-
|
|
77
|
+
if (time > last.time) {
|
|
78
|
+
if (trackValue.fill !== "forwards" && trackValue.fill !== "both") return null;
|
|
208
79
|
return {
|
|
209
|
-
|
|
210
|
-
|
|
80
|
+
target: trackValue.target,
|
|
81
|
+
channel: trackValue.channel,
|
|
82
|
+
value: last.value
|
|
211
83
|
};
|
|
212
84
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
238
|
-
|
|
102
|
+
target: trackValue.target,
|
|
103
|
+
channel: trackValue.channel,
|
|
104
|
+
value: interpolate(left.value, right.value, progress)
|
|
239
105
|
};
|
|
240
106
|
}
|
|
241
|
-
function
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
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
|
|
314
|
-
|
|
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
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
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(
|
|
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 ${
|
|
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:
|
|
229
|
+
peakCost: cost
|
|
344
230
|
};
|
|
345
231
|
}
|
|
346
232
|
};
|
|
347
|
-
function createBoardExtensionRegistry(
|
|
348
|
-
|
|
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,
|
|
238
|
+
export { BOARD_CHANNELS, BoardExtensionRegistry, DEFAULT_BOARD_LIMITS, compileComposition, composition, createBoardExtensionRegistry, proceduralClip, sampleCompositionTracks, sampleEasing, sampleTrack, track };
|
package/dist/board/codec.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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"
|