@expofp/renderer 3.37.0 → 3.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/dist/controls/CameraControls.js +10 -1
- package/dist/objects/images/ImageInstance.js +14 -14
- package/dist/objects/images/Images.d.ts +11 -0
- package/dist/objects/images/Images.js +172 -55
- package/dist/objects/images/atlas-packer.d.ts +92 -91
- package/dist/objects/images/atlas-packer.js +230 -258
- package/dist/objects/images/pack-sources.d.ts +190 -0
- package/dist/objects/images/pack-sources.js +272 -0
- package/dist/objects/images/texture-budget.js +2 -2
- package/dist/objects/images/worker/atlas-worker.d.ts +56 -0
- package/dist/objects/images/worker/atlas-worker.js +123 -0
- package/dist/objects/images/worker/client.d.ts +62 -0
- package/dist/objects/images/worker/client.js +249 -0
- package/dist/objects/images/worker/index.d.ts +10 -0
- package/dist/objects/images/worker/index.js +1 -0
- package/dist/objects/materials/TextureMaterial.js +11 -1
- package/dist/objects/materials/effects/color-dim.js +17 -6
- package/dist/types/defs.d.ts +16 -2
- package/package.json +2 -2
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type SourceReport } from '../pack-sources';
|
|
2
|
+
import { type WorkerAtlas, type WorkerSource } from './atlas-worker';
|
|
3
|
+
/** Options for {@link packInWorker}. */
|
|
4
|
+
export interface PackInWorkerOptions {
|
|
5
|
+
/** Abandons the pack. The worker is told to drop it, and a reply that still arrives is thrown away. */
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
/**
|
|
8
|
+
* Makes the pooled worker, on the first pack that needs it. Defaults to the bundled atlas worker,
|
|
9
|
+
* and exists so a spec can drive the protocol against a fake one without a browser. It is ignored
|
|
10
|
+
* once a worker is pooled.
|
|
11
|
+
*/
|
|
12
|
+
createWorker?: () => Worker;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Packs a set of sources in a web worker.
|
|
16
|
+
*
|
|
17
|
+
* **One worker serves every pack, and no pack ever stops it.** The atlases come back as
|
|
18
|
+
* `ImageBitmap`s that `OffscreenCanvas.transferToImageBitmap()` produced, and Chrome backs those
|
|
19
|
+
* with GPU memory the worker owns. `Images` re-reads an atlas's pixels on every texture-budget
|
|
20
|
+
* move, and disposing a texture forces a fresh upload, so the producing worker has to outlive every
|
|
21
|
+
* bitmap it made, not only the first upload of it. Terminating the worker used to run before this
|
|
22
|
+
* function resolved, which destroyed those pixels and left booth logos blank on real GPUs.
|
|
23
|
+
*
|
|
24
|
+
* The one `terminate()` in this file is in `retire`, and it only ever stops a worker that broke
|
|
25
|
+
* before it handed out an atlas. A worker that has produced pixels is dropped from the pool and
|
|
26
|
+
* left running. The worker script is inlined into the bundle, so starting one resolves no URL at
|
|
27
|
+
* runtime, which matters because the SDK is loaded from origins it does not control.
|
|
28
|
+
*
|
|
29
|
+
* Each pack carries an `id`, because a shared worker can hold two layers' packs at once and a reply
|
|
30
|
+
* has to find its own caller. An abandoned pack is settled here and its `id` forgotten, so a reply
|
|
31
|
+
* that still arrives is discarded — and its atlases are closed, or each superseded pack would leak
|
|
32
|
+
* a layer's worth of GPU memory.
|
|
33
|
+
*
|
|
34
|
+
* The sources go over in batches so the main thread is never blocked for long by one structured
|
|
35
|
+
* clone, and so the worker can decode the early batches while the later ones are still being sent.
|
|
36
|
+
* A closing message tells it every source has arrived.
|
|
37
|
+
* @param sources - The unique sources to pack, in the caller's order; placements address them by
|
|
38
|
+
* their index in this array.
|
|
39
|
+
* @param maxDimension - Largest atlas edge in pixels.
|
|
40
|
+
* @param options - The abort signal, and the worker factory a spec overrides.
|
|
41
|
+
* @returns The packed atlases and one report per source.
|
|
42
|
+
*/
|
|
43
|
+
export declare function packInWorker(sources: readonly WorkerSource[], maxDimension: number, options?: PackInWorkerOptions): Promise<{
|
|
44
|
+
atlases: WorkerAtlas[];
|
|
45
|
+
reports: SourceReport[];
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Forgets the pooled worker without stopping it, and abandons every pack waiting on it.
|
|
49
|
+
*
|
|
50
|
+
* Only a spec calls this. Each spec drives its own fake worker, and the pool would otherwise hand
|
|
51
|
+
* the first spec's fake to every spec after it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resetAtlasWorkerPool(): void;
|
|
54
|
+
/**
|
|
55
|
+
* How many sources one `postMessage` carries.
|
|
56
|
+
*
|
|
57
|
+
* Sending every source in one message left 18 dropped frames at a thousand logos on a 4x-throttled
|
|
58
|
+
* CPU; sending them in batches of 25 left one. Chunking is what removes the last stall, so this is
|
|
59
|
+
* the number that matters most in this file — tune it against the benchmark, not by taste.
|
|
60
|
+
*/
|
|
61
|
+
export declare const SOURCE_CHUNK_SIZE = 25;
|
|
62
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/* eslint-disable-next-line import/default -- Vite answers this specifier with a module it
|
|
2
|
+
generates, whose default export is a `Worker` subclass. The ESLint resolver drops the query and
|
|
3
|
+
reads the worker entry instead, which exports no default of its own. `vite/client` declares the
|
|
4
|
+
real shape, so tsc sees it. */
|
|
5
|
+
import AtlasWorker from './atlas-worker?worker&inline';
|
|
6
|
+
/**
|
|
7
|
+
* Packs a set of sources in a web worker.
|
|
8
|
+
*
|
|
9
|
+
* **One worker serves every pack, and no pack ever stops it.** The atlases come back as
|
|
10
|
+
* `ImageBitmap`s that `OffscreenCanvas.transferToImageBitmap()` produced, and Chrome backs those
|
|
11
|
+
* with GPU memory the worker owns. `Images` re-reads an atlas's pixels on every texture-budget
|
|
12
|
+
* move, and disposing a texture forces a fresh upload, so the producing worker has to outlive every
|
|
13
|
+
* bitmap it made, not only the first upload of it. Terminating the worker used to run before this
|
|
14
|
+
* function resolved, which destroyed those pixels and left booth logos blank on real GPUs.
|
|
15
|
+
*
|
|
16
|
+
* The one `terminate()` in this file is in `retire`, and it only ever stops a worker that broke
|
|
17
|
+
* before it handed out an atlas. A worker that has produced pixels is dropped from the pool and
|
|
18
|
+
* left running. The worker script is inlined into the bundle, so starting one resolves no URL at
|
|
19
|
+
* runtime, which matters because the SDK is loaded from origins it does not control.
|
|
20
|
+
*
|
|
21
|
+
* Each pack carries an `id`, because a shared worker can hold two layers' packs at once and a reply
|
|
22
|
+
* has to find its own caller. An abandoned pack is settled here and its `id` forgotten, so a reply
|
|
23
|
+
* that still arrives is discarded — and its atlases are closed, or each superseded pack would leak
|
|
24
|
+
* a layer's worth of GPU memory.
|
|
25
|
+
*
|
|
26
|
+
* The sources go over in batches so the main thread is never blocked for long by one structured
|
|
27
|
+
* clone, and so the worker can decode the early batches while the later ones are still being sent.
|
|
28
|
+
* A closing message tells it every source has arrived.
|
|
29
|
+
* @param sources - The unique sources to pack, in the caller's order; placements address them by
|
|
30
|
+
* their index in this array.
|
|
31
|
+
* @param maxDimension - Largest atlas edge in pixels.
|
|
32
|
+
* @param options - The abort signal, and the worker factory a spec overrides.
|
|
33
|
+
* @returns The packed atlases and one report per source.
|
|
34
|
+
*/
|
|
35
|
+
export function packInWorker(sources, maxDimension, options = {}) {
|
|
36
|
+
const { signal, createWorker = createAtlasWorker } = options;
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
// Read the signal before anything is sent. A pack that is already abandoned must cost the
|
|
39
|
+
// worker nothing, so it posts no message at all.
|
|
40
|
+
if (signal?.aborted) {
|
|
41
|
+
reject(abandoned());
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let worker;
|
|
45
|
+
try {
|
|
46
|
+
worker = pooledWorker(createWorker);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
// A host page whose `script-src` forbids `blob:` and `data:` makes `new Worker()` throw. The
|
|
50
|
+
// caller falls back to an inline pack.
|
|
51
|
+
reject(asError(error));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const id = ++lastRequestId;
|
|
55
|
+
// Every exit runs through here. Dropping this pack's entry is what stops delivery, because the
|
|
56
|
+
// worker is shared and goes on running.
|
|
57
|
+
const done = (settle) => {
|
|
58
|
+
signal?.removeEventListener('abort', onAbort);
|
|
59
|
+
pending.delete(id);
|
|
60
|
+
settle();
|
|
61
|
+
};
|
|
62
|
+
// A plain Error, not `signal.reason`, which Chrome 66 and Safari 15 do not have. Nothing
|
|
63
|
+
// branches on the error's type: every caller reads `signal.aborted` instead.
|
|
64
|
+
const onAbort = () => {
|
|
65
|
+
done(() => {
|
|
66
|
+
cancel(worker, id);
|
|
67
|
+
reject(abandoned());
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
pending.set(id, (message) => {
|
|
71
|
+
done(() => {
|
|
72
|
+
if (message.type === 'failed')
|
|
73
|
+
reject(new Error(message.message));
|
|
74
|
+
else
|
|
75
|
+
resolve({ atlases: message.atlases, reports: message.reports });
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
signal?.addEventListener('abort', onAbort);
|
|
79
|
+
try {
|
|
80
|
+
for (let start = 0; start < sources.length; start += SOURCE_CHUNK_SIZE) {
|
|
81
|
+
post(worker, {
|
|
82
|
+
type: 'sources',
|
|
83
|
+
id,
|
|
84
|
+
sources: sources.slice(start, start + SOURCE_CHUNK_SIZE),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
post(worker, { type: 'pack', id, maxDimension });
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
// `postMessage` throws when the platform refuses to copy a source. The worker keeps the
|
|
91
|
+
// batches that did arrive until it is told otherwise, and the closing message never comes.
|
|
92
|
+
done(() => {
|
|
93
|
+
cancel(worker, id);
|
|
94
|
+
reject(asError(error));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Forgets the pooled worker without stopping it, and abandons every pack waiting on it.
|
|
101
|
+
*
|
|
102
|
+
* Only a spec calls this. Each spec drives its own fake worker, and the pool would otherwise hand
|
|
103
|
+
* the first spec's fake to every spec after it.
|
|
104
|
+
*/
|
|
105
|
+
export function resetAtlasWorkerPool() {
|
|
106
|
+
pool = null;
|
|
107
|
+
pending.clear();
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Hands back the one worker every pack runs on, starting it on the first call.
|
|
111
|
+
*
|
|
112
|
+
* The reply handler lives on the worker and not on a pack, because the worker outlives every pack
|
|
113
|
+
* it serves and a reply for a pack nobody wants still has pixels to free.
|
|
114
|
+
* @param createWorker - Makes the worker, if there is not one yet.
|
|
115
|
+
* @returns The pooled worker.
|
|
116
|
+
*/
|
|
117
|
+
function pooledWorker(createWorker) {
|
|
118
|
+
if (pool)
|
|
119
|
+
return pool.worker;
|
|
120
|
+
// Not stored until it exists: a factory that throws must be retried by the next pack, not cached.
|
|
121
|
+
const started = { worker: createWorker(), produced: false };
|
|
122
|
+
started.worker.onmessage = (event) => {
|
|
123
|
+
const message = event.data;
|
|
124
|
+
const deliver = pending.get(message.id);
|
|
125
|
+
if (deliver) {
|
|
126
|
+
// These pixels are about to be mounted, so from here on this worker must not be stopped.
|
|
127
|
+
if (message.type === 'packed' && message.atlases.length > 0)
|
|
128
|
+
started.produced = true;
|
|
129
|
+
deliver(message);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Nobody is waiting: this pack was abandoned after the worker had already replied. An
|
|
133
|
+
// `ImageBitmap` holds its pixels outside the JavaScript heap and nothing else will ever close
|
|
134
|
+
// these, so every superseded pack would leak a layer's worth of GPU memory.
|
|
135
|
+
if (message.type === 'packed')
|
|
136
|
+
for (const atlas of message.atlases)
|
|
137
|
+
atlas.bitmap.close();
|
|
138
|
+
};
|
|
139
|
+
started.worker.onerror = (event) => {
|
|
140
|
+
retire(started);
|
|
141
|
+
failAll(event.message || 'atlas-worker: failed to start');
|
|
142
|
+
};
|
|
143
|
+
started.worker.onmessageerror = () => {
|
|
144
|
+
// This never fires on the primary target. `Worker`'s `messageerror` is `false` for Chrome,
|
|
145
|
+
// Chrome Android and Android WebView, and Safari only added it in 16.4, which is above the
|
|
146
|
+
// `safari15`/`ios15` floor (checked against `@mdn/browser-compat-data@6.1.5`, as installed
|
|
147
|
+
// here). Assigning it is free and Firefox has had it since 57, so it is kept for the browsers
|
|
148
|
+
// that do support it. It is not a guard the build targets can rely on.
|
|
149
|
+
//
|
|
150
|
+
// A reply that does not deserialize is lost, and it carries no id, so there is no way to tell
|
|
151
|
+
// whose it was. Without this the layer waits for a reply that never comes and stays blank.
|
|
152
|
+
retire(started);
|
|
153
|
+
failAll('atlas-worker: a reply could not be read');
|
|
154
|
+
};
|
|
155
|
+
pool = started;
|
|
156
|
+
return started.worker;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Drops a broken worker, so the next pack starts a fresh one.
|
|
160
|
+
*
|
|
161
|
+
* A worker that has handed out an atlas is dropped and left running. Stopping it would destroy the
|
|
162
|
+
* GPU memory behind every bitmap it made, which is the failure this whole file is shaped around. A
|
|
163
|
+
* worker that produced nothing is stopped, because nothing depends on it.
|
|
164
|
+
* @param broken - The pooled worker to drop.
|
|
165
|
+
*/
|
|
166
|
+
function retire(broken) {
|
|
167
|
+
if (pool === broken)
|
|
168
|
+
pool = null;
|
|
169
|
+
if (!broken.produced)
|
|
170
|
+
broken.worker.terminate();
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Abandons every pack waiting on the worker that just broke.
|
|
174
|
+
* @param message - What to reject each of them with.
|
|
175
|
+
*/
|
|
176
|
+
function failAll(message) {
|
|
177
|
+
for (const [id, deliver] of [...pending])
|
|
178
|
+
deliver({ type: 'failed', id, message });
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Tells the worker to drop a pack nobody wants any more.
|
|
182
|
+
*
|
|
183
|
+
* Termination used to do this. Now that the worker outlives its packs, a superseded pack would
|
|
184
|
+
* otherwise run to the end and hold the worker off the next one.
|
|
185
|
+
* @param worker - The pooled worker.
|
|
186
|
+
* @param id - The pack to drop.
|
|
187
|
+
*/
|
|
188
|
+
function cancel(worker, id) {
|
|
189
|
+
try {
|
|
190
|
+
post(worker, { type: 'cancel', id });
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// A worker that will not take this message is already unusable, and the pack it would have
|
|
194
|
+
// dropped is dropped either way.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Sends one request to the worker.
|
|
199
|
+
*
|
|
200
|
+
* **Nothing is ever transferred.** A `Blob` is a handle to an immutable, reference-counted byte
|
|
201
|
+
* store, so a structured clone copies the handle and not the bytes. Being cheap to clone is the
|
|
202
|
+
* property that matters here; being transferable is not, and transferring a Blob is not a thing the
|
|
203
|
+
* platform offers. A `Blob` is also the only source that reaches this function, so no object a def
|
|
204
|
+
* owns is ever detached by a pack.
|
|
205
|
+
*
|
|
206
|
+
* The typed parameter is why this function exists: `postMessage` takes `any`, so a message built at
|
|
207
|
+
* the call site would go unchecked.
|
|
208
|
+
* @param worker - The worker to send to.
|
|
209
|
+
* @param request - The message.
|
|
210
|
+
*/
|
|
211
|
+
function post(worker, request) {
|
|
212
|
+
worker.postMessage(request);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Makes the bundled atlas worker.
|
|
216
|
+
* @returns A fresh worker.
|
|
217
|
+
*/
|
|
218
|
+
function createAtlasWorker() {
|
|
219
|
+
return new AtlasWorker();
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The error every abandoned pack rejects with.
|
|
223
|
+
* @returns The error.
|
|
224
|
+
*/
|
|
225
|
+
function abandoned() {
|
|
226
|
+
return new Error('atlas-worker: the pack was abandoned');
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Turns whatever was thrown into an `Error`.
|
|
230
|
+
* @param error - What was thrown.
|
|
231
|
+
* @returns An error.
|
|
232
|
+
*/
|
|
233
|
+
function asError(error) {
|
|
234
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* How many sources one `postMessage` carries.
|
|
238
|
+
*
|
|
239
|
+
* Sending every source in one message left 18 dropped frames at a thousand logos on a 4x-throttled
|
|
240
|
+
* CPU; sending them in batches of 25 left one. Chunking is what removes the last stall, so this is
|
|
241
|
+
* the number that matters most in this file — tune it against the benchmark, not by taste.
|
|
242
|
+
*/
|
|
243
|
+
export const SOURCE_CHUNK_SIZE = 25;
|
|
244
|
+
/** Every pack that has been sent and is waiting for its reply, by request id. */
|
|
245
|
+
const pending = new Map();
|
|
246
|
+
/** The worker every pack runs on, once the first pack has started it. */
|
|
247
|
+
let pool = null;
|
|
248
|
+
/** The last id handed to a pack. Ids only ever climb, so no reply can find the wrong caller. */
|
|
249
|
+
let lastRequestId = 0;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The worker's public face: the main thread's half only.
|
|
3
|
+
*
|
|
4
|
+
* The worker entry is deliberately absent. It imports `packSources` and carries the
|
|
5
|
+
* `?worker&inline` specifier, and nothing outside this directory should pull either in by
|
|
6
|
+
* reaching past the barrel. Types are the exception, because they are erased at emit.
|
|
7
|
+
*/
|
|
8
|
+
export { type WorkerAtlas, type WorkerSource } from './atlas-worker';
|
|
9
|
+
export { packInWorker, type PackInWorkerOptions, SOURCE_CHUNK_SIZE } from './client';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { packInWorker, SOURCE_CHUNK_SIZE } from './client';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { OneFactor } from 'three';
|
|
2
3
|
import { useDimPatch } from './effects/color-dim';
|
|
3
4
|
import { sharedParams } from './shared';
|
|
4
5
|
/**
|
|
@@ -25,5 +26,14 @@ export function TextureMaterial(props) {
|
|
|
25
26
|
vMapUv = uv * ${uvOffset}.zw + ${uvOffset}.xy;
|
|
26
27
|
`.trim());
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
+
// An atlas texture is premultiplied (see `makeAtlasTexture`'s `premultiplyAlpha` comment), so
|
|
30
|
+
// this material needs the premultiplied blend, not `sharedParams`'s straight-alpha one.
|
|
31
|
+
// `blendSrc: OneFactor` is the only change: the source term becomes `1 * (c * a)`, which already
|
|
32
|
+
// equals the straight blend's own `c * a` source term, so the composited color comes out the
|
|
33
|
+
// same either way. `blendDst` stays `OneMinusSrcAlphaFactor` from `sharedParams`, since that half
|
|
34
|
+
// does not depend on how the source is stored.
|
|
35
|
+
//
|
|
36
|
+
// The override lives here, not in `sharedParams`, because `ColorMaterial` and `LineMaterial`
|
|
37
|
+
// share that object and their colors are never premultiplied.
|
|
38
|
+
return (_jsx("meshBasicMaterial", { ...sharedParams, blendSrc: OneFactor, map: map, onBeforeCompile: onBeforeCompile }));
|
|
29
39
|
}
|
|
@@ -166,14 +166,25 @@ const DIM_FUNCTION = 'vec4 dimColor(';
|
|
|
166
166
|
/**
|
|
167
167
|
* Dimming's file-scope declarations.
|
|
168
168
|
*
|
|
169
|
-
* Dim is a desaturation toward gray, not a darkening
|
|
169
|
+
* Dim is a desaturation toward gray, not a darkening. That is why it cannot be baked into a
|
|
170
170
|
* per-instance tint: a texel sampled per fragment has no tint to bake into.
|
|
171
171
|
*
|
|
172
|
-
* `dimColor`
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
172
|
+
* `dimColor` works on `gl_FragColor` whichever way it is stored, and hands it back stored the same
|
|
173
|
+
* way. It only ever scales `col.rgb`: `mix(c, dot(w, c) / k, t)` is linear and homogeneous in `c`,
|
|
174
|
+
* so scaling the input by any constant scales the output by that same constant. That homogeneity
|
|
175
|
+
* is also why a premultiplied, `SRGBColorSpace`-tagged atlas texture dims to the same composited
|
|
176
|
+
* pixels as the straight-alpha texture this renderer used before it. Measuring old against new
|
|
177
|
+
* leaves a small residual: at most about 8 of 255 on one channel, across every alpha and dim
|
|
178
|
+
* amount tested.
|
|
179
|
+
*
|
|
180
|
+
* `stories/objects/layer/Dim.stories.tsx` does not cover any of this. Its image layer draws
|
|
181
|
+
* `markerCanvas(tint)`, whose whole surface is opaque, so no pixel in that story ever carries a
|
|
182
|
+
* partial alpha for `dimColor` to distort.
|
|
183
|
+
*
|
|
184
|
+
* The imperative renderer divided rgb by alpha here and multiplied it back at the end. The round
|
|
185
|
+
* trip cancels for the same reason: `f(c / a) * a == f(c)` for every alpha. Dividing by alpha was
|
|
186
|
+
* also the one step that could produce a NaN, at `a == 0`, which SDF text edges generate in
|
|
187
|
+
* quantity. Dropping it changes no pixel.
|
|
177
188
|
*/
|
|
178
189
|
const DIM_DEFS = /* glsl */ `
|
|
179
190
|
uniform float ${LAYER_DIM};
|
package/dist/types/defs.d.ts
CHANGED
|
@@ -2,8 +2,22 @@ import { type Box, type LineLike, type Rect, type Shape } from '@expofp/geometry
|
|
|
2
2
|
import { type LayoutHandlers, type PointerHandlers } from './events';
|
|
3
3
|
/** A color as a css string or a packed numeric value. */
|
|
4
4
|
export type ColorInput = string | number;
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Any image-like source that the renderer can upload as a texture.
|
|
7
|
+
*
|
|
8
|
+
* A `Blob` is how anything fetched over the network arrives. It is a handle to an immutable byte
|
|
9
|
+
* store that the browser reference-counts. `postMessage` clones the handle, not the bytes. That is
|
|
10
|
+
* what lets atlas packing move to a worker. The renderer decodes the `Blob` itself, off the main
|
|
11
|
+
* thread wherever the environment allows it.
|
|
12
|
+
*
|
|
13
|
+
* `HTMLImageElement` is deliberately absent. Decoding one costs the main thread more than the
|
|
14
|
+
* packing it feeds: at a thousand logos on a throttled CPU, it stalled the page for two seconds.
|
|
15
|
+
* Leaving the member in the union would keep that slow path reachable. Pass a `Blob` instead.
|
|
16
|
+
*
|
|
17
|
+
* There is no `string` member either. The caller keeps all networking, so the renderer never
|
|
18
|
+
* learns about URLs, retries, or CORS.
|
|
19
|
+
*/
|
|
20
|
+
export type ImageSource = HTMLCanvasElement | ImageBitmap | OffscreenCanvas | Blob;
|
|
7
21
|
/** Scene definition */
|
|
8
22
|
export interface SceneDef {
|
|
9
23
|
/** Root layer of the scene tree */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expofp/renderer",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.38.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/expofp/efp-app.git",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"troika-three-text": "^0.52.5",
|
|
43
43
|
"tslib": "^2.3.0",
|
|
44
44
|
"webgl-memory": "^1.1.3",
|
|
45
|
-
"@expofp/geometry": "3.
|
|
45
|
+
"@expofp/geometry": "3.38.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@react-three/test-renderer": "^9.1.0",
|