@fcannizzaro/streamdeck-react 0.1.9 → 0.1.11
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/LICENSE +190 -21
- package/README.md +3 -1
- package/dist/action.d.ts +2 -2
- package/dist/action.js +2 -2
- package/dist/bundler-shared.d.ts +11 -0
- package/dist/bundler-shared.js +11 -0
- package/dist/context/event-bus.d.ts +1 -1
- package/dist/context/event-bus.js +1 -1
- package/dist/context/touchstrip-context.d.ts +2 -0
- package/dist/context/touchstrip-context.js +5 -0
- package/dist/devtools/bridge.d.ts +35 -7
- package/dist/devtools/bridge.js +153 -46
- package/dist/devtools/highlight.d.ts +6 -0
- package/dist/devtools/highlight.js +106 -57
- package/dist/devtools/index.js +6 -0
- package/dist/devtools/observers/lifecycle.d.ts +4 -4
- package/dist/devtools/server.d.ts +6 -1
- package/dist/devtools/server.js +6 -1
- package/dist/devtools/types.d.ts +50 -6
- package/dist/font-inline.d.ts +5 -1
- package/dist/font-inline.js +8 -3
- package/dist/hooks/animation.d.ts +154 -0
- package/dist/hooks/animation.js +381 -0
- package/dist/hooks/events.js +1 -5
- package/dist/hooks/touchstrip.d.ts +6 -0
- package/dist/hooks/touchstrip.js +37 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +3 -2
- package/dist/manifest-codegen.d.ts +38 -0
- package/dist/manifest-codegen.js +110 -0
- package/dist/node_modules/.bun/xxhash-wasm@1.1.0/node_modules/xxhash-wasm/esm/xxhash-wasm.js +3157 -0
- package/dist/plugin.js +20 -9
- package/dist/reconciler/host-config.js +19 -1
- package/dist/reconciler/vnode.d.ts +26 -0
- package/dist/reconciler/vnode.js +41 -10
- package/dist/render/buffer-pool.d.ts +19 -0
- package/dist/render/buffer-pool.js +51 -0
- package/dist/render/cache.d.ts +41 -0
- package/dist/render/cache.js +159 -5
- package/dist/render/image-cache.d.ts +53 -0
- package/dist/render/image-cache.js +128 -0
- package/dist/render/metrics.d.ts +58 -0
- package/dist/render/metrics.js +101 -0
- package/dist/render/pipeline.d.ts +46 -1
- package/dist/render/pipeline.js +370 -36
- package/dist/render/png.d.ts +10 -1
- package/dist/render/png.js +31 -13
- package/dist/render/render-pool.d.ts +26 -0
- package/dist/render/render-pool.js +141 -0
- package/dist/render/svg.d.ts +7 -0
- package/dist/render/svg.js +139 -0
- package/dist/render/worker.d.ts +1 -0
- package/dist/rollup.d.ts +23 -9
- package/dist/rollup.js +24 -9
- package/dist/roots/flush-coordinator.d.ts +18 -0
- package/dist/roots/flush-coordinator.js +38 -0
- package/dist/roots/registry.d.ts +6 -4
- package/dist/roots/registry.js +47 -33
- package/dist/roots/root.d.ts +32 -2
- package/dist/roots/root.js +104 -14
- package/dist/roots/settings-equality.d.ts +5 -0
- package/dist/roots/settings-equality.js +24 -0
- package/dist/roots/touchstrip-root.d.ts +93 -0
- package/dist/roots/touchstrip-root.js +383 -0
- package/dist/types.d.ts +62 -16
- package/dist/vite.d.ts +22 -8
- package/dist/vite.js +24 -8
- package/package.json +5 -4
- package/dist/context/touchbar-context.d.ts +0 -2
- package/dist/context/touchbar-context.js +0 -5
- package/dist/hooks/touchbar.d.ts +0 -6
- package/dist/hooks/touchbar.js +0 -37
- package/dist/roots/touchbar-root.d.ts +0 -45
- package/dist/roots/touchbar-root.js +0 -175
package/dist/render/pipeline.js
CHANGED
|
@@ -1,62 +1,364 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { fnv1a } from "./cache.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { clearDirtyFlags, isContainerDirty } from "../reconciler/vnode.js";
|
|
2
|
+
import { computeCacheKey, computeTreeHash, fnv1a } from "./cache.js";
|
|
3
|
+
import { getBufferPool } from "./buffer-pool.js";
|
|
4
|
+
import { encodePngAsync } from "./png.js";
|
|
5
|
+
import { getImageCache, getTouchstripCache } from "./image-cache.js";
|
|
6
|
+
import { metrics } from "./metrics.js";
|
|
7
|
+
import { serializeSvgTree } from "./svg.js";
|
|
6
8
|
//#region src/render/pipeline.ts
|
|
9
|
+
var ROOT_STYLE = {
|
|
10
|
+
display: "flex",
|
|
11
|
+
width: "100%",
|
|
12
|
+
height: "100%"
|
|
13
|
+
};
|
|
14
|
+
/** Warn threshold for consecutive identical renders in debug mode. */
|
|
15
|
+
var DUP_RENDER_WARN_THRESHOLD = 3;
|
|
16
|
+
/** Maximum recommended VNode tree depth. Warn in debug mode when exceeded. */
|
|
17
|
+
var MAX_DEPTH_WARN = 25;
|
|
18
|
+
/** Whether we've already warned about tree depth (avoid log spam). */
|
|
19
|
+
var depthWarned = false;
|
|
20
|
+
var SKIP_PROPS = new Set([
|
|
21
|
+
"children",
|
|
22
|
+
"className",
|
|
23
|
+
"src"
|
|
24
|
+
]);
|
|
25
|
+
function copyPropsToNode(target, props) {
|
|
26
|
+
const keys = Object.keys(props);
|
|
27
|
+
for (let i = 0; i < keys.length; i++) {
|
|
28
|
+
const key = keys[i];
|
|
29
|
+
if (SKIP_PROPS.has(key)) continue;
|
|
30
|
+
const value = props[key];
|
|
31
|
+
if (key === "tw") continue;
|
|
32
|
+
target[key] = value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function vnodeToTakumiNode(node, depth = 0) {
|
|
36
|
+
if (!depthWarned && depth > MAX_DEPTH_WARN) {
|
|
37
|
+
depthWarned = true;
|
|
38
|
+
console.warn(`[@fcannizzaro/streamdeck-react] VNode tree depth ${depth} exceeds recommended limit ${MAX_DEPTH_WARN}. Deep nesting increases render cost.`);
|
|
39
|
+
}
|
|
40
|
+
if (node.type === "#text") return {
|
|
41
|
+
type: "text",
|
|
42
|
+
text: node.text ?? ""
|
|
43
|
+
};
|
|
44
|
+
const props = node.props;
|
|
45
|
+
const rawTw = typeof props.tw === "string" ? props.tw : void 0;
|
|
46
|
+
const className = props.className;
|
|
47
|
+
let tw = rawTw;
|
|
48
|
+
if (typeof className === "string" && className.length > 0) tw = tw ? tw + " " + className : className;
|
|
49
|
+
if (node.type === "img" && typeof props.src === "string") {
|
|
50
|
+
const result = {
|
|
51
|
+
type: "image",
|
|
52
|
+
src: props.src
|
|
53
|
+
};
|
|
54
|
+
if (tw) result.tw = tw;
|
|
55
|
+
copyPropsToNode(result, props);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
if (node.type === "svg") {
|
|
59
|
+
const result = {
|
|
60
|
+
type: "image",
|
|
61
|
+
src: serializeSvgTree(node),
|
|
62
|
+
tagName: "svg"
|
|
63
|
+
};
|
|
64
|
+
const width = typeof props.width === "number" ? props.width : void 0;
|
|
65
|
+
const height = typeof props.height === "number" ? props.height : void 0;
|
|
66
|
+
if (width != null) result.width = width;
|
|
67
|
+
if (height != null) result.height = height;
|
|
68
|
+
if (tw) result.tw = tw;
|
|
69
|
+
if (props.style) result.style = props.style;
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
const result = { type: "container" };
|
|
73
|
+
if (tw) result.tw = tw;
|
|
74
|
+
copyPropsToNode(result, props);
|
|
75
|
+
if (node.children.length > 0) result.children = node.children.map((child) => vnodeToTakumiNode(child, depth + 1));
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
/** Build the root Takumi container wrapping the VNode children. */
|
|
79
|
+
function buildTakumiRoot(container) {
|
|
80
|
+
return {
|
|
81
|
+
type: "container",
|
|
82
|
+
style: ROOT_STYLE,
|
|
83
|
+
children: container.children.map(vnodeToTakumiNode)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Convert a container's VNode children to Takumi nodes.
|
|
88
|
+
* Used by the touchstrip native-format path to build the Takumi node tree
|
|
89
|
+
* once and share it across all N segment renders in a single flush.
|
|
90
|
+
*/
|
|
91
|
+
function buildTakumiChildren(container) {
|
|
92
|
+
return container.children.map(vnodeToTakumiNode);
|
|
93
|
+
}
|
|
94
|
+
function measureTree(nodes) {
|
|
95
|
+
let maxDepth = 0;
|
|
96
|
+
let count = 0;
|
|
97
|
+
function walk(children, depth) {
|
|
98
|
+
for (const child of children) {
|
|
99
|
+
count++;
|
|
100
|
+
if (depth > maxDepth) maxDepth = depth;
|
|
101
|
+
walk(child.children, depth + 1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
walk(nodes, 1);
|
|
105
|
+
return {
|
|
106
|
+
depth: maxDepth,
|
|
107
|
+
count
|
|
108
|
+
};
|
|
109
|
+
}
|
|
7
110
|
function bufferToDataUri(buffer, format) {
|
|
8
111
|
return `data:image/${format};base64,${(Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer)).toString("base64")}`;
|
|
9
112
|
}
|
|
113
|
+
function emitProfile(config, times, opts) {
|
|
114
|
+
const stats = measureTree(opts.container.children);
|
|
115
|
+
const cache = config.imageCacheMaxBytes > 0 ? getImageCache(config.imageCacheMaxBytes) : null;
|
|
116
|
+
config.onProfile({
|
|
117
|
+
vnodeToElementMs: times.t1 - times.t0,
|
|
118
|
+
fromJsxMs: times.t2 - times.t1,
|
|
119
|
+
takumiRenderMs: times.t3 - times.t2,
|
|
120
|
+
hashMs: times.t4 - times.t3,
|
|
121
|
+
base64Ms: times.t5 - times.t4,
|
|
122
|
+
totalMs: times.t5 - times.t0,
|
|
123
|
+
skipped: opts.skipped,
|
|
124
|
+
cacheHit: opts.cacheHit,
|
|
125
|
+
treeDepth: stats.depth,
|
|
126
|
+
nodeCount: stats.count,
|
|
127
|
+
cacheStats: cache?.stats ?? null
|
|
128
|
+
});
|
|
129
|
+
}
|
|
10
130
|
async function renderToDataUri(container, width, height, config) {
|
|
11
131
|
if (container.children.length === 0) return null;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
132
|
+
metrics.recordFlush();
|
|
133
|
+
if (config.caching && !isContainerDirty(container)) {
|
|
134
|
+
metrics.recordDirtySkip();
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const profiling = config.onProfile != null;
|
|
138
|
+
const t0 = profiling ? performance.now() : 0;
|
|
139
|
+
let t1 = t0;
|
|
140
|
+
let t2 = t0;
|
|
141
|
+
let t3 = t0;
|
|
142
|
+
let treeHash;
|
|
143
|
+
let cacheKey;
|
|
144
|
+
if (config.caching && config.imageCacheMaxBytes > 0) {
|
|
145
|
+
treeHash = computeTreeHash(container);
|
|
146
|
+
cacheKey = computeCacheKey(treeHash, width, height, config.devicePixelRatio, config.imageFormat);
|
|
147
|
+
const cached = getImageCache(config.imageCacheMaxBytes).get(cacheKey);
|
|
148
|
+
if (cached !== void 0) {
|
|
149
|
+
metrics.recordCacheHit();
|
|
150
|
+
if (profiling) {
|
|
151
|
+
const tNow = performance.now();
|
|
152
|
+
emitProfile(config, {
|
|
153
|
+
t0,
|
|
154
|
+
t1: tNow,
|
|
155
|
+
t2: tNow,
|
|
156
|
+
t3: tNow,
|
|
157
|
+
t4: tNow,
|
|
158
|
+
t5: tNow
|
|
159
|
+
}, {
|
|
160
|
+
skipped: false,
|
|
161
|
+
cacheHit: true,
|
|
162
|
+
container
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
container._dupCount = 0;
|
|
166
|
+
config.onRender?.(container, cached);
|
|
167
|
+
clearDirtyFlags(container);
|
|
168
|
+
return cached;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
let buffer;
|
|
172
|
+
if (config.renderPool?.isAvailable) {
|
|
173
|
+
buffer = await config.renderPool.render(container.children, width, height, config.imageFormat, config.devicePixelRatio);
|
|
174
|
+
t3 = profiling ? performance.now() : 0;
|
|
175
|
+
t1 = t0;
|
|
176
|
+
t2 = t0;
|
|
177
|
+
} else {
|
|
178
|
+
const rootNode = buildTakumiRoot(container);
|
|
179
|
+
t1 = profiling ? performance.now() : 0;
|
|
180
|
+
t2 = t1;
|
|
181
|
+
buffer = await config.renderer.render(rootNode, {
|
|
182
|
+
width,
|
|
183
|
+
height,
|
|
184
|
+
format: config.imageFormat,
|
|
185
|
+
devicePixelRatio: config.devicePixelRatio
|
|
186
|
+
});
|
|
187
|
+
t3 = profiling ? performance.now() : 0;
|
|
188
|
+
}
|
|
24
189
|
if (config.caching) {
|
|
25
190
|
const hash = fnv1a(buffer);
|
|
26
|
-
if (hash === container.lastSvgHash)
|
|
191
|
+
if (hash === container.lastSvgHash) {
|
|
192
|
+
metrics.recordHashDedup();
|
|
193
|
+
if (config.debug) {
|
|
194
|
+
container._dupCount++;
|
|
195
|
+
if (container._dupCount > DUP_RENDER_WARN_THRESHOLD) console.warn(`[@fcannizzaro/streamdeck-react] ${container._dupCount} consecutive identical renders — component likely re-rendering without visual change`);
|
|
196
|
+
}
|
|
197
|
+
if (profiling) {
|
|
198
|
+
const t4 = performance.now();
|
|
199
|
+
emitProfile(config, {
|
|
200
|
+
t0,
|
|
201
|
+
t1,
|
|
202
|
+
t2,
|
|
203
|
+
t3,
|
|
204
|
+
t4,
|
|
205
|
+
t5: t4
|
|
206
|
+
}, {
|
|
207
|
+
skipped: true,
|
|
208
|
+
cacheHit: false,
|
|
209
|
+
container
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
clearDirtyFlags(container);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
27
215
|
container.lastSvgHash = hash;
|
|
216
|
+
container._dupCount = 0;
|
|
28
217
|
}
|
|
218
|
+
const t4 = profiling ? performance.now() : 0;
|
|
29
219
|
const dataUri = bufferToDataUri(buffer, config.imageFormat);
|
|
220
|
+
const t5 = profiling ? performance.now() : 0;
|
|
221
|
+
if (config.caching && config.imageCacheMaxBytes > 0) {
|
|
222
|
+
if (treeHash === void 0 || cacheKey === void 0) {
|
|
223
|
+
treeHash = computeTreeHash(container);
|
|
224
|
+
cacheKey = computeCacheKey(treeHash, width, height, config.devicePixelRatio, config.imageFormat);
|
|
225
|
+
}
|
|
226
|
+
getImageCache(config.imageCacheMaxBytes).set(cacheKey, dataUri, dataUri.length * 2 + 64);
|
|
227
|
+
}
|
|
228
|
+
metrics.recordRender(t3 - t0);
|
|
229
|
+
if (profiling) emitProfile(config, {
|
|
230
|
+
t0,
|
|
231
|
+
t1,
|
|
232
|
+
t2,
|
|
233
|
+
t3,
|
|
234
|
+
t4,
|
|
235
|
+
t5
|
|
236
|
+
}, {
|
|
237
|
+
skipped: false,
|
|
238
|
+
cacheHit: false,
|
|
239
|
+
container
|
|
240
|
+
});
|
|
30
241
|
config.onRender?.(container, dataUri);
|
|
242
|
+
clearDirtyFlags(container);
|
|
31
243
|
return dataUri;
|
|
32
244
|
}
|
|
33
245
|
async function renderToRaw(container, width, height, config) {
|
|
34
246
|
if (container.children.length === 0) return null;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
247
|
+
metrics.recordFlush();
|
|
248
|
+
if (config.caching && !isContainerDirty(container)) {
|
|
249
|
+
metrics.recordDirtySkip();
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const profiling = config.onProfile != null;
|
|
253
|
+
const t0 = profiling ? performance.now() : 0;
|
|
254
|
+
let t1 = t0;
|
|
255
|
+
let t3 = t0;
|
|
256
|
+
let treeHash;
|
|
257
|
+
let cacheKey;
|
|
258
|
+
if (config.caching && config.touchstripCacheMaxBytes > 0) {
|
|
259
|
+
treeHash = computeTreeHash(container);
|
|
260
|
+
cacheKey = computeCacheKey(treeHash, width, height, config.devicePixelRatio, "raw");
|
|
261
|
+
const cached = getTouchstripCache(config.touchstripCacheMaxBytes).get(cacheKey);
|
|
262
|
+
if (cached !== void 0) {
|
|
263
|
+
metrics.recordCacheHit();
|
|
264
|
+
if (profiling) {
|
|
265
|
+
const tNow = performance.now();
|
|
266
|
+
emitProfile(config, {
|
|
267
|
+
t0,
|
|
268
|
+
t1: tNow,
|
|
269
|
+
t2: tNow,
|
|
270
|
+
t3: tNow,
|
|
271
|
+
t4: tNow,
|
|
272
|
+
t5: tNow
|
|
273
|
+
}, {
|
|
274
|
+
skipped: false,
|
|
275
|
+
cacheHit: true,
|
|
276
|
+
container
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
clearDirtyFlags(container);
|
|
280
|
+
return {
|
|
281
|
+
buffer: cached,
|
|
282
|
+
width,
|
|
283
|
+
height
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
let buffer;
|
|
288
|
+
if (config.renderPool?.isAvailable) {
|
|
289
|
+
buffer = await config.renderPool.render(container.children, width, height, "raw", config.devicePixelRatio);
|
|
290
|
+
t3 = profiling ? performance.now() : 0;
|
|
291
|
+
t1 = t0;
|
|
292
|
+
} else {
|
|
293
|
+
const rootNode = buildTakumiRoot(container);
|
|
294
|
+
t1 = profiling ? performance.now() : 0;
|
|
295
|
+
buffer = await config.renderer.render(rootNode, {
|
|
296
|
+
width,
|
|
297
|
+
height,
|
|
298
|
+
format: "raw",
|
|
299
|
+
devicePixelRatio: config.devicePixelRatio
|
|
300
|
+
});
|
|
301
|
+
t3 = profiling ? performance.now() : 0;
|
|
302
|
+
}
|
|
47
303
|
if (config.caching) {
|
|
48
304
|
const hash = fnv1a(buffer);
|
|
49
|
-
if (hash === container.lastSvgHash)
|
|
305
|
+
if (hash === container.lastSvgHash) {
|
|
306
|
+
metrics.recordHashDedup();
|
|
307
|
+
if (profiling) {
|
|
308
|
+
const t4 = performance.now();
|
|
309
|
+
emitProfile(config, {
|
|
310
|
+
t0,
|
|
311
|
+
t1,
|
|
312
|
+
t2: t1,
|
|
313
|
+
t3,
|
|
314
|
+
t4,
|
|
315
|
+
t5: t4
|
|
316
|
+
}, {
|
|
317
|
+
skipped: true,
|
|
318
|
+
cacheHit: false,
|
|
319
|
+
container
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
clearDirtyFlags(container);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
50
325
|
container.lastSvgHash = hash;
|
|
51
326
|
}
|
|
327
|
+
const buf = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
|
|
328
|
+
if (config.caching && config.touchstripCacheMaxBytes > 0) {
|
|
329
|
+
if (treeHash === void 0 || cacheKey === void 0) {
|
|
330
|
+
treeHash = computeTreeHash(container);
|
|
331
|
+
cacheKey = computeCacheKey(treeHash, width, height, config.devicePixelRatio, "raw");
|
|
332
|
+
}
|
|
333
|
+
getTouchstripCache(config.touchstripCacheMaxBytes).set(cacheKey, buf, buf.byteLength + 64);
|
|
334
|
+
}
|
|
335
|
+
metrics.recordRender(t3 - t0);
|
|
336
|
+
if (profiling) {
|
|
337
|
+
const tEnd = performance.now();
|
|
338
|
+
emitProfile(config, {
|
|
339
|
+
t0,
|
|
340
|
+
t1,
|
|
341
|
+
t2: t1,
|
|
342
|
+
t3,
|
|
343
|
+
t4: tEnd,
|
|
344
|
+
t5: tEnd
|
|
345
|
+
}, {
|
|
346
|
+
skipped: false,
|
|
347
|
+
cacheHit: false,
|
|
348
|
+
container
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
clearDirtyFlags(container);
|
|
52
352
|
return {
|
|
53
|
-
buffer:
|
|
353
|
+
buffer: buf,
|
|
54
354
|
width,
|
|
55
355
|
height
|
|
56
356
|
};
|
|
57
357
|
}
|
|
58
358
|
function cropSlice(raw, fullWidth, column, segmentWidth, segmentHeight) {
|
|
59
|
-
const
|
|
359
|
+
const pool = getBufferPool();
|
|
360
|
+
const sliceSize = segmentWidth * segmentHeight * 4;
|
|
361
|
+
const slice = pool.acquire(sliceSize);
|
|
60
362
|
const srcRowBytes = fullWidth * 4;
|
|
61
363
|
const dstRowBytes = segmentWidth * 4;
|
|
62
364
|
const xOffset = column * segmentWidth * 4;
|
|
@@ -67,8 +369,40 @@ function cropSlice(raw, fullWidth, column, segmentWidth, segmentHeight) {
|
|
|
67
369
|
}
|
|
68
370
|
return slice;
|
|
69
371
|
}
|
|
70
|
-
function
|
|
71
|
-
|
|
372
|
+
async function sliceToDataUriAsync(raw, fullWidth, fullHeight, column, segmentWidth, segmentHeight) {
|
|
373
|
+
const cropped = cropSlice(raw, fullWidth, column, segmentWidth, segmentHeight);
|
|
374
|
+
const png = await encodePngAsync(segmentWidth, segmentHeight, cropped);
|
|
375
|
+
getBufferPool().release(cropped);
|
|
376
|
+
return bufferToDataUri(png, "png");
|
|
377
|
+
}
|
|
378
|
+
async function renderSegmentToDataUri(container, fullWidth, segmentHeight, column, segmentWidth, format, config, prebuiltTakumiChildren) {
|
|
379
|
+
if (container.children.length === 0) return null;
|
|
380
|
+
const children = prebuiltTakumiChildren ?? container.children.map(vnodeToTakumiNode);
|
|
381
|
+
const innerNode = {
|
|
382
|
+
type: "container",
|
|
383
|
+
style: {
|
|
384
|
+
...ROOT_STYLE,
|
|
385
|
+
width: fullWidth,
|
|
386
|
+
height: segmentHeight,
|
|
387
|
+
marginLeft: -(column * segmentWidth)
|
|
388
|
+
},
|
|
389
|
+
children
|
|
390
|
+
};
|
|
391
|
+
const clipNode = {
|
|
392
|
+
type: "container",
|
|
393
|
+
style: {
|
|
394
|
+
width: segmentWidth,
|
|
395
|
+
height: segmentHeight,
|
|
396
|
+
overflow: "hidden"
|
|
397
|
+
},
|
|
398
|
+
children: [innerNode]
|
|
399
|
+
};
|
|
400
|
+
return bufferToDataUri(await config.renderer.render(clipNode, {
|
|
401
|
+
width: segmentWidth,
|
|
402
|
+
height: segmentHeight,
|
|
403
|
+
format,
|
|
404
|
+
devicePixelRatio: config.devicePixelRatio
|
|
405
|
+
}), format);
|
|
72
406
|
}
|
|
73
407
|
//#endregion
|
|
74
|
-
export { bufferToDataUri, renderToDataUri, renderToRaw,
|
|
408
|
+
export { bufferToDataUri, buildTakumiChildren, buildTakumiRoot, measureTree, renderSegmentToDataUri, renderToDataUri, renderToRaw, sliceToDataUriAsync };
|
package/dist/render/png.d.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Encode raw RGBA pixels into a PNG buffer.
|
|
2
|
+
* Encode raw RGBA pixels into a PNG buffer (synchronous).
|
|
3
3
|
*
|
|
4
4
|
* @param width Image width in pixels.
|
|
5
5
|
* @param height Image height in pixels.
|
|
6
6
|
* @param rgba Raw RGBA pixel data (width × height × 4 bytes, row-major).
|
|
7
7
|
*/
|
|
8
8
|
export declare function encodePng(width: number, height: number, rgba: Buffer | Uint8Array): Buffer;
|
|
9
|
+
/**
|
|
10
|
+
* Encode raw RGBA pixels into a PNG buffer (async).
|
|
11
|
+
* Uses libuv thread pool for deflate compression, avoiding main-thread blocking.
|
|
12
|
+
*
|
|
13
|
+
* @param width Image width in pixels.
|
|
14
|
+
* @param height Image height in pixels.
|
|
15
|
+
* @param rgba Raw RGBA pixel data (width × height × 4 bytes, row-major).
|
|
16
|
+
*/
|
|
17
|
+
export declare function encodePngAsync(width: number, height: number, rgba: Buffer | Uint8Array): Promise<Buffer>;
|
package/dist/render/png.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getBufferPool } from "./buffer-pool.js";
|
|
2
|
+
import { deflate } from "node:zlib";
|
|
3
|
+
import { promisify } from "node:util";
|
|
2
4
|
//#region src/render/png.ts
|
|
3
5
|
var crcTable = new Uint32Array(256);
|
|
4
6
|
for (let n = 0; n < 256; n++) {
|
|
@@ -35,14 +37,8 @@ var PNG_SIGNATURE = Buffer.from([
|
|
|
35
37
|
26,
|
|
36
38
|
10
|
|
37
39
|
]);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
*
|
|
41
|
-
* @param width Image width in pixels.
|
|
42
|
-
* @param height Image height in pixels.
|
|
43
|
-
* @param rgba Raw RGBA pixel data (width × height × 4 bytes, row-major).
|
|
44
|
-
*/
|
|
45
|
-
function encodePng(width, height, rgba) {
|
|
40
|
+
var deflateAsync = promisify(deflate);
|
|
41
|
+
function buildIhdr(width, height) {
|
|
46
42
|
const ihdr = Buffer.alloc(13);
|
|
47
43
|
ihdr.writeUInt32BE(width, 0);
|
|
48
44
|
ihdr.writeUInt32BE(height, 4);
|
|
@@ -51,15 +47,22 @@ function encodePng(width, height, rgba) {
|
|
|
51
47
|
ihdr[10] = 0;
|
|
52
48
|
ihdr[11] = 0;
|
|
53
49
|
ihdr[12] = 0;
|
|
50
|
+
return ihdr;
|
|
51
|
+
}
|
|
52
|
+
function buildFilteredScanlines(width, height, rgba) {
|
|
54
53
|
const rowBytes = width * 4;
|
|
55
|
-
const
|
|
54
|
+
const filteredSize = height * (1 + rowBytes);
|
|
55
|
+
const filtered = getBufferPool().acquire(filteredSize);
|
|
56
56
|
for (let y = 0; y < height; y++) {
|
|
57
57
|
const dstOff = y * (1 + rowBytes);
|
|
58
58
|
filtered[dstOff] = 0;
|
|
59
59
|
const srcOff = y * rowBytes;
|
|
60
|
-
|
|
60
|
+
if (Buffer.isBuffer(rgba)) rgba.copy(filtered, dstOff + 1, srcOff, srcOff + rowBytes);
|
|
61
|
+
else filtered.set(rgba.subarray(srcOff, srcOff + rowBytes), dstOff + 1);
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
+
return filtered;
|
|
64
|
+
}
|
|
65
|
+
function assemblePng(ihdr, compressed) {
|
|
63
66
|
return Buffer.concat([
|
|
64
67
|
PNG_SIGNATURE,
|
|
65
68
|
pngChunk("IHDR", ihdr),
|
|
@@ -67,5 +70,20 @@ function encodePng(width, height, rgba) {
|
|
|
67
70
|
pngChunk("IEND", Buffer.alloc(0))
|
|
68
71
|
]);
|
|
69
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Encode raw RGBA pixels into a PNG buffer (async).
|
|
75
|
+
* Uses libuv thread pool for deflate compression, avoiding main-thread blocking.
|
|
76
|
+
*
|
|
77
|
+
* @param width Image width in pixels.
|
|
78
|
+
* @param height Image height in pixels.
|
|
79
|
+
* @param rgba Raw RGBA pixel data (width × height × 4 bytes, row-major).
|
|
80
|
+
*/
|
|
81
|
+
async function encodePngAsync(width, height, rgba) {
|
|
82
|
+
const ihdr = buildIhdr(width, height);
|
|
83
|
+
const filtered = buildFilteredScanlines(width, height, rgba);
|
|
84
|
+
const compressed = await deflateAsync(filtered);
|
|
85
|
+
getBufferPool().release(filtered);
|
|
86
|
+
return assemblePng(ihdr, compressed);
|
|
87
|
+
}
|
|
70
88
|
//#endregion
|
|
71
|
-
export {
|
|
89
|
+
export { encodePngAsync };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FontConfig } from '../types';
|
|
2
|
+
import { VNode } from '../reconciler/vnode';
|
|
3
|
+
export declare class RenderPool {
|
|
4
|
+
private worker;
|
|
5
|
+
private ready;
|
|
6
|
+
private failed;
|
|
7
|
+
private nextId;
|
|
8
|
+
private pending;
|
|
9
|
+
private initPromise;
|
|
10
|
+
private fonts;
|
|
11
|
+
constructor(fonts: FontConfig[]);
|
|
12
|
+
/** Start the worker. Call once during plugin initialization. */
|
|
13
|
+
initialize(): Promise<boolean>;
|
|
14
|
+
private doInitialize;
|
|
15
|
+
/** Whether the worker is available for offloaded rendering. */
|
|
16
|
+
get isAvailable(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Render VNode children in the worker thread.
|
|
19
|
+
* Returns the raw raster buffer.
|
|
20
|
+
*/
|
|
21
|
+
render(vnodes: VNode[], width: number, height: number, format: string, dpr: number): Promise<Buffer>;
|
|
22
|
+
/** Gracefully shut down the worker. */
|
|
23
|
+
shutdown(): Promise<void>;
|
|
24
|
+
private handleResponse;
|
|
25
|
+
private handleWorkerDeath;
|
|
26
|
+
}
|