@oxy.so/bloom 7.1.2 → 7.1.4
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/README.md +114 -19
- package/assets/character-runtime/authored-parts.mjs +754 -0
- package/assets/character-runtime/catalog-parts.mjs +362 -0
- package/assets/character-runtime/character-recipe.mjs +247 -0
- package/assets/character-runtime/clippo-geometry.mjs +679 -0
- package/assets/character-runtime/controller-mode.mjs +208 -0
- package/assets/character-runtime/cyclops-geometry.mjs +180 -0
- package/assets/character-runtime/legacy-engine.mjs +406 -54
- package/assets/character-runtime/legacy-geometry.mjs +242 -85
- package/assets/character-runtime/legacy-worker.mjs +82 -23
- package/assets/character-runtime/managed-avatar.mjs +12 -17
- package/assets/character-runtime/migrated-contours.mjs +2067 -0
- package/assets/character-runtime/migrated-faces.mjs +59 -0
- package/assets/character-runtime/pose-transition.mjs +523 -0
- package/assets/character-runtime/preparation-cache.mjs +60 -0
- package/assets/character-runtime/render-budget.mjs +5 -3
- package/assets/character-runtime/render-errors.mjs +180 -0
- package/assets/character-runtime/resolution.mjs +10 -0
- package/assets/character-runtime/runtime.mjs +744 -295
- package/assets/character-runtime/sha256.mjs +44 -0
- package/assets/character-runtime/shared-programs.mjs +534 -0
- package/assets/character-runtime/shared-surface.mjs +490 -0
- package/assets/character-runtime/shared-textures.mjs +420 -0
- package/assets/character-runtime/thumbnails/eyes/todd.png +0 -0
- package/docs/agent-avatar.mdx +45 -8
- package/docs/agent-creator.mdx +13 -3
- package/lib/commonjs/agent-avatar/AgentAvatar.js +1 -1
- package/lib/commonjs/agent-avatar/AgentAvatar.js.map +1 -1
- package/lib/commonjs/agent-avatar/CharacterAvatar.js +2 -1
- package/lib/commonjs/agent-avatar/CharacterAvatar.js.map +1 -1
- package/lib/commonjs/agent-avatar/character-colors.js +19 -0
- package/lib/commonjs/agent-avatar/character-colors.js.map +1 -0
- package/lib/commonjs/agent-avatar/character-shapes.js +106 -0
- package/lib/commonjs/agent-avatar/character-shapes.js.map +1 -0
- package/lib/commonjs/agent-avatar/config-character.js +4 -0
- package/lib/commonjs/agent-avatar/config-character.js.map +1 -1
- package/lib/commonjs/agent-avatar/legacy-recipe.js +19 -6
- package/lib/commonjs/agent-avatar/legacy-recipe.js.map +1 -1
- package/lib/commonjs/agent-creator/AgentCreatorBase.js +67 -107
- package/lib/commonjs/agent-creator/AgentCreatorBase.js.map +1 -1
- package/lib/commonjs/agent-creator/CharacterControls.js +23 -5
- package/lib/commonjs/agent-creator/CharacterControls.js.map +1 -1
- package/lib/commonjs/agent-creator/ClippoArtwork.js +63 -0
- package/lib/commonjs/agent-creator/ClippoArtwork.js.map +1 -0
- package/lib/commonjs/agent-creator/CyclopsArtwork.js +45 -0
- package/lib/commonjs/agent-creator/CyclopsArtwork.js.map +1 -0
- package/lib/commonjs/agent-creator/EmotionPicker.js +8 -1
- package/lib/commonjs/agent-creator/EmotionPicker.js.map +1 -1
- package/lib/commonjs/agent-creator/ShapeSilhouette.js +8 -1
- package/lib/commonjs/agent-creator/ShapeSilhouette.js.map +1 -1
- package/lib/commonjs/agent-creator/ToddArtwork.js +29 -0
- package/lib/commonjs/agent-creator/ToddArtwork.js.map +1 -0
- package/lib/commonjs/agent-creator/constants.js +16 -17
- package/lib/commonjs/agent-creator/constants.js.map +1 -1
- package/lib/commonjs/agent-creator/messages.js +1 -1
- package/lib/commonjs/agent-creator/messages.js.map +1 -1
- package/lib/commonjs/locale/translations/ar.js +1 -1
- package/lib/commonjs/locale/translations/ar.js.map +1 -1
- package/lib/commonjs/locale/translations/bn.js +1 -1
- package/lib/commonjs/locale/translations/bn.js.map +1 -1
- package/lib/commonjs/locale/translations/ca.js +1 -1
- package/lib/commonjs/locale/translations/ca.js.map +1 -1
- package/lib/commonjs/locale/translations/de.js +1 -1
- package/lib/commonjs/locale/translations/de.js.map +1 -1
- package/lib/commonjs/locale/translations/es.js +1 -1
- package/lib/commonjs/locale/translations/es.js.map +1 -1
- package/lib/commonjs/locale/translations/fr.js +1 -1
- package/lib/commonjs/locale/translations/fr.js.map +1 -1
- package/lib/commonjs/locale/translations/hi.js +1 -1
- package/lib/commonjs/locale/translations/hi.js.map +1 -1
- package/lib/commonjs/locale/translations/id.js +1 -1
- package/lib/commonjs/locale/translations/id.js.map +1 -1
- package/lib/commonjs/locale/translations/it.js +1 -1
- package/lib/commonjs/locale/translations/it.js.map +1 -1
- package/lib/commonjs/locale/translations/ja.js +1 -1
- package/lib/commonjs/locale/translations/ja.js.map +1 -1
- package/lib/commonjs/locale/translations/pt.js +1 -1
- package/lib/commonjs/locale/translations/pt.js.map +1 -1
- package/lib/commonjs/locale/translations/ru.js +1 -1
- package/lib/commonjs/locale/translations/ru.js.map +1 -1
- package/lib/commonjs/locale/translations/tr.js +1 -1
- package/lib/commonjs/locale/translations/tr.js.map +1 -1
- package/lib/commonjs/locale/translations/zh.js +1 -1
- package/lib/commonjs/locale/translations/zh.js.map +1 -1
- package/lib/module/agent-avatar/AgentAvatar.js +1 -1
- package/lib/module/agent-avatar/AgentAvatar.js.map +1 -1
- package/lib/module/agent-avatar/CharacterAvatar.js +2 -1
- package/lib/module/agent-avatar/CharacterAvatar.js.map +1 -1
- package/lib/module/agent-avatar/character-colors.js +15 -0
- package/lib/module/agent-avatar/character-colors.js.map +1 -0
- package/lib/module/agent-avatar/character-shapes.js +97 -0
- package/lib/module/agent-avatar/character-shapes.js.map +1 -0
- package/lib/module/agent-avatar/config-character.js +4 -0
- package/lib/module/agent-avatar/config-character.js.map +1 -1
- package/lib/module/agent-avatar/legacy-recipe.js +19 -6
- package/lib/module/agent-avatar/legacy-recipe.js.map +1 -1
- package/lib/module/agent-creator/AgentCreatorBase.js +69 -109
- package/lib/module/agent-creator/AgentCreatorBase.js.map +1 -1
- package/lib/module/agent-creator/CharacterControls.js +24 -6
- package/lib/module/agent-creator/CharacterControls.js.map +1 -1
- package/lib/module/agent-creator/ClippoArtwork.js +59 -0
- package/lib/module/agent-creator/ClippoArtwork.js.map +1 -0
- package/lib/module/agent-creator/CyclopsArtwork.js +41 -0
- package/lib/module/agent-creator/CyclopsArtwork.js.map +1 -0
- package/lib/module/agent-creator/EmotionPicker.js +8 -1
- package/lib/module/agent-creator/EmotionPicker.js.map +1 -1
- package/lib/module/agent-creator/ShapeSilhouette.js +8 -1
- package/lib/module/agent-creator/ShapeSilhouette.js.map +1 -1
- package/lib/module/agent-creator/ToddArtwork.js +25 -0
- package/lib/module/agent-creator/ToddArtwork.js.map +1 -0
- package/lib/module/agent-creator/constants.js +8 -16
- package/lib/module/agent-creator/constants.js.map +1 -1
- package/lib/module/agent-creator/messages.js +1 -1
- package/lib/module/agent-creator/messages.js.map +1 -1
- package/lib/module/locale/translations/ar.js +1 -1
- package/lib/module/locale/translations/ar.js.map +1 -1
- package/lib/module/locale/translations/bn.js +1 -1
- package/lib/module/locale/translations/bn.js.map +1 -1
- package/lib/module/locale/translations/ca.js +1 -1
- package/lib/module/locale/translations/ca.js.map +1 -1
- package/lib/module/locale/translations/de.js +1 -1
- package/lib/module/locale/translations/de.js.map +1 -1
- package/lib/module/locale/translations/es.js +1 -1
- package/lib/module/locale/translations/es.js.map +1 -1
- package/lib/module/locale/translations/fr.js +1 -1
- package/lib/module/locale/translations/fr.js.map +1 -1
- package/lib/module/locale/translations/hi.js +1 -1
- package/lib/module/locale/translations/hi.js.map +1 -1
- package/lib/module/locale/translations/id.js +1 -1
- package/lib/module/locale/translations/id.js.map +1 -1
- package/lib/module/locale/translations/it.js +1 -1
- package/lib/module/locale/translations/it.js.map +1 -1
- package/lib/module/locale/translations/ja.js +1 -1
- package/lib/module/locale/translations/ja.js.map +1 -1
- package/lib/module/locale/translations/pt.js +1 -1
- package/lib/module/locale/translations/pt.js.map +1 -1
- package/lib/module/locale/translations/ru.js +1 -1
- package/lib/module/locale/translations/ru.js.map +1 -1
- package/lib/module/locale/translations/tr.js +1 -1
- package/lib/module/locale/translations/tr.js.map +1 -1
- package/lib/module/locale/translations/zh.js +1 -1
- package/lib/module/locale/translations/zh.js.map +1 -1
- package/lib/typescript/commonjs/agent-avatar/CharacterAvatar.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-avatar/character-colors.d.ts +13 -0
- package/lib/typescript/commonjs/agent-avatar/character-colors.d.ts.map +1 -0
- package/lib/typescript/commonjs/agent-avatar/character-shapes.d.ts +19 -0
- package/lib/typescript/commonjs/agent-avatar/character-shapes.d.ts.map +1 -0
- package/lib/typescript/commonjs/agent-avatar/config-character.d.ts +4 -0
- package/lib/typescript/commonjs/agent-avatar/config-character.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-avatar/legacy-recipe.d.ts +2 -10
- package/lib/typescript/commonjs/agent-avatar/legacy-recipe.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 +2 -1
- package/lib/typescript/commonjs/agent-creator/CharacterControls.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-creator/ClippoArtwork.d.ts +6 -0
- package/lib/typescript/commonjs/agent-creator/ClippoArtwork.d.ts.map +1 -0
- package/lib/typescript/commonjs/agent-creator/CyclopsArtwork.d.ts +5 -0
- package/lib/typescript/commonjs/agent-creator/CyclopsArtwork.d.ts.map +1 -0
- package/lib/typescript/commonjs/agent-creator/EmotionPicker.d.ts +1 -0
- package/lib/typescript/commonjs/agent-creator/EmotionPicker.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-creator/ShapeSilhouette.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-creator/ToddArtwork.d.ts +6 -0
- package/lib/typescript/commonjs/agent-creator/ToddArtwork.d.ts.map +1 -0
- package/lib/typescript/commonjs/agent-creator/constants.d.ts +7 -15
- package/lib/typescript/commonjs/agent-creator/constants.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-creator/messages.d.ts.map +1 -1
- package/lib/typescript/module/agent-avatar/CharacterAvatar.d.ts.map +1 -1
- package/lib/typescript/module/agent-avatar/character-colors.d.ts +13 -0
- package/lib/typescript/module/agent-avatar/character-colors.d.ts.map +1 -0
- package/lib/typescript/module/agent-avatar/character-shapes.d.ts +19 -0
- package/lib/typescript/module/agent-avatar/character-shapes.d.ts.map +1 -0
- package/lib/typescript/module/agent-avatar/config-character.d.ts +4 -0
- package/lib/typescript/module/agent-avatar/config-character.d.ts.map +1 -1
- package/lib/typescript/module/agent-avatar/legacy-recipe.d.ts +2 -10
- package/lib/typescript/module/agent-avatar/legacy-recipe.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 +2 -1
- package/lib/typescript/module/agent-creator/CharacterControls.d.ts.map +1 -1
- package/lib/typescript/module/agent-creator/ClippoArtwork.d.ts +6 -0
- package/lib/typescript/module/agent-creator/ClippoArtwork.d.ts.map +1 -0
- package/lib/typescript/module/agent-creator/CyclopsArtwork.d.ts +5 -0
- package/lib/typescript/module/agent-creator/CyclopsArtwork.d.ts.map +1 -0
- package/lib/typescript/module/agent-creator/EmotionPicker.d.ts +1 -0
- package/lib/typescript/module/agent-creator/EmotionPicker.d.ts.map +1 -1
- package/lib/typescript/module/agent-creator/ShapeSilhouette.d.ts.map +1 -1
- package/lib/typescript/module/agent-creator/ToddArtwork.d.ts +6 -0
- package/lib/typescript/module/agent-creator/ToddArtwork.d.ts.map +1 -0
- package/lib/typescript/module/agent-creator/constants.d.ts +7 -15
- package/lib/typescript/module/agent-creator/constants.d.ts.map +1 -1
- package/lib/typescript/module/agent-creator/messages.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agent-avatar/AgentAvatar.tsx +1 -1
- package/src/agent-avatar/CharacterAvatar.tsx +4 -1
- package/src/agent-avatar/character-colors.ts +12 -0
- package/src/agent-avatar/character-shapes.ts +122 -0
- package/src/agent-avatar/config-character.ts +39 -6
- package/src/agent-avatar/legacy-recipe.ts +34 -6
- package/src/agent-creator/AgentCreatorBase.tsx +121 -152
- package/src/agent-creator/CharacterControls.tsx +39 -14
- package/src/agent-creator/ClippoArtwork.tsx +49 -0
- package/src/agent-creator/CyclopsArtwork.tsx +27 -0
- package/src/agent-creator/EmotionPicker.tsx +55 -16
- package/src/agent-creator/ShapeSilhouette.tsx +16 -1
- package/src/agent-creator/ToddArtwork.tsx +24 -0
- package/src/agent-creator/constants.ts +47 -14
- package/src/agent-creator/messages.ts +2 -1
- package/src/locale/translations/ar.ts +1 -1
- package/src/locale/translations/bn.ts +1 -1
- package/src/locale/translations/ca.ts +1 -1
- package/src/locale/translations/de.ts +1 -1
- package/src/locale/translations/es.ts +1 -1
- package/src/locale/translations/fr.ts +1 -1
- package/src/locale/translations/hi.ts +1 -1
- package/src/locale/translations/id.ts +1 -1
- package/src/locale/translations/it.ts +1 -1
- package/src/locale/translations/ja.ts +1 -1
- package/src/locale/translations/pt.ts +1 -1
- package/src/locale/translations/ru.ts +1 -1
- package/src/locale/translations/tr.ts +1 -1
- package/src/locale/translations/zh.ts +1 -1
- package/src/__tests__/support/adoption-matrix.ts +0 -683
- package/src/__tests__/support/card-surface.ts +0 -25
- package/src/__tests__/support/collision-fixture-barrel.ts +0 -20
- package/src/__tests__/support/commerce-harness.tsx +0 -97
- package/src/__tests__/support/composite.ts +0 -68
- package/src/__tests__/support/constructed-style-sheets.ts +0 -68
- package/src/__tests__/support/messages-in.ts +0 -10
- package/src/__tests__/support/press-host.ts +0 -30
- package/src/__tests__/support/rendered-style.ts +0 -99
- package/src/__tests__/support/unread-hook-fixture.ts +0 -33
- package/src/__tests__/support/worklet-capture-fixture.tsx +0 -28
- package/src/agent-avatar/__tests__/artwork-golden.json +0 -774
- package/src/agent-creator/__tests__/support/character-runtime-fixture.tsx +0 -26
- package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +0 -9346
- package/src/theme/__tests__/__fixtures__/tonal-glass-primary-failures.json +0 -69
- package/src/theme/__tests__/fixtures/color-engine-golden.json +0 -1
|
@@ -1,48 +1,147 @@
|
|
|
1
1
|
// Optional renderer adapter. The generated engine and data alongside it remain unmodified.
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import {
|
|
3
|
+
customizeAppearance,
|
|
4
|
+
encodeAppearance,
|
|
5
|
+
decodeAppearance,
|
|
6
|
+
} from './appearance-codec.mjs';
|
|
7
|
+
import {
|
|
8
|
+
acquireLegacyEngine,
|
|
9
|
+
legacyEngineStats,
|
|
10
|
+
getCharacterEngine,
|
|
11
|
+
acquireCharacterPreparation,
|
|
12
|
+
} from './legacy-engine.mjs';
|
|
5
13
|
import { createManagedAvatar, managedAvatarStats } from './managed-avatar.mjs';
|
|
14
|
+
import {
|
|
15
|
+
acquireSharedSurface,
|
|
16
|
+
sharedSurfaceStats,
|
|
17
|
+
renderSharedBatchDeferred,
|
|
18
|
+
waitForSharedRender,
|
|
19
|
+
sharedRenderPending,
|
|
20
|
+
} from './shared-surface.mjs';
|
|
21
|
+
import { characterPixels } from './resolution.mjs';
|
|
22
|
+
import {
|
|
23
|
+
characterRecipe,
|
|
24
|
+
authoredPartsFor,
|
|
25
|
+
bodySignatureFor,
|
|
26
|
+
bodySelectionDefaults,
|
|
27
|
+
NATIVE_PARTS,
|
|
28
|
+
ORIGINAL_PRESETS,
|
|
29
|
+
MIGRATED_SHAPE_IDS,
|
|
30
|
+
isLegacyRecipe,
|
|
31
|
+
withCharacterGeometry,
|
|
32
|
+
} from './character-recipe.mjs';
|
|
33
|
+
import { originalBodyColor, originalPalettePaint } from './catalog-parts.mjs';
|
|
6
34
|
|
|
7
|
-
let modulePromise;
|
|
8
|
-
let serial = 0;
|
|
9
35
|
let frame = 0;
|
|
10
|
-
let
|
|
36
|
+
let rendering = false;
|
|
11
37
|
let nextClient = 0;
|
|
12
38
|
let lastRenderMilliseconds = 0;
|
|
39
|
+
let lastRenderWaitMilliseconds = 0;
|
|
40
|
+
let renderCost = 8;
|
|
41
|
+
let attentionTurn = false;
|
|
13
42
|
const clients = new Set();
|
|
14
43
|
const category = { shape: 0, color: 1, eyes: 2, eyewear: 3, accessory: 4 };
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
modulePromise = undefined;
|
|
20
|
-
throw error;
|
|
21
|
-
}));
|
|
22
|
-
}
|
|
44
|
+
const appearanceFingerprint = (recipe) => {
|
|
45
|
+
const { eyeSpacing: _historicalSpacing, ...appearance } = recipe;
|
|
46
|
+
return JSON.stringify(appearance);
|
|
47
|
+
};
|
|
23
48
|
function schedule() {
|
|
24
|
-
if (!frame && clients.size && !document.hidden)
|
|
49
|
+
if (!frame && !rendering && clients.size && !document.hidden)
|
|
25
50
|
frame = requestAnimationFrame(tick);
|
|
26
51
|
}
|
|
27
|
-
function tick(time) {
|
|
52
|
+
async function tick(time) {
|
|
28
53
|
frame = 0;
|
|
29
54
|
if (document.hidden) return;
|
|
30
|
-
|
|
31
|
-
|
|
55
|
+
rendering = true;
|
|
56
|
+
try {
|
|
32
57
|
const batch = [...clients];
|
|
33
58
|
const started = performance.now();
|
|
59
|
+
const presentations = [];
|
|
60
|
+
// Await the GPU without blocking the page. Bound CPU work and batch size
|
|
61
|
+
// separately: driver wait time must not collapse every batch to one draw.
|
|
62
|
+
const batchLimit = Math.min(8, Math.max(1, Math.ceil(8 / renderCost)));
|
|
63
|
+
let submittedAt = started,
|
|
64
|
+
presentationCost = 0;
|
|
65
|
+
const touched = [];
|
|
66
|
+
// Preparation is globally serialized by the recovered engine. Its owner
|
|
67
|
+
// must get a chance to commit and paint every frame; waiting behind every
|
|
68
|
+
// resident avatar makes startup latency grow quadratically with the crowd.
|
|
69
|
+
const preparing = batch.find((client) => client.preparing());
|
|
70
|
+
// Give the most recent interaction a responsive cadence, but reserve
|
|
71
|
+
// alternate turns for the round-robin crowd so no avatar is starved.
|
|
72
|
+
const attended = batch
|
|
73
|
+
.filter((client) => client.attention(time) > 0)
|
|
74
|
+
.sort((a, b) => b.attention(time) - a.attention(time))[0];
|
|
75
|
+
const preferred = preparing ?? (attentionTurn ? attended : undefined);
|
|
76
|
+
attentionTurn = !attentionTurn;
|
|
77
|
+
const ordered = preferred ? [preferred] : [];
|
|
34
78
|
for (let i = 0; i < batch.length; i++) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
79
|
+
const client = batch[(nextClient + i) % batch.length];
|
|
80
|
+
if (client !== preferred) ordered.push(client);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
await renderSharedBatchDeferred(
|
|
84
|
+
() => {
|
|
85
|
+
for (const client of ordered) {
|
|
86
|
+
if (client !== preferred)
|
|
87
|
+
nextClient = (batch.indexOf(client) + 1) % batch.length;
|
|
88
|
+
// Rate-limit each character, not the whole document. Crowds can use
|
|
89
|
+
// intervening browser frames without forcing every avatar down to 30/N.
|
|
90
|
+
if (
|
|
91
|
+
clients.has(client) &&
|
|
92
|
+
(client === preparing ||
|
|
93
|
+
time - (client.lastTick ?? -Infinity) >= 1000 / 30 - 1)
|
|
94
|
+
) {
|
|
95
|
+
client.lastTick = time;
|
|
96
|
+
touched.push(client);
|
|
97
|
+
const present = client.render(time / 1000);
|
|
98
|
+
if (present) presentations.push(present);
|
|
99
|
+
}
|
|
100
|
+
// One native render cannot be interrupted, but a crowded page must not
|
|
101
|
+
// submit every remaining avatar after it has spent its frame allowance.
|
|
102
|
+
if (
|
|
103
|
+
presentations.length >= batchLimit ||
|
|
104
|
+
performance.now() - started >= 8
|
|
105
|
+
)
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
submittedAt = performance.now();
|
|
109
|
+
},
|
|
110
|
+
() => {
|
|
111
|
+
const began = performance.now();
|
|
112
|
+
for (const present of presentations) present();
|
|
113
|
+
presentationCost = performance.now() - began;
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
// A context-level error cannot be attributed after a shared submission.
|
|
118
|
+
// Discard every pending image and report it to every touched character.
|
|
119
|
+
presentations.length = 0;
|
|
120
|
+
for (const client of touched) {
|
|
121
|
+
try {
|
|
122
|
+
client.fail(error);
|
|
123
|
+
} catch (callbackError) {
|
|
124
|
+
queueMicrotask(() => {
|
|
125
|
+
throw callbackError;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
lastRenderMilliseconds = submittedAt - started + presentationCost;
|
|
131
|
+
lastRenderWaitMilliseconds = Math.max(
|
|
132
|
+
0,
|
|
133
|
+
performance.now() - started - lastRenderMilliseconds,
|
|
134
|
+
);
|
|
135
|
+
if (presentations.length) {
|
|
136
|
+
const measured = lastRenderMilliseconds / presentations.length;
|
|
137
|
+
// React immediately to a slower GPU; recover capacity gradually to avoid
|
|
138
|
+
// alternating huge batches and long blocked browser frames.
|
|
139
|
+
renderCost = Math.max(measured, renderCost * 0.9 + measured * 0.1, 0.1);
|
|
42
140
|
}
|
|
43
|
-
|
|
141
|
+
} finally {
|
|
142
|
+
rendering = false;
|
|
143
|
+
schedule();
|
|
44
144
|
}
|
|
45
|
-
schedule();
|
|
46
145
|
}
|
|
47
146
|
function visibility() {
|
|
48
147
|
for (const client of instances) client.visibility();
|
|
@@ -50,7 +149,7 @@ function visibility() {
|
|
|
50
149
|
}
|
|
51
150
|
const instances = new Set();
|
|
52
151
|
|
|
53
|
-
/** Original engine and shared animation clock;
|
|
152
|
+
/** Original engine and shared animation clock; scoped geometry identities stay distinct. */
|
|
54
153
|
export async function createAvatar(canvas, initial, callbacks = {}) {
|
|
55
154
|
if (!initial.config.character && !initial.legacy)
|
|
56
155
|
throw new Error('A recovered character recipe is required');
|
|
@@ -58,9 +157,7 @@ export async function createAvatar(canvas, initial, callbacks = {}) {
|
|
|
58
157
|
return createManagedAvatar(canvas, initial, callbacks, createCharacter);
|
|
59
158
|
}
|
|
60
159
|
function createCharacter(canvas, initial, callbacks) {
|
|
61
|
-
return initial
|
|
62
|
-
? createLegacyCharacter(canvas, initial, callbacks)
|
|
63
|
-
: createLiveCharacter(canvas, initial, callbacks);
|
|
160
|
+
return createScopedCharacter(canvas, initial, callbacks);
|
|
64
161
|
}
|
|
65
162
|
|
|
66
163
|
async function createLiveCharacter(
|
|
@@ -70,34 +167,41 @@ async function createLiveCharacter(
|
|
|
70
167
|
isCurrent = () => true,
|
|
71
168
|
previousImage = null,
|
|
72
169
|
) {
|
|
170
|
+
const authoredParts = authoredPartsFor(initial);
|
|
73
171
|
const lease = initial.legacy?.points
|
|
74
|
-
? await acquireLegacyEngine(initial.legacy.points)
|
|
172
|
+
? await acquireLegacyEngine(initial.legacy.points, authoredParts)
|
|
75
173
|
: null;
|
|
76
|
-
const module = lease?.module ?? (await
|
|
174
|
+
const module = lease?.module ?? (await getCharacterEngine(authoredParts));
|
|
175
|
+
let releasePreparation = await acquireCharacterPreparation();
|
|
176
|
+
await waitForSharedRender();
|
|
77
177
|
if (!isCurrent() || !canvas.isConnected) {
|
|
178
|
+
releasePreparation();
|
|
78
179
|
lease?.release();
|
|
79
180
|
return { update() {}, dispose() {}, diagnostics: () => ({ ready: false }) };
|
|
80
181
|
}
|
|
81
|
-
const id = `bloom-character-${++serial}`;
|
|
82
182
|
const output = canvas.getContext('2d');
|
|
83
183
|
if (!output) {
|
|
184
|
+
releasePreparation();
|
|
84
185
|
lease?.release();
|
|
85
186
|
throw new Error('Character output canvas is unavailable');
|
|
86
187
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
188
|
+
let surfaceLease;
|
|
189
|
+
try {
|
|
190
|
+
surfaceLease = acquireSharedSurface(canvas.parentElement);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
releasePreparation();
|
|
193
|
+
lease?.release();
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
const supportsActivities = !initial.portrait;
|
|
96
197
|
let character;
|
|
97
198
|
try {
|
|
98
|
-
character =
|
|
199
|
+
character = surfaceLease.create(module, 128, 128, {
|
|
200
|
+
activities: supportsActivities,
|
|
201
|
+
});
|
|
99
202
|
} catch (error) {
|
|
100
|
-
|
|
203
|
+
releasePreparation();
|
|
204
|
+
surfaceLease.release();
|
|
101
205
|
lease?.release();
|
|
102
206
|
throw error;
|
|
103
207
|
}
|
|
@@ -118,6 +222,9 @@ async function createLiveCharacter(
|
|
|
118
222
|
episode = 0n,
|
|
119
223
|
activity = 0;
|
|
120
224
|
let workTimer,
|
|
225
|
+
workRequest = 0,
|
|
226
|
+
stoppingWork = false,
|
|
227
|
+
queuedReaction = false,
|
|
121
228
|
workDuration,
|
|
122
229
|
lastActivityResult,
|
|
123
230
|
previousWork,
|
|
@@ -125,7 +232,64 @@ async function createLiveCharacter(
|
|
|
125
232
|
let pointerId, pointerStart, previousReaction;
|
|
126
233
|
let lastReaction = null,
|
|
127
234
|
lastReactionKind = null;
|
|
128
|
-
|
|
235
|
+
let attentionStarted = 0,
|
|
236
|
+
attentionUntil = 0;
|
|
237
|
+
const attend = (duration) => {
|
|
238
|
+
attentionStarted = performance.now();
|
|
239
|
+
attentionUntil = attentionStarted + duration;
|
|
240
|
+
};
|
|
241
|
+
// hasPendingUpdate also includes reactions whose clock is suspended while
|
|
242
|
+
// Work owns the character. Geometry completion must use preparation revisions,
|
|
243
|
+
// otherwise that old reaction holds the global preparation lease forever.
|
|
244
|
+
const scenePrepared = () => {
|
|
245
|
+
const state = character.preparationStats();
|
|
246
|
+
return (
|
|
247
|
+
!state.pending &&
|
|
248
|
+
!state.failed &&
|
|
249
|
+
state.generation === state.committedGeneration
|
|
250
|
+
);
|
|
251
|
+
};
|
|
252
|
+
let acquiringPreparation = false;
|
|
253
|
+
const preparationQueue = [];
|
|
254
|
+
function finishPreparation(force = false) {
|
|
255
|
+
if (!force && (acquiringPreparation || preparationQueue.length)) return;
|
|
256
|
+
const release = releasePreparation;
|
|
257
|
+
releasePreparation = undefined;
|
|
258
|
+
release?.();
|
|
259
|
+
}
|
|
260
|
+
function prepare(operation) {
|
|
261
|
+
if (disposed) return;
|
|
262
|
+
if (releasePreparation && !sharedRenderPending()) {
|
|
263
|
+
operation();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
preparationQueue.push(operation);
|
|
267
|
+
if (acquiringPreparation) return;
|
|
268
|
+
acquiringPreparation = true;
|
|
269
|
+
const admission = releasePreparation
|
|
270
|
+
? Promise.resolve(releasePreparation)
|
|
271
|
+
: acquireCharacterPreparation();
|
|
272
|
+
void admission.then(async (release) => {
|
|
273
|
+
await waitForSharedRender();
|
|
274
|
+
acquiringPreparation = false;
|
|
275
|
+
if (disposed) {
|
|
276
|
+
preparationQueue.length = 0;
|
|
277
|
+
release();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
releasePreparation = release;
|
|
281
|
+
try {
|
|
282
|
+
for (const next of preparationQueue.splice(0)) next();
|
|
283
|
+
if (scenePrepared()) finishPreparation();
|
|
284
|
+
wake();
|
|
285
|
+
} catch (error) {
|
|
286
|
+
finishPreparation();
|
|
287
|
+
callbacks.onError?.(error);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
character.setQuality(1); // Resize selects mesh detail using CSS size, independent of DPR.
|
|
129
293
|
// Readiness means painted pixels, not merely an engine submission.
|
|
130
294
|
const play = (kind) => {
|
|
131
295
|
if (activity === kind) return;
|
|
@@ -146,91 +310,39 @@ async function createLiveCharacter(
|
|
|
146
310
|
// A queued still must finish if its selector moves it offscreen mid-prepare;
|
|
147
311
|
// otherwise one clipped slot blocks every visible thumbnail until timeout.
|
|
148
312
|
const active = () => (visible || props.portrait) && !document.hidden;
|
|
313
|
+
let wakeQueued = false;
|
|
149
314
|
const wake = () => {
|
|
150
315
|
if (disposed) return;
|
|
316
|
+
if (sharedRenderPending()) {
|
|
317
|
+
if (!wakeQueued) {
|
|
318
|
+
wakeQueued = true;
|
|
319
|
+
void waitForSharedRender().then(() => {
|
|
320
|
+
wakeQueued = false;
|
|
321
|
+
wake();
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
151
326
|
character.setActive(active());
|
|
152
327
|
if (active() && (dirty || !ready || animate())) clients.add(client);
|
|
153
328
|
else clients.delete(client);
|
|
154
329
|
schedule();
|
|
155
330
|
};
|
|
156
331
|
const client = {
|
|
332
|
+
preparing: () => Boolean(releasePreparation),
|
|
333
|
+
attention: (time) => (time < attentionUntil ? attentionStarted : 0),
|
|
157
334
|
visibility: wake,
|
|
335
|
+
fail(error) {
|
|
336
|
+
clients.delete(client);
|
|
337
|
+
finishPreparation(true);
|
|
338
|
+
callbacks.onError?.(error);
|
|
339
|
+
},
|
|
158
340
|
render(time) {
|
|
159
341
|
try {
|
|
160
|
-
const submitted =
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
!character.preparationStats().pending &&
|
|
165
|
-
!character.hasPendingUpdate()
|
|
166
|
-
) {
|
|
167
|
-
measurement.width = canvas.width;
|
|
168
|
-
measurement.height = canvas.height;
|
|
169
|
-
measure.drawImage(surface, 0, 0, canvas.width, canvas.height);
|
|
170
|
-
const { data, width, height } = measure.getImageData(
|
|
171
|
-
0,
|
|
172
|
-
0,
|
|
173
|
-
canvas.width,
|
|
174
|
-
canvas.height,
|
|
175
|
-
);
|
|
176
|
-
let minX = width,
|
|
177
|
-
minY = height,
|
|
178
|
-
maxX = -1,
|
|
179
|
-
maxY = -1;
|
|
180
|
-
for (let y = 0; y < height; y++)
|
|
181
|
-
for (let x = 0; x < width; x++) {
|
|
182
|
-
if (data[(y * width + x) * 4 + 3] > 24) {
|
|
183
|
-
minX = Math.min(minX, x);
|
|
184
|
-
maxX = Math.max(maxX, x);
|
|
185
|
-
minY = Math.min(minY, y);
|
|
186
|
-
maxY = Math.max(maxY, y);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
if (maxX >= minX) {
|
|
190
|
-
const extent = Math.max(maxX - minX + 1, maxY - minY + 1) * 1.22;
|
|
191
|
-
fit = [
|
|
192
|
-
(minX + maxX) / 2 - extent / 2,
|
|
193
|
-
(minY + maxY) / 2 - extent / 2,
|
|
194
|
-
extent,
|
|
195
|
-
];
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
// Retain the previous portrait while a changed shape is preparing.
|
|
199
|
-
if (!fit) return;
|
|
200
|
-
output.clearRect(0, 0, canvas.width, canvas.height);
|
|
201
|
-
if (fit)
|
|
202
|
-
output.drawImage(
|
|
203
|
-
surface,
|
|
204
|
-
fit[0],
|
|
205
|
-
fit[1],
|
|
206
|
-
fit[2],
|
|
207
|
-
fit[2],
|
|
208
|
-
0,
|
|
209
|
-
0,
|
|
210
|
-
canvas.width,
|
|
211
|
-
canvas.height,
|
|
212
|
-
);
|
|
213
|
-
else output.drawImage(surface, 0, 0, canvas.width, canvas.height);
|
|
214
|
-
}
|
|
215
|
-
if (submitted && fit && transitionImage) {
|
|
216
|
-
// Geometry preparation keeps the previous image; blend only after the
|
|
217
|
-
// new original-engine surface has pixels. Reduced motion skips it.
|
|
218
|
-
transitionStarted ??= time;
|
|
219
|
-
const progress = animate()
|
|
220
|
-
? Math.min(1, (time - transitionStarted) / 0.18)
|
|
221
|
-
: 1;
|
|
222
|
-
if (progress < 1) {
|
|
223
|
-
output.globalAlpha = 1 - progress;
|
|
224
|
-
output.drawImage(
|
|
225
|
-
transitionImage,
|
|
226
|
-
0,
|
|
227
|
-
0,
|
|
228
|
-
canvas.width,
|
|
229
|
-
canvas.height,
|
|
230
|
-
);
|
|
231
|
-
output.globalAlpha = 1;
|
|
232
|
-
} else transitionImage = null;
|
|
233
|
-
}
|
|
342
|
+
const submitted = surfaceLease.render(
|
|
343
|
+
() => character.render(time),
|
|
344
|
+
scenePrepared,
|
|
345
|
+
);
|
|
234
346
|
const renderError = character.renderError();
|
|
235
347
|
if (renderError || character.preparationStats().failed)
|
|
236
348
|
throw new Error(
|
|
@@ -238,113 +350,293 @@ async function createLiveCharacter(
|
|
|
238
350
|
character.preparationError() ||
|
|
239
351
|
'Character preparation failed',
|
|
240
352
|
);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
fingerprint === pendingFingerprint
|
|
247
|
-
) {
|
|
248
|
-
dirty = false;
|
|
249
|
-
if (!ready) {
|
|
250
|
-
ready = true;
|
|
251
|
-
callbacks.onReady?.();
|
|
252
|
-
}
|
|
253
|
-
callbacks.onPaint?.(!transitionImage);
|
|
254
|
-
if (activityMode && workDuration && !workTimer) {
|
|
255
|
-
workTimer = setTimeout(() => {
|
|
256
|
-
workTimer = undefined;
|
|
257
|
-
workDuration = undefined;
|
|
258
|
-
play(0);
|
|
259
|
-
switchMode(false);
|
|
260
|
-
dirty = true;
|
|
261
|
-
wake();
|
|
262
|
-
}, workDuration);
|
|
263
|
-
}
|
|
264
|
-
if (!animate()) clients.delete(client);
|
|
353
|
+
// Return to pointer/reaction control only after the authored Work outro
|
|
354
|
+
// clears its phase and episode; render skips do not indicate completion.
|
|
355
|
+
if (stoppingWork && module.controllerState(character).settled) {
|
|
356
|
+
stoppingWork = false;
|
|
357
|
+
switchMode(false);
|
|
265
358
|
}
|
|
359
|
+
if (queuedReaction && !activityMode) attemptReaction();
|
|
360
|
+
if (!submitted) return;
|
|
361
|
+
const revision = surfaceLease.revision;
|
|
362
|
+
return () => {
|
|
363
|
+
if (disposed || revision !== surfaceLease.revision) return;
|
|
364
|
+
try {
|
|
365
|
+
if (submitted) {
|
|
366
|
+
if (!fit && scenePrepared()) {
|
|
367
|
+
measurement.width = canvas.width;
|
|
368
|
+
measurement.height = canvas.height;
|
|
369
|
+
surfaceLease.copy(
|
|
370
|
+
measure,
|
|
371
|
+
0,
|
|
372
|
+
0,
|
|
373
|
+
canvas.width,
|
|
374
|
+
canvas.height,
|
|
375
|
+
0,
|
|
376
|
+
0,
|
|
377
|
+
canvas.width,
|
|
378
|
+
canvas.height,
|
|
379
|
+
);
|
|
380
|
+
const { data, width, height } = measure.getImageData(
|
|
381
|
+
0,
|
|
382
|
+
0,
|
|
383
|
+
canvas.width,
|
|
384
|
+
canvas.height,
|
|
385
|
+
);
|
|
386
|
+
let minX = width,
|
|
387
|
+
minY = height,
|
|
388
|
+
maxX = -1,
|
|
389
|
+
maxY = -1;
|
|
390
|
+
for (let y = 0; y < height; y++)
|
|
391
|
+
for (let x = 0; x < width; x++) {
|
|
392
|
+
if (data[(y * width + x) * 4 + 3] > 24) {
|
|
393
|
+
minX = Math.min(minX, x);
|
|
394
|
+
maxX = Math.max(maxX, x);
|
|
395
|
+
minY = Math.min(minY, y);
|
|
396
|
+
maxY = Math.max(maxY, y);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (maxX >= minX) {
|
|
400
|
+
const extent =
|
|
401
|
+
Math.max(maxX - minX + 1, maxY - minY + 1) * 1.22;
|
|
402
|
+
fit = [
|
|
403
|
+
(minX + maxX) / 2 - extent / 2,
|
|
404
|
+
(minY + maxY) / 2 - extent / 2,
|
|
405
|
+
extent,
|
|
406
|
+
];
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Retain the previous portrait while a changed shape is preparing.
|
|
410
|
+
if (!fit) return;
|
|
411
|
+
output.clearRect(0, 0, canvas.width, canvas.height);
|
|
412
|
+
if (fit)
|
|
413
|
+
surfaceLease.copy(
|
|
414
|
+
output,
|
|
415
|
+
fit[0],
|
|
416
|
+
fit[1],
|
|
417
|
+
fit[2],
|
|
418
|
+
fit[2],
|
|
419
|
+
0,
|
|
420
|
+
0,
|
|
421
|
+
canvas.width,
|
|
422
|
+
canvas.height,
|
|
423
|
+
);
|
|
424
|
+
else surfaceLease.copy(output);
|
|
425
|
+
}
|
|
426
|
+
if (submitted && fit && transitionImage) {
|
|
427
|
+
// Geometry preparation keeps the previous image; blend only after the
|
|
428
|
+
// new original-engine surface has pixels. Reduced motion skips it.
|
|
429
|
+
transitionStarted ??= time;
|
|
430
|
+
const progress = animate()
|
|
431
|
+
? Math.min(1, (time - transitionStarted) / 0.18)
|
|
432
|
+
: 1;
|
|
433
|
+
if (progress < 1) {
|
|
434
|
+
output.globalAlpha = 1 - progress;
|
|
435
|
+
output.drawImage(
|
|
436
|
+
transitionImage,
|
|
437
|
+
0,
|
|
438
|
+
0,
|
|
439
|
+
canvas.width,
|
|
440
|
+
canvas.height,
|
|
441
|
+
);
|
|
442
|
+
output.globalAlpha = 1;
|
|
443
|
+
} else transitionImage = null;
|
|
444
|
+
}
|
|
445
|
+
if (
|
|
446
|
+
submitted &&
|
|
447
|
+
fit &&
|
|
448
|
+
scenePrepared() &&
|
|
449
|
+
fingerprint === pendingFingerprint
|
|
450
|
+
) {
|
|
451
|
+
dirty = false;
|
|
452
|
+
finishPreparation();
|
|
453
|
+
if (!ready) {
|
|
454
|
+
ready = true;
|
|
455
|
+
callbacks.onReady?.();
|
|
456
|
+
}
|
|
457
|
+
callbacks.onPaint?.(!transitionImage);
|
|
458
|
+
if (activityMode && workDuration && !workTimer) {
|
|
459
|
+
const request = workRequest;
|
|
460
|
+
workTimer = setTimeout(() => {
|
|
461
|
+
if (request !== workRequest) return;
|
|
462
|
+
workTimer = undefined;
|
|
463
|
+
workDuration = undefined;
|
|
464
|
+
prepare(() => {
|
|
465
|
+
if (request !== workRequest) return;
|
|
466
|
+
play(0);
|
|
467
|
+
stoppingWork = true;
|
|
468
|
+
});
|
|
469
|
+
dirty = true;
|
|
470
|
+
wake();
|
|
471
|
+
}, workDuration);
|
|
472
|
+
}
|
|
473
|
+
if (!animate()) clients.delete(client);
|
|
474
|
+
}
|
|
475
|
+
} catch (error) {
|
|
476
|
+
clients.delete(client);
|
|
477
|
+
finishPreparation();
|
|
478
|
+
callbacks.onError?.(error);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
266
481
|
} catch (error) {
|
|
267
482
|
clients.delete(client);
|
|
483
|
+
finishPreparation();
|
|
268
484
|
callbacks.onError?.(error);
|
|
269
485
|
}
|
|
270
486
|
},
|
|
271
487
|
};
|
|
488
|
+
let currentQuality, currentScale;
|
|
272
489
|
const resize = () => {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
Math.min(512, Math.round(css * Math.min(devicePixelRatio || 1, 2))),
|
|
490
|
+
const cssSize = Math.max(
|
|
491
|
+
1,
|
|
492
|
+
Math.min(canvas.clientWidth, canvas.clientHeight),
|
|
277
493
|
);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
canvas.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
494
|
+
const backingSize = characterPixels(canvas);
|
|
495
|
+
if (
|
|
496
|
+
canvas.width === backingSize &&
|
|
497
|
+
canvas.height === backingSize &&
|
|
498
|
+
currentQuality === (props.portrait || cssSize <= 128 ? 1 : 2) &&
|
|
499
|
+
currentScale === backingSize / cssSize
|
|
500
|
+
)
|
|
501
|
+
return;
|
|
502
|
+
prepare(() => {
|
|
503
|
+
const css = Math.max(
|
|
504
|
+
1,
|
|
505
|
+
Math.min(canvas.clientWidth, canvas.clientHeight),
|
|
506
|
+
);
|
|
507
|
+
const pixels = characterPixels(canvas);
|
|
508
|
+
if (canvas.width !== pixels || canvas.height !== pixels) {
|
|
509
|
+
canvas.width = pixels;
|
|
510
|
+
canvas.height = pixels;
|
|
511
|
+
fit = null;
|
|
512
|
+
}
|
|
513
|
+
const quality = props.portrait || css <= 128 ? 1 : 2;
|
|
514
|
+
if (currentQuality !== quality) {
|
|
515
|
+
character.setQuality(quality);
|
|
516
|
+
currentQuality = quality;
|
|
517
|
+
}
|
|
518
|
+
character.resize(pixels, pixels);
|
|
519
|
+
if (currentScale !== pixels / css) {
|
|
520
|
+
character.setDisplayScale(pixels / css);
|
|
521
|
+
currentScale = pixels / css;
|
|
522
|
+
}
|
|
523
|
+
dirty = true;
|
|
524
|
+
wake();
|
|
525
|
+
});
|
|
526
|
+
};
|
|
527
|
+
const bindController = (next = false) => {
|
|
528
|
+
if (!supportsActivities) return;
|
|
529
|
+
const result = character.applyActivity({
|
|
530
|
+
command: 3,
|
|
531
|
+
sequence: 0n,
|
|
532
|
+
episodeId: 0n,
|
|
533
|
+
episodeHighWater: 0n,
|
|
534
|
+
activity: 0,
|
|
535
|
+
outcome: 0,
|
|
536
|
+
entry: 1,
|
|
537
|
+
});
|
|
538
|
+
if (result !== 0)
|
|
539
|
+
throw new Error(`Character activity initialization failed: ${result}`);
|
|
540
|
+
module.controllerMode(character, next);
|
|
287
541
|
};
|
|
288
542
|
const switchMode = (next, resetAppearance = false) => {
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
543
|
+
if (resetAppearance) {
|
|
544
|
+
// Only a frozen appearance edit needs a new native scene: the original
|
|
545
|
+
// appearance blend otherwise stops on its first frame. React/Work never
|
|
546
|
+
// enter this branch and keep their character, preparation, clock and fit.
|
|
547
|
+
const state = new Uint8Array(character.state());
|
|
548
|
+
character.delete();
|
|
549
|
+
character = surfaceLease.create(module, 128, 128, {
|
|
550
|
+
activities: supportsActivities,
|
|
551
|
+
});
|
|
552
|
+
currentQuality = currentScale = undefined;
|
|
553
|
+
sequence = episode = 0n;
|
|
554
|
+
activity = 0;
|
|
555
|
+
character.setQuality(1);
|
|
556
|
+
character.restore(state);
|
|
557
|
+
character.setReducedMotion(false);
|
|
558
|
+
bindController(next);
|
|
559
|
+
resize();
|
|
560
|
+
} else {
|
|
561
|
+
if (activityMode === next) return;
|
|
562
|
+
surfaceLease.beginTransition(180);
|
|
563
|
+
module.controllerMode(character, next);
|
|
564
|
+
}
|
|
295
565
|
activityMode = next;
|
|
296
|
-
fit = null;
|
|
297
|
-
sequence = 0n;
|
|
298
|
-
episode = 0n;
|
|
299
|
-
activity = 0;
|
|
300
|
-
ready = false;
|
|
301
566
|
dirty = true;
|
|
302
|
-
character.setQuality(props.portrait ? 1 : 0);
|
|
303
|
-
character.restore(state);
|
|
304
|
-
// This recovered engine paints a transparent frame when reduced motion is
|
|
305
|
-
// enabled before its first appearance. Freeze our clock after a painted
|
|
306
|
-
// frame instead, preserving both initial visibility and reduced motion.
|
|
307
|
-
character.setReducedMotion(false);
|
|
308
|
-
|
|
309
|
-
if (next)
|
|
310
|
-
character.applyActivity({
|
|
311
|
-
command: 3,
|
|
312
|
-
sequence: 0n,
|
|
313
|
-
episodeId: 0n,
|
|
314
|
-
episodeHighWater: 0n,
|
|
315
|
-
activity: 0,
|
|
316
|
-
outcome: 0,
|
|
317
|
-
entry: 1,
|
|
318
|
-
});
|
|
319
|
-
resize();
|
|
320
567
|
};
|
|
321
|
-
const
|
|
322
|
-
if (!ready || !animate()) return;
|
|
323
|
-
if (activityMode) return;
|
|
568
|
+
const attemptReaction = () => {
|
|
324
569
|
lastReactionKind = 2;
|
|
325
|
-
lastReaction =
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
570
|
+
lastReaction = module.controllerSignature(
|
|
571
|
+
character,
|
|
572
|
+
bodySignatureFor(props),
|
|
573
|
+
);
|
|
574
|
+
if (lastReaction === 2)
|
|
575
|
+
throw new Error('Original body signature is unavailable');
|
|
576
|
+
// An interrupted signature resumes its clock on the next reactive render.
|
|
577
|
+
// Keep one latest request queued until that signature releases the controller.
|
|
578
|
+
queuedReaction = lastReaction === 1;
|
|
579
|
+
};
|
|
580
|
+
const react = () => {
|
|
581
|
+
const request = ++workRequest;
|
|
582
|
+
clearTimeout(workTimer);
|
|
583
|
+
workTimer = workDuration = undefined;
|
|
584
|
+
const apply = () => {
|
|
585
|
+
if (disposed || request !== workRequest || !ready || !animate()) return;
|
|
586
|
+
attend(3000);
|
|
587
|
+
queuedReaction = true;
|
|
588
|
+
if (activityMode) {
|
|
589
|
+
play(0);
|
|
590
|
+
stoppingWork = true;
|
|
591
|
+
} else attemptReaction();
|
|
592
|
+
dirty = true;
|
|
593
|
+
wake();
|
|
594
|
+
};
|
|
595
|
+
if (sharedRenderPending()) void waitForSharedRender().then(apply);
|
|
596
|
+
else apply();
|
|
334
597
|
};
|
|
335
598
|
const work = () => {
|
|
336
|
-
|
|
599
|
+
// Invalidate an already queued Stop as soon as a new request arrives,
|
|
600
|
+
// including requests made while the previous GPU batch is still in flight.
|
|
601
|
+
const request = ++workRequest;
|
|
337
602
|
clearTimeout(workTimer);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
603
|
+
workTimer = workDuration = undefined;
|
|
604
|
+
prepare(() => {
|
|
605
|
+
if (request !== workRequest || !animate() || !supportsActivities) return;
|
|
606
|
+
queuedReaction = false;
|
|
607
|
+
stoppingWork = false;
|
|
608
|
+
switchMode(true);
|
|
609
|
+
play(1);
|
|
610
|
+
workDuration = 2200 * Math.max(1, Math.min(10, props.workingCycles || 1));
|
|
611
|
+
attend(workDuration + 1000);
|
|
612
|
+
});
|
|
613
|
+
};
|
|
614
|
+
let appearanceTimer, pendingFingerprint, lastCapabilities;
|
|
615
|
+
const reportCapabilities = (available, selected) => {
|
|
616
|
+
lastCapabilities = {
|
|
617
|
+
key: JSON.stringify(
|
|
618
|
+
isLegacyRecipe(props) ? characterRecipe(props) : props.config.character,
|
|
619
|
+
),
|
|
620
|
+
available,
|
|
621
|
+
selected,
|
|
622
|
+
};
|
|
623
|
+
callbacks.onCapabilities?.(lastCapabilities);
|
|
342
624
|
};
|
|
343
|
-
let appearanceTimer, pendingFingerprint;
|
|
344
625
|
const applyAppearance = (value) => {
|
|
345
626
|
const recipe = value;
|
|
346
|
-
const nextFingerprint =
|
|
627
|
+
const nextFingerprint = appearanceFingerprint(recipe);
|
|
347
628
|
if (nextFingerprint !== fingerprint) {
|
|
629
|
+
const defaultPreset = ORIGINAL_PRESETS.includes(recipe.preset)
|
|
630
|
+
? recipe.preset
|
|
631
|
+
: authoredParts.bodyPreset;
|
|
632
|
+
const sourcePaint = defaultPreset
|
|
633
|
+
? originalPalettePaint(module, defaultPreset)
|
|
634
|
+
: null;
|
|
635
|
+
const customizedParts = Object.keys(authoredParts).length > 0;
|
|
636
|
+
const implicitBodyColor =
|
|
637
|
+
customizedParts && !recipe.bodyColor && !recipe.selections?.color
|
|
638
|
+
? sourcePaint?.bodyColor
|
|
639
|
+
: undefined;
|
|
348
640
|
// Catalog selection owns the engine's part transitions. Restoring the
|
|
349
641
|
// named preset on every selection needlessly resets the whole character.
|
|
350
642
|
const removedOverride = Object.keys(appliedRecipe?.selections ?? {}).some(
|
|
@@ -356,28 +648,74 @@ async function createLiveCharacter(
|
|
|
356
648
|
removedOverride ||
|
|
357
649
|
(appliedRecipe.bodyColor && !recipe.bodyColor)
|
|
358
650
|
) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
651
|
+
let base =
|
|
652
|
+
props.legacy ||
|
|
653
|
+
recipe.preset === 'clippo' ||
|
|
654
|
+
recipe.preset === 'legacy' ||
|
|
655
|
+
authoredParts.bodyPreset ||
|
|
656
|
+
(sourcePaint?.bodyColor && customizedParts)
|
|
657
|
+
? encodeAppearance({
|
|
658
|
+
version: 1,
|
|
659
|
+
shape: 'circle',
|
|
660
|
+
color: sourcePaint?.color ?? 'blue',
|
|
661
|
+
eyes: 'dots',
|
|
662
|
+
eyewear: 'none',
|
|
663
|
+
accessories: [],
|
|
664
|
+
accessoryColors: {},
|
|
665
|
+
constrained: 0,
|
|
666
|
+
depth: 0.5,
|
|
667
|
+
model: null,
|
|
668
|
+
rig: null,
|
|
669
|
+
hereCharacter: null,
|
|
670
|
+
})
|
|
671
|
+
: module.presetAppearance(recipe.preset);
|
|
672
|
+
if (
|
|
673
|
+
!props.legacy &&
|
|
674
|
+
recipe.preset !== 'clippo' &&
|
|
675
|
+
recipe.preset !== 'legacy' &&
|
|
676
|
+
!authoredParts.bodyPreset &&
|
|
677
|
+
Object.keys(authoredParts).length
|
|
678
|
+
) {
|
|
679
|
+
// Selecting the preset's existing shape/backing eye is a native no-op,
|
|
680
|
+
// so select() never clears HERE's material overrides. Donor pieces
|
|
681
|
+
// need an editable controller while retaining all authored defaults.
|
|
682
|
+
const editable = decodeAppearance(base);
|
|
683
|
+
editable.hereCharacter = null;
|
|
684
|
+
base = encodeAppearance(editable);
|
|
685
|
+
}
|
|
375
686
|
if (character.restore(base))
|
|
376
687
|
throw new Error('Invalid character appearance');
|
|
377
688
|
}
|
|
378
689
|
for (const key of Object.keys(category)) {
|
|
379
|
-
const
|
|
380
|
-
|
|
690
|
+
const selected =
|
|
691
|
+
(key === 'shape' ? authoredParts.faceShape : undefined) ??
|
|
692
|
+
authoredParts[key] ??
|
|
693
|
+
recipe.selections?.[key];
|
|
694
|
+
// Virtual choices replace copied prepared meshes in the worker. The
|
|
695
|
+
// original engine still selects its own valid backing geometry.
|
|
696
|
+
const value =
|
|
697
|
+
key === 'shape' && MIGRATED_SHAPE_IDS.includes(selected)
|
|
698
|
+
? 'circle'
|
|
699
|
+
: key === 'shape' && selected === 'todd'
|
|
700
|
+
? 'circle'
|
|
701
|
+
: key === 'shape' && selected === 'clippo'
|
|
702
|
+
? 'circle'
|
|
703
|
+
: key === 'eyes' && authoredParts.eyes
|
|
704
|
+
? ['oval', 'round_inset', 'dots', ...NATIVE_PARTS.eyes].find(
|
|
705
|
+
(id) => character.isAvailable(category.eyes, id),
|
|
706
|
+
)
|
|
707
|
+
: key === 'accessory' && authoredParts.accessory
|
|
708
|
+
? 'none'
|
|
709
|
+
: key === 'eyewear' && authoredParts.eyewear
|
|
710
|
+
? 'none'
|
|
711
|
+
: selected;
|
|
712
|
+
if (!value) {
|
|
713
|
+
if (key === 'eyes' && authoredParts.eyes)
|
|
714
|
+
throw new Error(
|
|
715
|
+
`No compatible fitting reference for ${selected} eyes`,
|
|
716
|
+
);
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
381
719
|
if (key === 'accessory' && value === 'none') {
|
|
382
720
|
for (const item of module.catalog(4))
|
|
383
721
|
if (character.isSelected(4, item.id)) character.select(4, item.id);
|
|
@@ -390,8 +728,10 @@ async function createLiveCharacter(
|
|
|
390
728
|
throw new Error(`Unsupported character selection: ${key}`);
|
|
391
729
|
}
|
|
392
730
|
const patch =
|
|
393
|
-
props.legacy?.patch ??
|
|
394
|
-
(recipe.bodyColor
|
|
731
|
+
(isLegacyRecipe(props) ? props.legacy?.patch : undefined) ??
|
|
732
|
+
(recipe.bodyColor || implicitBodyColor
|
|
733
|
+
? { bodyColor: recipe.bodyColor ?? implicitBodyColor }
|
|
734
|
+
: null);
|
|
395
735
|
if (patch) {
|
|
396
736
|
// One authored preset uses an unnamed color. Enter the engine's normal
|
|
397
737
|
// editable palette before replacing that paint with an explicit RGB.
|
|
@@ -429,34 +769,107 @@ async function createLiveCharacter(
|
|
|
429
769
|
const selected = {};
|
|
430
770
|
for (const [key, index] of Object.entries(category)) {
|
|
431
771
|
for (const item of module.catalog(index)) {
|
|
432
|
-
available[`${key}:${item.id}`] =
|
|
433
|
-
|
|
434
|
-
item.id
|
|
435
|
-
);
|
|
772
|
+
available[`${key}:${item.id}`] = NATIVE_PARTS[key]?.includes(item.id)
|
|
773
|
+
? true
|
|
774
|
+
: character.isAvailable(index, item.id);
|
|
436
775
|
if (character.isSelected(index, item.id)) selected[key] = item.id;
|
|
437
776
|
}
|
|
438
777
|
}
|
|
778
|
+
if (authoredParts.bodyPreset)
|
|
779
|
+
Object.assign(
|
|
780
|
+
selected,
|
|
781
|
+
bodySelectionDefaults(
|
|
782
|
+
authoredParts.bodyPreset,
|
|
783
|
+
decodeAppearance(module.presetAppearance(authoredParts.bodyPreset)),
|
|
784
|
+
),
|
|
785
|
+
);
|
|
786
|
+
for (const key of ['eyes', 'eyewear', 'accessory'])
|
|
787
|
+
if (authoredParts[key]) selected[key] = authoredParts[key];
|
|
788
|
+
available['shape:todd'] = true;
|
|
789
|
+
for (const id of MIGRATED_SHAPE_IDS) available[`shape:${id}`] = true;
|
|
790
|
+
if (MIGRATED_SHAPE_IDS.includes(recipe.selections?.shape))
|
|
791
|
+
selected.shape = recipe.selections.shape;
|
|
792
|
+
available['eyes:cyclops'] = true;
|
|
793
|
+
if (
|
|
794
|
+
recipe.selections?.shape === 'todd' ||
|
|
795
|
+
authoredParts.bodyPreset === 'lime_frog' ||
|
|
796
|
+
(recipe.preset === 'lime_frog' && !recipe.selections?.shape)
|
|
797
|
+
)
|
|
798
|
+
selected.shape = 'todd';
|
|
799
|
+
available['shape:clippo'] = true;
|
|
800
|
+
available['eyes:clippo'] = true;
|
|
801
|
+
available['eyes:todd'] = true;
|
|
802
|
+
if (authoredParts.shape) selected.shape = authoredParts.shape;
|
|
803
|
+
available['accessory:felipe_beret'] = true;
|
|
804
|
+
if (
|
|
805
|
+
authoredParts.eyes ||
|
|
806
|
+
(recipe.preset === 'lime_frog' && !recipe.selections?.eyes)
|
|
807
|
+
)
|
|
808
|
+
selected.eyes = authoredParts.eyes ?? 'todd';
|
|
809
|
+
if (
|
|
810
|
+
authoredParts.accessory ||
|
|
811
|
+
(recipe.preset === 'blue_beret' && !recipe.selections?.accessory)
|
|
812
|
+
)
|
|
813
|
+
selected.accessory = authoredParts.accessory ?? 'felipe_beret';
|
|
439
814
|
available['accessory:none'] = true;
|
|
440
815
|
selected.accessory ??= 'none';
|
|
441
|
-
|
|
816
|
+
const originalPreset = ORIGINAL_PRESETS.includes(recipe.preset)
|
|
817
|
+
? recipe.preset
|
|
818
|
+
: authoredParts.bodyPreset;
|
|
819
|
+
if (
|
|
820
|
+
!recipe.bodyColor &&
|
|
821
|
+
!recipe.selections?.color &&
|
|
822
|
+
ORIGINAL_PRESETS.includes(originalPreset) &&
|
|
823
|
+
!isLegacyRecipe(props)
|
|
824
|
+
) {
|
|
825
|
+
const originalColor = decodeAppearance(
|
|
826
|
+
module.presetAppearance(originalPreset),
|
|
827
|
+
).color;
|
|
828
|
+
if (
|
|
829
|
+
!module
|
|
830
|
+
.catalog(category.color)
|
|
831
|
+
.some((item) => item.id === originalColor)
|
|
832
|
+
) {
|
|
833
|
+
delete selected.color;
|
|
834
|
+
selected.bodyColor = originalBodyColor(module, originalPreset);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (recipe.bodyColor) {
|
|
838
|
+
delete selected.color;
|
|
839
|
+
selected.bodyColor = recipe.bodyColor;
|
|
840
|
+
}
|
|
841
|
+
reportCapabilities(available, selected);
|
|
442
842
|
dirty = true;
|
|
443
843
|
}
|
|
444
844
|
};
|
|
845
|
+
let deferredUpdate;
|
|
445
846
|
const update = (next, silent = false) => {
|
|
446
847
|
if (disposed) return;
|
|
848
|
+
if (sharedRenderPending()) {
|
|
849
|
+
const queued = Boolean(deferredUpdate);
|
|
850
|
+
deferredUpdate = [next, silent];
|
|
851
|
+
if (!queued)
|
|
852
|
+
void waitForSharedRender().then(() => {
|
|
853
|
+
const value = deferredUpdate;
|
|
854
|
+
deferredUpdate = undefined;
|
|
855
|
+
if (value && !disposed) update(...value);
|
|
856
|
+
});
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
447
859
|
props = next;
|
|
448
|
-
const recipe = props
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
860
|
+
const recipe = characterRecipe(props);
|
|
861
|
+
const nextFingerprint = appearanceFingerprint(recipe);
|
|
862
|
+
// Explicitly choosing a preset's default changes its saved recipe, but not
|
|
863
|
+
// its prepared appearance. Re-key the actual capabilities without rebuilding.
|
|
864
|
+
const rawKey = JSON.stringify(
|
|
865
|
+
isLegacyRecipe(props) ? recipe : props.config.character,
|
|
866
|
+
);
|
|
867
|
+
if (
|
|
868
|
+
nextFingerprint === fingerprint &&
|
|
869
|
+
lastCapabilities &&
|
|
870
|
+
lastCapabilities.key !== rawKey
|
|
871
|
+
)
|
|
872
|
+
reportCapabilities(lastCapabilities.available, lastCapabilities.selected);
|
|
460
873
|
// Dragging the shared color picker may emit dozens of values per second.
|
|
461
874
|
// Prepare only its latest value while retaining the existing painted frame.
|
|
462
875
|
if (pendingFingerprint !== nextFingerprint) {
|
|
@@ -470,13 +883,19 @@ async function createLiveCharacter(
|
|
|
470
883
|
appearanceTimer = setTimeout(() => {
|
|
471
884
|
if (disposed) return;
|
|
472
885
|
try {
|
|
473
|
-
|
|
886
|
+
prepare(() => {
|
|
887
|
+
if (pendingFingerprint === nextFingerprint)
|
|
888
|
+
applyAppearance(recipe);
|
|
889
|
+
});
|
|
474
890
|
wake();
|
|
475
891
|
} catch (error) {
|
|
476
892
|
callbacks.onError?.(error);
|
|
477
893
|
}
|
|
478
894
|
}, 80);
|
|
479
|
-
} else
|
|
895
|
+
} else
|
|
896
|
+
prepare(() => {
|
|
897
|
+
if (pendingFingerprint === nextFingerprint) applyAppearance(recipe);
|
|
898
|
+
});
|
|
480
899
|
}
|
|
481
900
|
// The recovered engine needs its first appearance rendered with this off.
|
|
482
901
|
// Our scheduler freezes the completed frame instead.
|
|
@@ -520,6 +939,18 @@ async function createLiveCharacter(
|
|
|
520
939
|
dirty = true;
|
|
521
940
|
wake();
|
|
522
941
|
};
|
|
942
|
+
function input(operation) {
|
|
943
|
+
const apply = () => {
|
|
944
|
+
if (disposed) return;
|
|
945
|
+
try {
|
|
946
|
+
operation();
|
|
947
|
+
} catch (error) {
|
|
948
|
+
client.fail(error);
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
if (sharedRenderPending()) void waitForSharedRender().then(apply);
|
|
952
|
+
else apply();
|
|
953
|
+
}
|
|
523
954
|
function pointer(event) {
|
|
524
955
|
if (!props.interactive || !animate()) return;
|
|
525
956
|
const phase = {
|
|
@@ -530,8 +961,11 @@ async function createLiveCharacter(
|
|
|
530
961
|
}[event.type];
|
|
531
962
|
if (event.type === 'pointerdown') {
|
|
532
963
|
if (event.button !== 0 || pointerId != null || !event.isPrimary) return;
|
|
964
|
+
attend(3000);
|
|
533
965
|
pointerStart = [event.clientX, event.clientY];
|
|
534
966
|
pointerId = event.pointerId;
|
|
967
|
+
// Capture while the physical pointer is still down. GPU completion may
|
|
968
|
+
// arrive after pointerup, when setPointerCapture would throw instead.
|
|
535
969
|
canvas.setPointerCapture(pointerId);
|
|
536
970
|
}
|
|
537
971
|
if (pointerId != null && pointerId !== event.pointerId) return;
|
|
@@ -541,38 +975,41 @@ async function createLiveCharacter(
|
|
|
541
975
|
localY = (event.clientY - r.top) / r.height;
|
|
542
976
|
const x = fit ? (fit[0] + localX * fit[2]) / canvas.width : localX;
|
|
543
977
|
const y = fit ? (fit[1] + localY * fit[2]) / canvas.height : localY;
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
978
|
+
const id = event.pointerId,
|
|
979
|
+
at = performance.now() / 1000;
|
|
980
|
+
const tap =
|
|
981
|
+
phase === 2 &&
|
|
982
|
+
pointerStart &&
|
|
983
|
+
Math.hypot(
|
|
984
|
+
event.clientX - pointerStart[0],
|
|
985
|
+
event.clientY - pointerStart[1],
|
|
986
|
+
) < 6;
|
|
987
|
+
input(() => {
|
|
988
|
+
if (activityMode && phase === 1) character.setReadyGaze(x, y, at);
|
|
989
|
+
if (!activityMode) character.pointer(phase, id, x, y, at);
|
|
990
|
+
if (tap) react();
|
|
991
|
+
dirty = true;
|
|
992
|
+
wake();
|
|
993
|
+
});
|
|
548
994
|
if (phase === 2 || phase === 3) {
|
|
549
|
-
if (
|
|
550
|
-
phase === 2 &&
|
|
551
|
-
pointerStart &&
|
|
552
|
-
Math.hypot(
|
|
553
|
-
event.clientX - pointerStart[0],
|
|
554
|
-
event.clientY - pointerStart[1],
|
|
555
|
-
) < 6
|
|
556
|
-
)
|
|
557
|
-
react();
|
|
558
|
-
if (canvas.hasPointerCapture(event.pointerId))
|
|
559
|
-
canvas.releasePointerCapture(event.pointerId);
|
|
995
|
+
if (canvas.hasPointerCapture(id)) canvas.releasePointerCapture(id);
|
|
560
996
|
pointerId = undefined;
|
|
997
|
+
pointerStart = undefined;
|
|
561
998
|
}
|
|
562
|
-
dirty = true;
|
|
563
|
-
wake();
|
|
564
999
|
}
|
|
565
1000
|
const cancel = () => {
|
|
566
|
-
|
|
567
|
-
character.pointer(3, pointerId, 0.5, 0.5, performance.now() / 1000);
|
|
1001
|
+
const id = pointerId;
|
|
568
1002
|
pointerId = undefined;
|
|
569
1003
|
pointerStart = undefined;
|
|
1004
|
+
if (id != null)
|
|
1005
|
+
input(() => character.pointer(3, id, 0.5, 0.5, performance.now() / 1000));
|
|
570
1006
|
};
|
|
571
1007
|
const leave = () => {
|
|
572
|
-
if (pointerId == null)
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
1008
|
+
if (pointerId == null)
|
|
1009
|
+
input(() => {
|
|
1010
|
+
if (activityMode) character.clearReadyGaze(performance.now() / 1000);
|
|
1011
|
+
else character.pointer(1, 0, 0.5, 0.5, performance.now() / 1000);
|
|
1012
|
+
});
|
|
576
1013
|
};
|
|
577
1014
|
canvas.addEventListener('lostpointercapture', cancel);
|
|
578
1015
|
canvas.addEventListener('pointerleave', leave);
|
|
@@ -602,9 +1039,14 @@ async function createLiveCharacter(
|
|
|
602
1039
|
canvas.removeEventListener('lostpointercapture', cancel);
|
|
603
1040
|
canvas.removeEventListener('pointerleave', leave);
|
|
604
1041
|
window.removeEventListener('blur', cancel);
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
1042
|
+
const release = () => {
|
|
1043
|
+
character.delete();
|
|
1044
|
+
finishPreparation(true);
|
|
1045
|
+
surfaceLease.release();
|
|
1046
|
+
lease?.release();
|
|
1047
|
+
};
|
|
1048
|
+
if (sharedRenderPending()) void waitForSharedRender().then(release);
|
|
1049
|
+
else release();
|
|
608
1050
|
if (!instances.size) {
|
|
609
1051
|
document.removeEventListener('visibilitychange', visibility);
|
|
610
1052
|
cancelAnimationFrame(frame);
|
|
@@ -614,6 +1056,7 @@ async function createLiveCharacter(
|
|
|
614
1056
|
try {
|
|
615
1057
|
update(initial);
|
|
616
1058
|
resize();
|
|
1059
|
+
bindController();
|
|
617
1060
|
} catch (error) {
|
|
618
1061
|
dispose();
|
|
619
1062
|
throw error;
|
|
@@ -628,18 +1071,21 @@ async function createLiveCharacter(
|
|
|
628
1071
|
lastReactionKind,
|
|
629
1072
|
lastActivityResult,
|
|
630
1073
|
activityMode,
|
|
1074
|
+
stoppingWork,
|
|
1075
|
+
queuedReaction,
|
|
631
1076
|
ready,
|
|
632
1077
|
pending:
|
|
633
1078
|
dirty ||
|
|
634
|
-
|
|
635
|
-
|
|
1079
|
+
acquiringPreparation ||
|
|
1080
|
+
preparationQueue.length > 0 ||
|
|
1081
|
+
!scenePrepared(),
|
|
636
1082
|
}),
|
|
637
1083
|
};
|
|
638
1084
|
}
|
|
639
1085
|
|
|
640
1086
|
// A geometry cache must never serve a different silhouette under the same
|
|
641
|
-
// appearance key. Keep the old painted image while
|
|
642
|
-
async function
|
|
1087
|
+
// appearance key. Keep the old painted image while replacing the scoped character.
|
|
1088
|
+
async function createScopedCharacter(canvas, initial, callbacks) {
|
|
643
1089
|
let controller,
|
|
644
1090
|
shapeKey,
|
|
645
1091
|
revision = 0,
|
|
@@ -647,8 +1093,12 @@ async function createLegacyCharacter(canvas, initial, callbacks) {
|
|
|
647
1093
|
latest = initial;
|
|
648
1094
|
const update = async (next, silent = false) => {
|
|
649
1095
|
if (disposed) return;
|
|
1096
|
+
next = withCharacterGeometry(next);
|
|
650
1097
|
latest = next;
|
|
651
|
-
const key = JSON.stringify(
|
|
1098
|
+
const key = JSON.stringify([
|
|
1099
|
+
next.legacy?.points ?? null,
|
|
1100
|
+
authoredPartsFor(next),
|
|
1101
|
+
]);
|
|
652
1102
|
if (shapeKey === key) {
|
|
653
1103
|
controller?.update(next, silent);
|
|
654
1104
|
return;
|
|
@@ -708,7 +1158,8 @@ async function createLegacyCharacter(canvas, initial, callbacks) {
|
|
|
708
1158
|
},
|
|
709
1159
|
react: () => controller?.react(),
|
|
710
1160
|
work: () => controller?.work(),
|
|
711
|
-
diagnostics: () =>
|
|
1161
|
+
diagnostics: () =>
|
|
1162
|
+
controller?.diagnostics() ?? { ready: false, pending: !disposed },
|
|
712
1163
|
};
|
|
713
1164
|
}
|
|
714
1165
|
|
|
@@ -723,16 +1174,7 @@ function createPortrait(canvas, initial, callbacks) {
|
|
|
723
1174
|
cancel;
|
|
724
1175
|
const update = (props) => {
|
|
725
1176
|
if (disposed) return;
|
|
726
|
-
const pixels =
|
|
727
|
-
64,
|
|
728
|
-
Math.min(
|
|
729
|
-
512,
|
|
730
|
-
Math.round(
|
|
731
|
-
Math.min(canvas.clientWidth, canvas.clientHeight) *
|
|
732
|
-
Math.min(devicePixelRatio || 1, 2),
|
|
733
|
-
),
|
|
734
|
-
),
|
|
735
|
-
);
|
|
1177
|
+
const pixels = characterPixels(canvas);
|
|
736
1178
|
const key = JSON.stringify([
|
|
737
1179
|
props.config.character,
|
|
738
1180
|
props.legacy,
|
|
@@ -778,6 +1220,18 @@ function createPortrait(canvas, initial, callbacks) {
|
|
|
778
1220
|
canvas,
|
|
779
1221
|
{ ...props, paused: true, interactive: false },
|
|
780
1222
|
{
|
|
1223
|
+
onPreparationStart() {
|
|
1224
|
+
if (finished || !current()) return;
|
|
1225
|
+
// Waiting for another avatar's preparation is not engine work.
|
|
1226
|
+
// Start the deadline only after this controller is constructed.
|
|
1227
|
+
timer = setTimeout(() => {
|
|
1228
|
+
if (current())
|
|
1229
|
+
callbacks.onError?.(
|
|
1230
|
+
new Error('Character preview preparation timed out'),
|
|
1231
|
+
);
|
|
1232
|
+
finish();
|
|
1233
|
+
}, 20000);
|
|
1234
|
+
},
|
|
781
1235
|
onCapabilities(value) {
|
|
782
1236
|
capabilities = value;
|
|
783
1237
|
if (current()) callbacks.onCapabilities?.(value);
|
|
@@ -805,14 +1259,7 @@ function createPortrait(canvas, initial, callbacks) {
|
|
|
805
1259
|
if (finished || !current()) {
|
|
806
1260
|
controller.dispose();
|
|
807
1261
|
finish();
|
|
808
|
-
}
|
|
809
|
-
timer = setTimeout(() => {
|
|
810
|
-
if (current())
|
|
811
|
-
callbacks.onError?.(
|
|
812
|
-
new Error('Character preview preparation timed out'),
|
|
813
|
-
);
|
|
814
|
-
finish();
|
|
815
|
-
}, 20000);
|
|
1262
|
+
}
|
|
816
1263
|
await completion;
|
|
817
1264
|
} catch (error) {
|
|
818
1265
|
if (current()) callbacks.onError?.(error);
|
|
@@ -841,10 +1288,12 @@ const registryKey = Symbol.for('bloom.character.runtimes');
|
|
|
841
1288
|
export function runtimeStats() {
|
|
842
1289
|
return {
|
|
843
1290
|
renderMilliseconds: lastRenderMilliseconds,
|
|
1291
|
+
renderWaitMilliseconds: lastRenderWaitMilliseconds,
|
|
844
1292
|
budget: managedAvatarStats(),
|
|
1293
|
+
surface: sharedSurfaceStats(),
|
|
845
1294
|
instances: instances.size,
|
|
846
1295
|
active: clients.size,
|
|
847
|
-
scheduled: !!frame,
|
|
1296
|
+
scheduled: !!frame || rendering,
|
|
848
1297
|
cachedPortraits: portraits.size,
|
|
849
1298
|
legacy: legacyEngineStats(),
|
|
850
1299
|
};
|