@oxy.so/bloom 7.1.0 → 7.1.2
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/assets/character-runtime/managed-avatar.mjs +258 -0
- package/assets/character-runtime/render-budget.mjs +144 -0
- package/assets/character-runtime/runtime.mjs +49 -18
- package/docs/adoption-matrix.mdx +3 -3
- package/docs/agent-avatar.mdx +11 -2
- package/docs/bottom-sheet.mdx +1 -1
- package/lib/commonjs/agent-avatar/AgentAvatarProvider.js +8 -3
- package/lib/commonjs/agent-avatar/AgentAvatarProvider.js.map +1 -1
- package/lib/commonjs/agent-avatar/CharacterAvatar.js +3 -2
- package/lib/commonjs/agent-avatar/CharacterAvatar.js.map +1 -1
- package/lib/commonjs/agent-avatar/context.js +4 -1
- package/lib/commonjs/agent-avatar/context.js.map +1 -1
- package/lib/commonjs/agent-creator/AgentCreatorBase.js +7 -5
- package/lib/commonjs/agent-creator/AgentCreatorBase.js.map +1 -1
- package/lib/commonjs/agent-creator/CharacterControls.js +4 -2
- package/lib/commonjs/agent-creator/CharacterControls.js.map +1 -1
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js +33 -49
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/dialog/DialogBottomSheet.js +1 -1
- package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/commonjs/surface/SurfacePaint.js +17 -4
- package/lib/commonjs/surface/SurfacePaint.js.map +1 -1
- package/lib/module/agent-avatar/AgentAvatarProvider.js +9 -4
- package/lib/module/agent-avatar/AgentAvatarProvider.js.map +1 -1
- package/lib/module/agent-avatar/CharacterAvatar.js +3 -2
- package/lib/module/agent-avatar/CharacterAvatar.js.map +1 -1
- package/lib/module/agent-avatar/context.js +3 -0
- package/lib/module/agent-avatar/context.js.map +1 -1
- package/lib/module/agent-creator/AgentCreatorBase.js +9 -7
- package/lib/module/agent-creator/AgentCreatorBase.js.map +1 -1
- package/lib/module/agent-creator/CharacterControls.js +5 -3
- package/lib/module/agent-creator/CharacterControls.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +33 -49
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/dialog/DialogBottomSheet.js +1 -1
- package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/module/surface/SurfacePaint.js +18 -5
- package/lib/module/surface/SurfacePaint.js.map +1 -1
- package/lib/typescript/commonjs/agent-avatar/AgentAvatarProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-avatar/CharacterAvatar.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-avatar/context.d.ts +4 -1
- package/lib/typescript/commonjs/agent-avatar/context.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-creator/AgentCreatorBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-creator/CharacterControls.d.ts.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/types.d.ts +1 -0
- package/lib/typescript/commonjs/bottom-sheet/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/surface/SurfacePaint.d.ts.map +1 -1
- package/lib/typescript/module/agent-avatar/AgentAvatarProvider.d.ts.map +1 -1
- package/lib/typescript/module/agent-avatar/CharacterAvatar.d.ts.map +1 -1
- package/lib/typescript/module/agent-avatar/context.d.ts +4 -1
- package/lib/typescript/module/agent-avatar/context.d.ts.map +1 -1
- package/lib/typescript/module/agent-creator/AgentCreatorBase.d.ts.map +1 -1
- package/lib/typescript/module/agent-creator/CharacterControls.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/types.d.ts +1 -0
- package/lib/typescript/module/bottom-sheet/types.d.ts.map +1 -1
- package/lib/typescript/module/surface/SurfacePaint.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/support/adoption-matrix.ts +683 -0
- package/src/__tests__/support/card-surface.ts +25 -0
- package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
- package/src/__tests__/support/commerce-harness.tsx +97 -0
- package/src/__tests__/support/composite.ts +68 -0
- package/src/__tests__/support/constructed-style-sheets.ts +68 -0
- package/src/__tests__/support/messages-in.ts +10 -0
- package/src/__tests__/support/press-host.ts +30 -0
- package/src/__tests__/support/rendered-style.ts +99 -0
- package/src/__tests__/support/unread-hook-fixture.ts +33 -0
- package/src/__tests__/support/worklet-capture-fixture.tsx +28 -0
- package/src/agent-avatar/AgentAvatarProvider.tsx +11 -4
- package/src/agent-avatar/CharacterAvatar.tsx +9 -4
- package/src/agent-avatar/__tests__/artwork-golden.json +774 -0
- package/src/agent-avatar/context.ts +5 -1
- package/src/agent-creator/AgentCreatorBase.tsx +13 -5
- package/src/agent-creator/CharacterControls.tsx +7 -3
- package/src/agent-creator/__tests__/support/character-runtime-fixture.tsx +26 -0
- package/src/bottom-sheet/BottomSheetBase.tsx +24 -34
- package/src/bottom-sheet/types.ts +1 -0
- package/src/dialog/DialogBottomSheet.tsx +1 -1
- package/src/surface/SurfacePaint.tsx +14 -4
- package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +9346 -0
- package/src/theme/__tests__/__fixtures__/tonal-glass-primary-failures.json +69 -0
- package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { renderBudget } from './render-budget.mjs';
|
|
2
|
+
|
|
3
|
+
const snapshots = new Map();
|
|
4
|
+
const waiting = new Map();
|
|
5
|
+
const pixelsFor = (canvas) =>
|
|
6
|
+
Math.max(
|
|
7
|
+
64,
|
|
8
|
+
Math.min(
|
|
9
|
+
512,
|
|
10
|
+
Math.round(
|
|
11
|
+
Math.min(canvas.clientWidth, canvas.clientHeight) *
|
|
12
|
+
Math.min(devicePixelRatio || 1, 2),
|
|
13
|
+
),
|
|
14
|
+
),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
// Presentation lifetime is independent of the GPU lease. Overflow avatars keep
|
|
18
|
+
// their real engine pixels and can reclaim a lease through pointer/command input.
|
|
19
|
+
export function createManagedAvatar(canvas, initial, callbacks, factory) {
|
|
20
|
+
let props = initial,
|
|
21
|
+
controller,
|
|
22
|
+
disposed = false,
|
|
23
|
+
ready = false;
|
|
24
|
+
let visible = false,
|
|
25
|
+
hovered = false,
|
|
26
|
+
urgentUntil = 0,
|
|
27
|
+
timer;
|
|
28
|
+
let pendingWork = false,
|
|
29
|
+
pendingReaction = false,
|
|
30
|
+
starting = false;
|
|
31
|
+
let capabilities,
|
|
32
|
+
lastDiagnostics = {},
|
|
33
|
+
cacheKey = '',
|
|
34
|
+
cachedKey = '';
|
|
35
|
+
const still = () =>
|
|
36
|
+
props.portrait ||
|
|
37
|
+
props.paused ||
|
|
38
|
+
props.reduced ||
|
|
39
|
+
props.config.motion === 0;
|
|
40
|
+
const fingerprint = () =>
|
|
41
|
+
JSON.stringify([
|
|
42
|
+
props.config.character,
|
|
43
|
+
props.legacy,
|
|
44
|
+
props.config.lookAt,
|
|
45
|
+
pixelsFor(canvas),
|
|
46
|
+
]);
|
|
47
|
+
const priority = () =>
|
|
48
|
+
pendingWork || pendingReaction || performance.now() < urgentUntil
|
|
49
|
+
? 3
|
|
50
|
+
: hovered
|
|
51
|
+
? 2
|
|
52
|
+
: props.interactive
|
|
53
|
+
? 1
|
|
54
|
+
: 0;
|
|
55
|
+
const refresh = () =>
|
|
56
|
+
lease.update({
|
|
57
|
+
visible: (visible || props.portrait) && !document.hidden,
|
|
58
|
+
still: still(),
|
|
59
|
+
priority: still() ? 0 : priority(),
|
|
60
|
+
needsPaint: !ready,
|
|
61
|
+
});
|
|
62
|
+
function promote(duration = 5000) {
|
|
63
|
+
if (still()) return;
|
|
64
|
+
urgentUntil = performance.now() + duration;
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
timer = setTimeout(refresh, duration + 1);
|
|
67
|
+
refresh();
|
|
68
|
+
}
|
|
69
|
+
function commands() {
|
|
70
|
+
if (!controller?.diagnostics?.()?.ready) return;
|
|
71
|
+
if (pendingWork)
|
|
72
|
+
promote(
|
|
73
|
+
2500 * Math.max(1, Math.min(10, props.workingCycles || 1)) + 1000,
|
|
74
|
+
);
|
|
75
|
+
else if (pendingReaction) promote();
|
|
76
|
+
if (pendingWork) {
|
|
77
|
+
pendingWork = false;
|
|
78
|
+
controller.work?.();
|
|
79
|
+
}
|
|
80
|
+
if (pendingReaction) {
|
|
81
|
+
pendingReaction = false;
|
|
82
|
+
controller.react?.();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const lease = renderBudget.register({
|
|
86
|
+
async start() {
|
|
87
|
+
starting = true;
|
|
88
|
+
try {
|
|
89
|
+
const created = await factory(canvas, props, {
|
|
90
|
+
onCapabilities(value) {
|
|
91
|
+
if (disposed) return;
|
|
92
|
+
capabilities = value;
|
|
93
|
+
callbacks.onCapabilities?.(value);
|
|
94
|
+
},
|
|
95
|
+
onError(error) {
|
|
96
|
+
if (!disposed) lease.fail(error);
|
|
97
|
+
},
|
|
98
|
+
onReady() {
|
|
99
|
+
if (!disposed) queueMicrotask(commands);
|
|
100
|
+
},
|
|
101
|
+
onPaint(settled = true) {
|
|
102
|
+
if (disposed || !settled) return;
|
|
103
|
+
if (!ready) {
|
|
104
|
+
ready = true;
|
|
105
|
+
lease.painted();
|
|
106
|
+
callbacks.onReady?.();
|
|
107
|
+
}
|
|
108
|
+
if (cachedKey !== cacheKey) {
|
|
109
|
+
const image = document.createElement('canvas');
|
|
110
|
+
image.width = canvas.width;
|
|
111
|
+
image.height = canvas.height;
|
|
112
|
+
image.getContext('2d').drawImage(canvas, 0, 0);
|
|
113
|
+
snapshots.delete(cacheKey);
|
|
114
|
+
snapshots.set(cacheKey, { image, capabilities });
|
|
115
|
+
if (snapshots.size > 64)
|
|
116
|
+
snapshots.delete(snapshots.keys().next().value);
|
|
117
|
+
cachedKey = cacheKey;
|
|
118
|
+
for (const receive of waiting.get(cacheKey) ?? []) receive();
|
|
119
|
+
}
|
|
120
|
+
callbacks.onPaint?.();
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
controller = created;
|
|
124
|
+
created.update(props, true);
|
|
125
|
+
return created;
|
|
126
|
+
} finally {
|
|
127
|
+
starting = false;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
suspend(value) {
|
|
131
|
+
lastDiagnostics = value.diagnostics?.() ?? lastDiagnostics;
|
|
132
|
+
value.dispose();
|
|
133
|
+
if (controller === value) controller = undefined;
|
|
134
|
+
},
|
|
135
|
+
error(error) {
|
|
136
|
+
if (!disposed) callbacks.onError?.(error);
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
function paintSnapshot() {
|
|
140
|
+
if (disposed || ready || controller || starting) return;
|
|
141
|
+
const saved = snapshots.get(cacheKey);
|
|
142
|
+
if (!saved) return;
|
|
143
|
+
canvas.width = saved.image.width;
|
|
144
|
+
canvas.height = saved.image.height;
|
|
145
|
+
canvas.getContext('2d').drawImage(saved.image, 0, 0);
|
|
146
|
+
ready = true;
|
|
147
|
+
cachedKey = cacheKey;
|
|
148
|
+
capabilities = saved.capabilities;
|
|
149
|
+
callbacks.onCapabilities?.(capabilities);
|
|
150
|
+
callbacks.onReady?.();
|
|
151
|
+
callbacks.onPaint?.();
|
|
152
|
+
refresh();
|
|
153
|
+
}
|
|
154
|
+
function unwatch() {
|
|
155
|
+
const watchers = waiting.get(cacheKey);
|
|
156
|
+
watchers?.delete(paintSnapshot);
|
|
157
|
+
if (watchers?.size === 0) waiting.delete(cacheKey);
|
|
158
|
+
}
|
|
159
|
+
function appearance() {
|
|
160
|
+
const next = fingerprint();
|
|
161
|
+
if (next === cacheKey) return;
|
|
162
|
+
unwatch();
|
|
163
|
+
cacheKey = next;
|
|
164
|
+
ready = false;
|
|
165
|
+
if (!waiting.has(cacheKey)) waiting.set(cacheKey, new Set());
|
|
166
|
+
waiting.get(cacheKey).add(paintSnapshot);
|
|
167
|
+
paintSnapshot();
|
|
168
|
+
}
|
|
169
|
+
const update = (next) => {
|
|
170
|
+
if (disposed) return;
|
|
171
|
+
const work =
|
|
172
|
+
next.workingKey !== undefined && next.workingKey !== props.workingKey;
|
|
173
|
+
const reaction =
|
|
174
|
+
next.reactionKey !== undefined && next.reactionKey !== props.reactionKey;
|
|
175
|
+
props = next;
|
|
176
|
+
if (still()) {
|
|
177
|
+
pendingWork = false;
|
|
178
|
+
pendingReaction = false;
|
|
179
|
+
}
|
|
180
|
+
appearance();
|
|
181
|
+
const queueCommands = !controller?.diagnostics?.()?.ready;
|
|
182
|
+
if (!still() && (work || reaction)) {
|
|
183
|
+
if (queueCommands) {
|
|
184
|
+
pendingWork ||= work;
|
|
185
|
+
pendingReaction ||= reaction;
|
|
186
|
+
}
|
|
187
|
+
promote(
|
|
188
|
+
work
|
|
189
|
+
? 2500 * Math.max(1, Math.min(10, props.workingCycles || 1)) + 5000
|
|
190
|
+
: 5000,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
controller?.update(props, queueCommands);
|
|
194
|
+
refresh();
|
|
195
|
+
};
|
|
196
|
+
const enter = () => {
|
|
197
|
+
if (props.interactive) {
|
|
198
|
+
hovered = true;
|
|
199
|
+
refresh();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const leave = () => {
|
|
203
|
+
hovered = false;
|
|
204
|
+
refresh();
|
|
205
|
+
};
|
|
206
|
+
const down = (event) => {
|
|
207
|
+
if (!props.interactive || event.button !== 0 || !event.isPrimary || still())
|
|
208
|
+
return;
|
|
209
|
+
// Cold surfaces cannot receive the original pointer-down yet. Replay one
|
|
210
|
+
// original reaction once ready; warm surfaces receive their normal events.
|
|
211
|
+
if (!controller || !controller.diagnostics?.()?.ready)
|
|
212
|
+
pendingReaction = true;
|
|
213
|
+
promote();
|
|
214
|
+
};
|
|
215
|
+
canvas.addEventListener('pointerenter', enter);
|
|
216
|
+
canvas.addEventListener('pointerleave', leave);
|
|
217
|
+
canvas.addEventListener('pointerdown', down, true);
|
|
218
|
+
const intersection = new IntersectionObserver((entries) => {
|
|
219
|
+
visible = entries[0]?.isIntersecting === true;
|
|
220
|
+
refresh();
|
|
221
|
+
});
|
|
222
|
+
intersection.observe(canvas);
|
|
223
|
+
const resize = new ResizeObserver(() => {
|
|
224
|
+
appearance();
|
|
225
|
+
refresh();
|
|
226
|
+
});
|
|
227
|
+
resize.observe(canvas);
|
|
228
|
+
document.addEventListener('visibilitychange', refresh);
|
|
229
|
+
appearance();
|
|
230
|
+
refresh();
|
|
231
|
+
return {
|
|
232
|
+
update,
|
|
233
|
+
diagnostics: () => ({
|
|
234
|
+
...lastDiagnostics,
|
|
235
|
+
...controller?.diagnostics?.(),
|
|
236
|
+
ready,
|
|
237
|
+
pending: !ready || !!controller?.diagnostics?.()?.pending,
|
|
238
|
+
suspended: !controller,
|
|
239
|
+
}),
|
|
240
|
+
dispose() {
|
|
241
|
+
if (disposed) return;
|
|
242
|
+
disposed = true;
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
unwatch();
|
|
245
|
+
intersection.disconnect();
|
|
246
|
+
resize.disconnect();
|
|
247
|
+
canvas.removeEventListener('pointerenter', enter);
|
|
248
|
+
canvas.removeEventListener('pointerleave', leave);
|
|
249
|
+
canvas.removeEventListener('pointerdown', down, true);
|
|
250
|
+
document.removeEventListener('visibilitychange', refresh);
|
|
251
|
+
lease.dispose();
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
export const managedAvatarStats = () => ({
|
|
256
|
+
...renderBudget.stats(),
|
|
257
|
+
snapshots: snapshots.size,
|
|
258
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// One budget per document, including separately imported/versioned adapters.
|
|
2
|
+
// Renderers retain their painted 2D canvas when their GPU lease is released.
|
|
3
|
+
const key = Symbol.for('bloom.character.render-budget');
|
|
4
|
+
export function createRenderBudget(
|
|
5
|
+
limit = 4,
|
|
6
|
+
schedule = (fn) => requestAnimationFrame(fn),
|
|
7
|
+
) {
|
|
8
|
+
const entries = new Set();
|
|
9
|
+
let scheduled = false,
|
|
10
|
+
serial = 0,
|
|
11
|
+
peak = 0;
|
|
12
|
+
const resident = (entry) => entry.starting || entry.controller;
|
|
13
|
+
function wake() {
|
|
14
|
+
if (scheduled) return;
|
|
15
|
+
scheduled = true;
|
|
16
|
+
schedule(pump);
|
|
17
|
+
}
|
|
18
|
+
function stop(entry) {
|
|
19
|
+
entry.wanted = false;
|
|
20
|
+
if (entry.controller) {
|
|
21
|
+
const controller = entry.controller;
|
|
22
|
+
entry.controller = null;
|
|
23
|
+
entry.suspend(controller);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function start(entry) {
|
|
27
|
+
entry.starting = true;
|
|
28
|
+
peak = Math.max(peak, [...entries].filter(resident).length);
|
|
29
|
+
try {
|
|
30
|
+
const controller = await entry.start();
|
|
31
|
+
if (!entry.wanted || entry.removed) entry.suspend(controller);
|
|
32
|
+
else entry.controller = controller;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
entry.failed = true;
|
|
35
|
+
entry.error(error);
|
|
36
|
+
} finally {
|
|
37
|
+
entry.starting = false;
|
|
38
|
+
if (entry.removed) entries.delete(entry);
|
|
39
|
+
wake();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function pump() {
|
|
43
|
+
scheduled = false;
|
|
44
|
+
const eligible = [...entries].filter(
|
|
45
|
+
(e) => !e.removed && !e.failed && e.visible,
|
|
46
|
+
);
|
|
47
|
+
const selected = new Set();
|
|
48
|
+
// Explicit work/touch takes precedence. A single preparing surface keeps
|
|
49
|
+
// large lists from constructing dozens of native renderers in one task.
|
|
50
|
+
const ordered = eligible.sort(
|
|
51
|
+
(a, b) => b.priority - a.priority || b.order - a.order,
|
|
52
|
+
);
|
|
53
|
+
let preparing = [...entries].some((e) => resident(e) && e.needsPaint);
|
|
54
|
+
for (const e of ordered.filter((e) => e.priority >= 2)) {
|
|
55
|
+
if (selected.size === limit) break;
|
|
56
|
+
if (e.needsPaint && !resident(e) && preparing) continue;
|
|
57
|
+
selected.add(e);
|
|
58
|
+
if (e.needsPaint) preparing = true;
|
|
59
|
+
}
|
|
60
|
+
const pending = eligible
|
|
61
|
+
.filter((e) => e.needsPaint && !selected.has(e))
|
|
62
|
+
.sort(
|
|
63
|
+
(a, b) =>
|
|
64
|
+
Number(!!resident(b)) - Number(!!resident(a)) || a.order - b.order,
|
|
65
|
+
);
|
|
66
|
+
if (selected.size < limit && pending.length) {
|
|
67
|
+
const next =
|
|
68
|
+
pending.find((e) => resident(e)) ?? (!preparing ? pending[0] : null);
|
|
69
|
+
if (next) selected.add(next);
|
|
70
|
+
}
|
|
71
|
+
for (const e of ordered
|
|
72
|
+
.filter((e) => !e.needsPaint && !e.still)
|
|
73
|
+
.sort(
|
|
74
|
+
(a, b) =>
|
|
75
|
+
b.priority - a.priority ||
|
|
76
|
+
Number(!!resident(b)) - Number(!!resident(a)) ||
|
|
77
|
+
b.order - a.order,
|
|
78
|
+
)) {
|
|
79
|
+
if (selected.size === limit) break;
|
|
80
|
+
selected.add(e);
|
|
81
|
+
}
|
|
82
|
+
for (const e of entries) if (!selected.has(e)) stop(e);
|
|
83
|
+
let count = [...entries].filter(resident).length;
|
|
84
|
+
for (const e of selected) {
|
|
85
|
+
e.wanted = true;
|
|
86
|
+
if (!resident(e) && count < limit) {
|
|
87
|
+
count++;
|
|
88
|
+
void start(e);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
register(options) {
|
|
95
|
+
const entry = {
|
|
96
|
+
visible: false,
|
|
97
|
+
priority: 0,
|
|
98
|
+
needsPaint: true,
|
|
99
|
+
still: false,
|
|
100
|
+
starting: false,
|
|
101
|
+
controller: null,
|
|
102
|
+
wanted: false,
|
|
103
|
+
removed: false,
|
|
104
|
+
failed: false,
|
|
105
|
+
order: ++serial,
|
|
106
|
+
...options,
|
|
107
|
+
};
|
|
108
|
+
entries.add(entry);
|
|
109
|
+
wake();
|
|
110
|
+
return {
|
|
111
|
+
update(value) {
|
|
112
|
+
Object.assign(entry, value);
|
|
113
|
+
if (value.priority >= 2) entry.order = ++serial;
|
|
114
|
+
wake();
|
|
115
|
+
},
|
|
116
|
+
painted() {
|
|
117
|
+
if (entry.needsPaint) {
|
|
118
|
+
entry.needsPaint = false;
|
|
119
|
+
wake();
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
fail(error) {
|
|
123
|
+
entry.failed = true;
|
|
124
|
+
stop(entry);
|
|
125
|
+
entry.error(error);
|
|
126
|
+
wake();
|
|
127
|
+
},
|
|
128
|
+
dispose() {
|
|
129
|
+
entry.removed = true;
|
|
130
|
+
stop(entry);
|
|
131
|
+
if (!entry.starting) entries.delete(entry);
|
|
132
|
+
wake();
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
stats: () => ({
|
|
137
|
+
limit,
|
|
138
|
+
resident: [...entries].filter(resident).length,
|
|
139
|
+
registered: [...entries].filter((e) => !e.removed).length,
|
|
140
|
+
peak,
|
|
141
|
+
}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export const renderBudget = (globalThis[key] ??= createRenderBudget());
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
import createModule from './orbit-characters.mjs';
|
|
3
3
|
import { customizeAppearance, encodeAppearance } from './appearance-codec.mjs';
|
|
4
4
|
import { acquireLegacyEngine, legacyEngineStats } from './legacy-engine.mjs';
|
|
5
|
+
import { createManagedAvatar, managedAvatarStats } from './managed-avatar.mjs';
|
|
5
6
|
|
|
6
7
|
let modulePromise;
|
|
7
8
|
let serial = 0;
|
|
8
9
|
let frame = 0;
|
|
9
10
|
let lastFrame = -Infinity;
|
|
11
|
+
let nextClient = 0;
|
|
12
|
+
let lastRenderMilliseconds = 0;
|
|
10
13
|
const clients = new Set();
|
|
11
14
|
const category = { shape: 0, color: 1, eyes: 2, eyewear: 3, accessory: 4 };
|
|
12
15
|
function engine() {
|
|
@@ -26,7 +29,18 @@ function tick(time) {
|
|
|
26
29
|
if (document.hidden) return;
|
|
27
30
|
if (time - lastFrame >= 1000 / 30 - 1) {
|
|
28
31
|
lastFrame = time;
|
|
29
|
-
|
|
32
|
+
const batch = [...clients];
|
|
33
|
+
const started = performance.now();
|
|
34
|
+
for (let i = 0; i < batch.length; i++) {
|
|
35
|
+
const index = nextClient % batch.length;
|
|
36
|
+
nextClient = (index + 1) % batch.length;
|
|
37
|
+
const client = batch[index];
|
|
38
|
+
if (clients.has(client)) client.render(time / 1000);
|
|
39
|
+
// One native render cannot be interrupted, but a crowded page must not
|
|
40
|
+
// submit every remaining avatar after it has spent its frame allowance.
|
|
41
|
+
if (performance.now() - started >= 8) break;
|
|
42
|
+
}
|
|
43
|
+
lastRenderMilliseconds = performance.now() - started;
|
|
30
44
|
}
|
|
31
45
|
schedule();
|
|
32
46
|
}
|
|
@@ -41,8 +55,12 @@ export async function createAvatar(canvas, initial, callbacks = {}) {
|
|
|
41
55
|
if (!initial.config.character && !initial.legacy)
|
|
42
56
|
throw new Error('A recovered character recipe is required');
|
|
43
57
|
if (initial.portrait) return createPortrait(canvas, initial, callbacks);
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
return createManagedAvatar(canvas, initial, callbacks, createCharacter);
|
|
59
|
+
}
|
|
60
|
+
function createCharacter(canvas, initial, callbacks) {
|
|
61
|
+
return initial.legacy
|
|
62
|
+
? createLegacyCharacter(canvas, initial, callbacks)
|
|
63
|
+
: createLiveCharacter(canvas, initial, callbacks);
|
|
46
64
|
}
|
|
47
65
|
|
|
48
66
|
async function createLiveCharacter(
|
|
@@ -224,14 +242,15 @@ async function createLiveCharacter(
|
|
|
224
242
|
submitted &&
|
|
225
243
|
fit &&
|
|
226
244
|
!character.preparationStats().pending &&
|
|
227
|
-
!character.hasPendingUpdate()
|
|
245
|
+
!character.hasPendingUpdate() &&
|
|
246
|
+
fingerprint === pendingFingerprint
|
|
228
247
|
) {
|
|
229
248
|
dirty = false;
|
|
230
249
|
if (!ready) {
|
|
231
250
|
ready = true;
|
|
232
251
|
callbacks.onReady?.();
|
|
233
252
|
}
|
|
234
|
-
callbacks.onPaint?.();
|
|
253
|
+
callbacks.onPaint?.(!transitionImage);
|
|
235
254
|
if (activityMode && workDuration && !workTimer) {
|
|
236
255
|
workTimer = setTimeout(() => {
|
|
237
256
|
workTimer = undefined;
|
|
@@ -313,6 +332,14 @@ async function createLiveCharacter(
|
|
|
313
332
|
dirty = true;
|
|
314
333
|
wake();
|
|
315
334
|
};
|
|
335
|
+
const work = () => {
|
|
336
|
+
if (!animate()) return;
|
|
337
|
+
clearTimeout(workTimer);
|
|
338
|
+
switchMode(true);
|
|
339
|
+
play(1);
|
|
340
|
+
workTimer = undefined;
|
|
341
|
+
workDuration = 2200 * Math.max(1, Math.min(10, props.workingCycles || 1));
|
|
342
|
+
};
|
|
316
343
|
let appearanceTimer, pendingFingerprint;
|
|
317
344
|
const applyAppearance = (value) => {
|
|
318
345
|
const recipe = value;
|
|
@@ -415,7 +442,7 @@ async function createLiveCharacter(
|
|
|
415
442
|
dirty = true;
|
|
416
443
|
}
|
|
417
444
|
};
|
|
418
|
-
const update = (next) => {
|
|
445
|
+
const update = (next, silent = false) => {
|
|
419
446
|
if (disposed) return;
|
|
420
447
|
props = next;
|
|
421
448
|
const recipe = props.legacy
|
|
@@ -456,19 +483,16 @@ async function createLiveCharacter(
|
|
|
456
483
|
character.setReducedMotion(false);
|
|
457
484
|
if (
|
|
458
485
|
hasWork &&
|
|
486
|
+
!silent &&
|
|
459
487
|
previousWork !== props.workingKey &&
|
|
460
488
|
props.workingKey !== undefined &&
|
|
461
489
|
animate()
|
|
462
490
|
) {
|
|
463
|
-
|
|
464
|
-
switchMode(true);
|
|
465
|
-
play(1);
|
|
466
|
-
workTimer = undefined;
|
|
467
|
-
// Count visible activity time, not time spent preparing its scene.
|
|
468
|
-
workDuration = 2200 * Math.max(1, Math.min(10, props.workingCycles || 1));
|
|
491
|
+
work();
|
|
469
492
|
}
|
|
470
493
|
if (
|
|
471
494
|
hasWork &&
|
|
495
|
+
!silent &&
|
|
472
496
|
previousReaction !== props.reactionKey &&
|
|
473
497
|
props.reactionKey !== undefined
|
|
474
498
|
)
|
|
@@ -597,6 +621,8 @@ async function createLiveCharacter(
|
|
|
597
621
|
return {
|
|
598
622
|
update,
|
|
599
623
|
dispose,
|
|
624
|
+
react,
|
|
625
|
+
work,
|
|
600
626
|
diagnostics: () => ({
|
|
601
627
|
lastReaction,
|
|
602
628
|
lastReactionKind,
|
|
@@ -619,12 +645,12 @@ async function createLegacyCharacter(canvas, initial, callbacks) {
|
|
|
619
645
|
revision = 0,
|
|
620
646
|
disposed = false,
|
|
621
647
|
latest = initial;
|
|
622
|
-
const update = async (next) => {
|
|
648
|
+
const update = async (next, silent = false) => {
|
|
623
649
|
if (disposed) return;
|
|
624
650
|
latest = next;
|
|
625
651
|
const key = JSON.stringify(next.legacy.points ?? null);
|
|
626
652
|
if (shapeKey === key) {
|
|
627
|
-
controller?.update(next);
|
|
653
|
+
controller?.update(next, silent);
|
|
628
654
|
return;
|
|
629
655
|
}
|
|
630
656
|
const snapshot = controller?.diagnostics().ready
|
|
@@ -661,7 +687,7 @@ async function createLegacyCharacter(canvas, initial, callbacks) {
|
|
|
661
687
|
return;
|
|
662
688
|
}
|
|
663
689
|
controller = created;
|
|
664
|
-
controller.update(latest);
|
|
690
|
+
controller.update(latest, true);
|
|
665
691
|
} catch (error) {
|
|
666
692
|
if (!current()) return;
|
|
667
693
|
shapeKey = undefined;
|
|
@@ -670,8 +696,8 @@ async function createLegacyCharacter(canvas, initial, callbacks) {
|
|
|
670
696
|
};
|
|
671
697
|
await update(initial);
|
|
672
698
|
return {
|
|
673
|
-
update(next) {
|
|
674
|
-
void update(next).catch((error) => {
|
|
699
|
+
update(next, silent = false) {
|
|
700
|
+
void update(next, silent).catch((error) => {
|
|
675
701
|
if (!disposed) callbacks.onError?.(error);
|
|
676
702
|
});
|
|
677
703
|
},
|
|
@@ -680,6 +706,8 @@ async function createLegacyCharacter(canvas, initial, callbacks) {
|
|
|
680
706
|
++revision;
|
|
681
707
|
controller?.dispose();
|
|
682
708
|
},
|
|
709
|
+
react: () => controller?.react(),
|
|
710
|
+
work: () => controller?.work(),
|
|
683
711
|
diagnostics: () => controller?.diagnostics(),
|
|
684
712
|
};
|
|
685
713
|
}
|
|
@@ -746,7 +774,7 @@ function createPortrait(canvas, initial, callbacks) {
|
|
|
746
774
|
};
|
|
747
775
|
cancel = finish;
|
|
748
776
|
try {
|
|
749
|
-
controller = await
|
|
777
|
+
controller = await createManagedAvatar(
|
|
750
778
|
canvas,
|
|
751
779
|
{ ...props, paused: true, interactive: false },
|
|
752
780
|
{
|
|
@@ -772,6 +800,7 @@ function createPortrait(canvas, initial, callbacks) {
|
|
|
772
800
|
queueMicrotask(finish);
|
|
773
801
|
},
|
|
774
802
|
},
|
|
803
|
+
createCharacter,
|
|
775
804
|
);
|
|
776
805
|
if (finished || !current()) {
|
|
777
806
|
controller.dispose();
|
|
@@ -811,6 +840,8 @@ const registryKey = Symbol.for('bloom.character.runtimes');
|
|
|
811
840
|
/** Diagnostics are read on demand; no DOM/debug updates run on the animation clock. */
|
|
812
841
|
export function runtimeStats() {
|
|
813
842
|
return {
|
|
843
|
+
renderMilliseconds: lastRenderMilliseconds,
|
|
844
|
+
budget: managedAvatarStats(),
|
|
814
845
|
instances: instances.size,
|
|
815
846
|
active: clients.size,
|
|
816
847
|
scheduled: !!frame,
|
package/docs/adoption-matrix.mdx
CHANGED
|
@@ -113,7 +113,7 @@ The classification is an EQUALITY in `src/__tests__/adoption-matrix.test.ts`, no
|
|
|
113
113
|
| `activity-heatmap` | `@oxy.so/bloom/activity-heatmap` + barrel | universal | `activity-heatmap.mdx` | yes | 1 | | | | |
|
|
114
114
|
| `address` | `@oxy.so/bloom/address` + barrel | universal | `address.mdx` | yes | 2 | n/a¹ | | | |
|
|
115
115
|
| `admonition` | `@oxy.so/bloom/admonition` + barrel | universal | `admonition.mdx` | yes | 1 | | | | |
|
|
116
|
-
| `agent-avatar` | `@oxy.so/bloom/agent-avatar` + barrel | universal | `agent-avatar.mdx` | yes |
|
|
116
|
+
| `agent-avatar` | `@oxy.so/bloom/agent-avatar` + barrel | universal | `agent-avatar.mdx` | yes | 15 | | | | |
|
|
117
117
|
| `agent-chat` | `@oxy.so/bloom/agent-chat` + barrel | web fork | `agent-chat.mdx` | yes | 3 | n/a¹ | | | reads |
|
|
118
118
|
| `agent-creator` | `@oxy.so/bloom/agent-creator` + barrel | web fork | `agent-creator.mdx` | yes | 5 | reads | | | |
|
|
119
119
|
| `agent-limits-card` | `@oxy.so/bloom/agent-limits-card` + barrel | universal | `agent-limits-card.mdx` | yes | 2 | | | | reads |
|
|
@@ -294,7 +294,7 @@ The classification is an EQUALITY in `src/__tests__/adoption-matrix.test.ts`, no
|
|
|
294
294
|
| `sticker` | `@oxy.so/bloom/sticker` + barrel | web fork | `sticker.mdx` | yes | 1 | | | | |
|
|
295
295
|
| `styles` | `@oxy.so/bloom/styles` + barrel | universal | `styles.mdx` | yes | 85 | n/a¹ | | | |
|
|
296
296
|
| `subtle-hover` | `@oxy.so/bloom/subtle-hover` + barrel | universal | `subtle-hover.mdx` | yes | 1 | | | | |
|
|
297
|
-
| `surface` | `@oxy.so/bloom/surface` + barrel | web fork | `surface.mdx` | yes |
|
|
297
|
+
| `surface` | `@oxy.so/bloom/surface` + barrel | web fork | `surface.mdx` | yes | 39 | | | | |
|
|
298
298
|
| `surfaces` | `@oxy.so/bloom/surfaces` + barrel | web fork | `alert.mdx` | yes | 3 | | | | |
|
|
299
299
|
| `swipe-row` | `@oxy.so/bloom/swipe-row` + barrel | universal | `swipe-row.mdx` | yes | 2 | | | | |
|
|
300
300
|
| `switch` | `@oxy.so/bloom/switch` + barrel | universal | `switch.mdx` | yes | 7 | reads | reads | | reads |
|
|
@@ -307,7 +307,7 @@ The classification is an EQUALITY in `src/__tests__/adoption-matrix.test.ts`, no
|
|
|
307
307
|
| `tenancy` | `@oxy.so/bloom/tenancy` + barrel | universal | `tenancy.mdx` | yes | 5 | | reads | | |
|
|
308
308
|
| `text-field` | `@oxy.so/bloom/text-field` + barrel | universal | `text-field.mdx` | yes | 17 | reads | reads | | |
|
|
309
309
|
| `textarea` | `@oxy.so/bloom/textarea` + barrel | universal | `textarea.mdx` | yes | 4 | reads | reads | | n/a¹ |
|
|
310
|
-
| `theme` | `@oxy.so/bloom/theme` + barrel | web fork | `theme.mdx` | yes |
|
|
310
|
+
| `theme` | `@oxy.so/bloom/theme` + barrel | web fork | `theme.mdx` | yes | 354 | | | n/a¹ | reads |
|
|
311
311
|
| `theme-toggle` | `@oxy.so/bloom/theme-toggle` + barrel | universal | `theme-toggle.mdx` | yes | 2 | n/a¹ | | | |
|
|
312
312
|
| `toast` | `@oxy.so/bloom/toast` + barrel | universal | `toast.mdx` | yes | 17 | | | | |
|
|
313
313
|
| `tooltip` | `@oxy.so/bloom/tooltip` + barrel | web fork | `tooltip.mdx` | yes | 1 | | | | |
|
package/docs/agent-avatar.mdx
CHANGED
|
@@ -38,12 +38,21 @@ Alien avatars have been removed from the available catalog. Saved aliens, mascot
|
|
|
38
38
|
|
|
39
39
|
`paused`, reduced motion and `motion: 0` stop animation after a complete appearance frame. `portrait` prepares a static thumbnail. The beta can report readiness while drawing transparency if initialized with its reduced-motion flag; the adapter waits for painted pixels and then freezes its clock instead.
|
|
40
40
|
|
|
41
|
-
Web shares the beta engine and one animation clock per document
|
|
41
|
+
Web shares the beta engine and one animation clock per document, with a shared budget of **four live renderers** across original and migrated avatars. Additional avatars retain real 3D pixels from their last completed frame while their GPU renderer is released. Pointer interaction and React/Work commands prioritize reclaiming a renderer; overflow images remain visible while that renderer prepares. A newly visible avatar without a cached frame waits for its first rendering slot. This bounds graphics resources; the recovered engine’s synchronous first preparation can still cause a long task, especially for uncached migrated contours. Leaving the viewport or hiding the document releases the renderer lease, and returning avatars can display their retained image before resuming animation.
|
|
42
|
+
|
|
43
|
+
Different migrated contours isolate their rendering modules because the recovered engine caches geometry by its original appearance key; they share one preparation worker. Unused contour modules are released. Use `portrait` for galleries and selectors. Static preparation is serialized and the image cache is bounded to 64 entries; unmount releases listeners and rendering resources. Storybook’s **Many Avatars** story exercises 48 original/migrated avatars with per-avatar React/Work controls and document scrolling.
|
|
44
|
+
|
|
45
|
+
In the repository, run the scheduler tests and the browser budget check (with Storybook running):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
node --test scripts/test-avatar-render-budget.mjs
|
|
49
|
+
node scripts/verify-avatar-render-budget.mjs
|
|
50
|
+
```
|
|
42
51
|
|
|
43
52
|
`className` and `style` apply to the outer layout node through Bloom's NativeWind wrapper. `label` defaults to the translated “Agent avatar”; `locale` overrides the scope and `accessibilityLabel` overrides the name. Prefer a meaningful name when the avatar identifies an agent.
|
|
44
53
|
|
|
45
54
|
## Native and fallback
|
|
46
55
|
|
|
47
|
-
The optional native bridge uses `react-native-webview` and requires an absolute runtime URL reachable by the device. Web is verified in Chrome; physical-device validation remains pending. Separate WebViews cannot share engine memory or
|
|
56
|
+
The optional native bridge uses `react-native-webview` and requires an absolute runtime URL reachable by the device. Web is verified in Chrome; physical-device validation remains pending. Each native WebView owns a separate document, so the four-renderer web budget is **not an application-wide native limit**. Separate WebViews cannot share engine memory, graphics contexts or the web document’s viewport reclamation.
|
|
48
57
|
|
|
49
58
|
An unavailable runtime or missing native peer displays the translated unavailable state. Consumers without a runtime provider retain the universal SVG renderer for procedural recipes. That renderer preserves the original contours, folds, expression springs and deterministic grain; it does not support the beta's new 3D parts.
|
package/docs/bottom-sheet.mdx
CHANGED
|
@@ -56,7 +56,7 @@ On web the open sheet owns the keyboard like a dialog does (`overlay/ModalKeyboa
|
|
|
56
56
|
| `onDismiss?` | `() => void` | — | Fires when the sheet finishes closing. |
|
|
57
57
|
| `enablePanDownToClose?` | `boolean` | `true` | |
|
|
58
58
|
| `enableHandlePanningGesture?` | `boolean` | `true` | |
|
|
59
|
-
| `onDismissAttempt?` | `() => boolean` | — | Return `false` to veto
|
|
59
|
+
| `onDismissAttempt?` | `() => boolean` | — | Return `false` to veto user Back/backdrop/Escape/pan before closing. Explicit ref dismissal bypasses this guard and always reports completion. |
|
|
60
60
|
| `detached?` | `boolean` | `false` | Float with horizontal margins and rounded corners on all sides. |
|
|
61
61
|
| `showHandle?` | `boolean` | `true` | Toggle the drag handle pill. |
|
|
62
62
|
| `backdropOpacity?` | `number` | `0.5` | Opacity (0–1) of the dimming backdrop once fully visible. |
|
|
@@ -53,12 +53,17 @@ function AgentAvatarProvider({
|
|
|
53
53
|
}, [runtimeUrl]);
|
|
54
54
|
const value = (0, _react.useMemo)(() => ({
|
|
55
55
|
runtimeUrl,
|
|
56
|
-
capabilitiesByKey,
|
|
57
56
|
reportCapabilities
|
|
58
|
-
}), [runtimeUrl,
|
|
57
|
+
}), [runtimeUrl, reportCapabilities]);
|
|
58
|
+
const catalog = (0, _react.useMemo)(() => ({
|
|
59
|
+
capabilitiesByKey
|
|
60
|
+
}), [capabilitiesByKey]);
|
|
59
61
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_context.CharacterRuntimeContext.Provider, {
|
|
60
62
|
value: value,
|
|
61
|
-
children:
|
|
63
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_context.CharacterCapabilitiesContext.Provider, {
|
|
64
|
+
value: catalog,
|
|
65
|
+
children: children
|
|
66
|
+
})
|
|
62
67
|
});
|
|
63
68
|
}
|
|
64
69
|
//# sourceMappingURL=AgentAvatarProvider.js.map
|