@perspective-dev/viewer-charts 5.1.0 → 5.3.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/dist/cdn/perspective-viewer-charts.js +2 -2
- package/dist/cdn/perspective-viewer-charts.js.map +4 -4
- package/dist/esm/charts/candlestick/candlestick-render.d.ts +3 -0
- package/dist/esm/charts/cartesian/cartesian.d.ts +1 -0
- package/dist/esm/charts/chart-base.d.ts +22 -9
- package/dist/esm/charts/chart.d.ts +4 -0
- package/dist/esm/charts/common/category-axis-resolver.d.ts +1 -1
- package/dist/esm/charts/common/tree-chart.d.ts +1 -0
- package/dist/esm/charts/heatmap/heatmap.d.ts +1 -0
- package/dist/esm/charts/series/series-type.d.ts +18 -0
- package/dist/esm/charts/series/series.d.ts +4 -1
- package/dist/esm/charts/sunburst/sunburst-render.d.ts +4 -0
- package/dist/esm/perspective-viewer-charts.js +2 -2
- package/dist/esm/perspective-viewer-charts.js.map +4 -4
- package/dist/esm/plugin/charts.d.ts +24 -0
- package/dist/esm/plugin/plugin.d.ts +8 -1
- package/dist/esm/theme/gradient.d.ts +31 -0
- package/dist/esm/theme/overrides.d.ts +19 -0
- package/dist/esm/transport/protocol.d.ts +13 -2
- package/dist/esm/utils/css.d.ts +4 -0
- package/dist/esm/worker/renderer.worker.d.ts +27 -0
- package/package.json +1 -1
- package/src/ts/charts/candlestick/candlestick-build.ts +1 -0
- package/src/ts/charts/candlestick/candlestick-render.ts +8 -0
- package/src/ts/charts/candlestick/candlestick.ts +4 -3
- package/src/ts/charts/cartesian/cartesian-render.ts +5 -0
- package/src/ts/charts/cartesian/cartesian.ts +4 -0
- package/src/ts/charts/chart-base.ts +42 -10
- package/src/ts/charts/chart.ts +5 -0
- package/src/ts/charts/common/category-axis-resolver.ts +44 -5
- package/src/ts/charts/common/tree-chart.ts +4 -0
- package/src/ts/charts/heatmap/heatmap-build.ts +7 -1
- package/src/ts/charts/heatmap/heatmap-interact.ts +10 -1
- package/src/ts/charts/heatmap/heatmap-render.ts +5 -0
- package/src/ts/charts/heatmap/heatmap.ts +8 -3
- package/src/ts/charts/series/series-build.ts +1 -0
- package/src/ts/charts/series/series-interact.ts +2 -13
- package/src/ts/charts/series/series-render.ts +5 -0
- package/src/ts/charts/series/series-type.ts +65 -0
- package/src/ts/charts/series/series.ts +21 -8
- package/src/ts/charts/sunburst/sunburst-render.ts +9 -0
- package/src/ts/charts/treemap/treemap-render.ts +5 -0
- package/src/ts/layout/ticks.ts +36 -12
- package/src/ts/plugin/charts.ts +69 -3
- package/src/ts/plugin/plugin.ts +99 -10
- package/src/ts/theme/gradient.ts +241 -1
- package/src/ts/theme/overrides.ts +76 -0
- package/src/ts/transport/protocol.ts +14 -2
- package/src/ts/transport/renderer-transport.ts +24 -10
- package/src/ts/utils/css.ts +12 -0
- package/src/ts/worker/renderer.worker.ts +113 -5
package/src/ts/theme/gradient.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
|
-
import { parseCSSColorToVec3 } from "../utils/css";
|
|
13
|
+
import { parseCSSColorToVec3, vec3ToHexColor } from "../utils/css";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* A single stop on a parsed CSS gradient. `offset` ∈ [0, 1].
|
|
@@ -178,6 +178,246 @@ export function parseCssGradient(
|
|
|
178
178
|
return result;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* A color literal — `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb()` or
|
|
183
|
+
* `rgba()` — as a `[0, 1]` RGB triple, or `null` for anything else.
|
|
184
|
+
*/
|
|
185
|
+
export function parseCssColorStrict(
|
|
186
|
+
src: string,
|
|
187
|
+
): [number, number, number] | null {
|
|
188
|
+
const s = src.trim();
|
|
189
|
+
if (s.startsWith("#")) {
|
|
190
|
+
const hex = s.slice(1);
|
|
191
|
+
if (!/^[0-9a-f]+$/i.test(hex)) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
196
|
+
return [
|
|
197
|
+
parseInt(hex[0] + hex[0], 16) / 255,
|
|
198
|
+
parseInt(hex[1] + hex[1], 16) / 255,
|
|
199
|
+
parseInt(hex[2] + hex[2], 16) / 255,
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (hex.length === 6 || hex.length === 8) {
|
|
204
|
+
return [
|
|
205
|
+
parseInt(hex.slice(0, 2), 16) / 255,
|
|
206
|
+
parseInt(hex.slice(2, 4), 16) / 255,
|
|
207
|
+
parseInt(hex.slice(4, 6), 16) / 255,
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const m = s.match(/^rgba?\(([^)]*)\)$/i);
|
|
215
|
+
if (!m) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const tokens = m[1]
|
|
220
|
+
.split("/")[0]
|
|
221
|
+
.split(/[\s,]+/)
|
|
222
|
+
.filter((x) => x.length > 0);
|
|
223
|
+
if (tokens.length < 3 || tokens.length > 4) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const channel = (token: string): number | null => {
|
|
228
|
+
const pct = token.endsWith("%");
|
|
229
|
+
const n = parseFloat(pct ? token.slice(0, -1) : token);
|
|
230
|
+
if (!isFinite(n)) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const v = pct ? (n / 100) * 255 : n;
|
|
235
|
+
return Math.max(0, Math.min(255, Math.round(v))) / 255;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const r = channel(tokens[0]);
|
|
239
|
+
const g = channel(tokens[1]);
|
|
240
|
+
const b = channel(tokens[2]);
|
|
241
|
+
return r === null || g === null || b === null ? null : [r, g, b];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The shared `linear-gradient(...)` tokenizer of the viewer's CSS-valued
|
|
246
|
+
* config grammar.
|
|
247
|
+
*/
|
|
248
|
+
export function tokenizeLinearGradient(
|
|
249
|
+
src: string,
|
|
250
|
+
): Array<[[number, number, number], number | null]> | null {
|
|
251
|
+
const m = src.trim().match(/^linear-gradient\s*\((.*)\)$/is);
|
|
252
|
+
if (!m) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const body = m[1];
|
|
257
|
+
const parts: string[] = [];
|
|
258
|
+
let depth = 0;
|
|
259
|
+
let start = 0;
|
|
260
|
+
for (let i = 0; i < body.length; i++) {
|
|
261
|
+
const ch = body[i];
|
|
262
|
+
if (ch === "(") {
|
|
263
|
+
depth++;
|
|
264
|
+
} else if (ch === ")") {
|
|
265
|
+
depth--;
|
|
266
|
+
} else if (ch === "," && depth === 0) {
|
|
267
|
+
parts.push(body.substring(start, i));
|
|
268
|
+
start = i + 1;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
parts.push(body.substring(start));
|
|
273
|
+
const out: Array<[[number, number, number], number | null]> = [];
|
|
274
|
+
for (let i = 0; i < parts.length; i++) {
|
|
275
|
+
const piece = parts[i].trim();
|
|
276
|
+
if (!piece) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const lower = piece.toLowerCase();
|
|
281
|
+
if (
|
|
282
|
+
i === 0 &&
|
|
283
|
+
(lower.startsWith("to ") ||
|
|
284
|
+
/^[-\d.]+(deg|rad|grad|turn)$/.test(lower))
|
|
285
|
+
) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const posMatch = piece.match(/\s([^\s)]+)$/);
|
|
290
|
+
let color = piece;
|
|
291
|
+
let offset: number | null = null;
|
|
292
|
+
if (posMatch) {
|
|
293
|
+
const tail = posMatch[1];
|
|
294
|
+
if (tail.endsWith("%")) {
|
|
295
|
+
const n = parseFloat(tail.slice(0, -1));
|
|
296
|
+
if (!isFinite(n)) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
color = piece.substring(0, posMatch.index).trim();
|
|
301
|
+
offset = n / 100;
|
|
302
|
+
} else if (/^[-\d.]/.test(tail)) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const rgb = parseCssColorStrict(color);
|
|
308
|
+
if (!rgb) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
out.push([rgb, offset]);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Strict gradient reader: a `linear-gradient(...)` with ≥ 2 stops,
|
|
320
|
+
* positions optional, offsets clamped to `[0, 1]` and sorted, or `null`
|
|
321
|
+
* on malformed input.
|
|
322
|
+
*/
|
|
323
|
+
export function parseCssGradientStrict(src: string): GradientStop[] | null {
|
|
324
|
+
const entries = tokenizeLinearGradient(src);
|
|
325
|
+
if (!entries || entries.length < 2) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const offsets: Array<number | null> = entries.map(([, p]) => p);
|
|
330
|
+
const last = offsets.length - 1;
|
|
331
|
+
if (offsets[0] === null) {
|
|
332
|
+
offsets[0] = 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (offsets[last] === null) {
|
|
336
|
+
offsets[last] = 1;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for (let i = 1; i < last; i++) {
|
|
340
|
+
if (offsets[i] !== null) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let j = i + 1;
|
|
345
|
+
while (offsets[j] === null) {
|
|
346
|
+
j++;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const before = offsets[i - 1]!;
|
|
350
|
+
const after = offsets[j]!;
|
|
351
|
+
const span = j - (i - 1);
|
|
352
|
+
for (let k = i; k < j; k++) {
|
|
353
|
+
offsets[k] = before + ((k - (i - 1)) / span) * (after - before);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
i = j - 1;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const stops: GradientStop[] = entries.map(([rgb], i) => ({
|
|
360
|
+
offset: Math.max(0, Math.min(1, offsets[i]!)),
|
|
361
|
+
color: [rgb[0], rgb[1], rgb[2], 1],
|
|
362
|
+
}));
|
|
363
|
+
|
|
364
|
+
stops.sort((a, b) => a.offset - b.offset);
|
|
365
|
+
return stops;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Strict palette reader: a `linear-gradient(...)` of ≥ 1 colors with no
|
|
370
|
+
* positions, or `null` on malformed input.
|
|
371
|
+
*/
|
|
372
|
+
export function parseCssColorList(
|
|
373
|
+
src: string,
|
|
374
|
+
): Array<[number, number, number]> | null {
|
|
375
|
+
const entries = tokenizeLinearGradient(src);
|
|
376
|
+
if (!entries || entries.length === 0) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const out: Array<[number, number, number]> = [];
|
|
381
|
+
for (const [rgb, offset] of entries) {
|
|
382
|
+
if (offset !== null) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
out.push(rgb);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function formatPercent(offset: number): string {
|
|
393
|
+
const rounded = Math.round(Math.max(0, Math.min(1, offset)) * 1000) / 10;
|
|
394
|
+
return `${rounded}%`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* The viewer's canonical gradient string for `stops`: `linear-gradient(to
|
|
399
|
+
* right, #rrggbb P%, …)`, sorted, positions at 0.1% resolution.
|
|
400
|
+
*/
|
|
401
|
+
export function stopsToCss(stops: GradientStop[]): string {
|
|
402
|
+
const body = [...stops]
|
|
403
|
+
.sort((a, b) => a.offset - b.offset)
|
|
404
|
+
.map(
|
|
405
|
+
(stop) =>
|
|
406
|
+
`${vec3ToHexColor([stop.color[0], stop.color[1], stop.color[2]])} ${formatPercent(stop.offset)}`,
|
|
407
|
+
)
|
|
408
|
+
.join(", ");
|
|
409
|
+
|
|
410
|
+
return `linear-gradient(to right, ${body})`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* The viewer's canonical palette string for `colors`: the same frame
|
|
415
|
+
* with bare `#rrggbb` entries and no positions.
|
|
416
|
+
*/
|
|
417
|
+
export function colorsToCss(colors: Array<[number, number, number]>): string {
|
|
418
|
+
return `linear-gradient(to right, ${colors.map(vec3ToHexColor).join(", ")})`;
|
|
419
|
+
}
|
|
420
|
+
|
|
181
421
|
/**
|
|
182
422
|
* Piecewise-linear color sample at `t ∈ [0, 1]`. Returns RGBA in [0, 1].
|
|
183
423
|
* Clamps `t` to the gradient's first/last stop outside `[0, 1]`.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
parseCssColorList,
|
|
15
|
+
parseCssGradientStrict,
|
|
16
|
+
type GradientStop,
|
|
17
|
+
} from "./gradient";
|
|
18
|
+
import type { Theme } from "./theme";
|
|
19
|
+
import type { Vec3 } from "./palette";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Per-column color-scale overrides from the viewer's `columns_config`,
|
|
23
|
+
* patched into the resolved `Theme`'s `seriesPalette` / `gradientStops`
|
|
24
|
+
* fields.
|
|
25
|
+
*/
|
|
26
|
+
export function applyColumnColorOverrides(
|
|
27
|
+
theme: Theme,
|
|
28
|
+
columnsConfig: Record<string, any> | undefined,
|
|
29
|
+
colorColumn: string | null,
|
|
30
|
+
): Theme {
|
|
31
|
+
if (!colorColumn) {
|
|
32
|
+
return theme;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const cfg = columnsConfig?.[colorColumn];
|
|
36
|
+
if (!cfg) {
|
|
37
|
+
return theme;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let out = theme;
|
|
41
|
+
const gradient = parseGradientOverride(cfg.gradient);
|
|
42
|
+
if (gradient) {
|
|
43
|
+
out = { ...out, gradientStops: gradient };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const palette = parsePaletteOverride(cfg.palette);
|
|
47
|
+
if (palette) {
|
|
48
|
+
out = { ...out, seriesPalette: palette };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse a stored `palette` value into the `seriesPalette` shape, or
|
|
56
|
+
* `null` on anything malformed.
|
|
57
|
+
*/
|
|
58
|
+
export function parsePaletteOverride(raw: unknown): Vec3[] | null {
|
|
59
|
+
if (typeof raw !== "string") {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return parseCssColorList(raw);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse a stored `gradient` value into sorted `GradientStop[]`, or
|
|
68
|
+
* `null` on malformed input or fewer than 2 stops.
|
|
69
|
+
*/
|
|
70
|
+
export function parseGradientOverride(raw: unknown): GradientStop[] | null {
|
|
71
|
+
if (typeof raw !== "string") {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return parseCssGradientStrict(raw);
|
|
76
|
+
}
|
|
@@ -100,8 +100,20 @@ export interface InitMsg {
|
|
|
100
100
|
* `dpr` and there is no host-side GL drawing buffer.
|
|
101
101
|
*/
|
|
102
102
|
glCanvas?: OffscreenCanvas;
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The gridlines (bottom) and chrome/axes (top) 2D layers.
|
|
106
|
+
* Transferred via `transferControlToOffscreen` on the host, present
|
|
107
|
+
* iff `renderMode === "direct"` — their pixels reach the screen
|
|
108
|
+
* through the compositor. In blit mode the renderer allocates
|
|
109
|
+
* worker-local surfaces instead and composites both layers into the
|
|
110
|
+
* shipped `FrameBitmapMsg`, so every layer of the frame rides the
|
|
111
|
+
* host's staged-present hold (a transferred canvas presents on the
|
|
112
|
+
* compositor's own schedule, unsynchronized with the host's layout
|
|
113
|
+
* commits — the one-frame axes warp on anticipated resizes).
|
|
114
|
+
*/
|
|
115
|
+
gridlinesCanvas?: OffscreenCanvas;
|
|
116
|
+
chromeCanvas?: OffscreenCanvas;
|
|
105
117
|
|
|
106
118
|
/**
|
|
107
119
|
* `MessagePort` to the host's `ProxySession`. Worker mode only —
|
|
@@ -299,15 +299,24 @@ export class RendererTransport {
|
|
|
299
299
|
// `ImageBitmap`. Direct mode transfers the visible canvas's
|
|
300
300
|
// drawing buffer to the renderer so GL paints straight to
|
|
301
301
|
// screen.
|
|
302
|
+
// Blit mode also keeps the gridlines/chrome layers worker-side:
|
|
303
|
+
// a transferred canvas presents on the compositor's schedule,
|
|
304
|
+
// unsynchronized with the host's layout commits, so it can't
|
|
305
|
+
// participate in the staged-present hold. The renderer
|
|
306
|
+
// composites both layers into the shipped frame instead, and
|
|
307
|
+
// the host-side placeholder canvases stay untouched
|
|
308
|
+
// (transparent). Direct mode transfers all three surfaces.
|
|
302
309
|
let glOC: OffscreenCanvas | undefined;
|
|
310
|
+
let gridlinesOC: OffscreenCanvas | undefined;
|
|
311
|
+
let chromeOC: OffscreenCanvas | undefined;
|
|
303
312
|
if (opts.renderBlitMode === "blit") {
|
|
304
313
|
this._displayCtx = opts.gl.getContext("2d");
|
|
305
314
|
} else {
|
|
306
315
|
glOC = opts.gl.transferControlToOffscreen();
|
|
316
|
+
gridlinesOC = opts.gridlines.transferControlToOffscreen();
|
|
317
|
+
chromeOC = opts.chrome.transferControlToOffscreen();
|
|
307
318
|
}
|
|
308
319
|
|
|
309
|
-
const gridlinesOC = opts.gridlines.transferControlToOffscreen();
|
|
310
|
-
const chromeOC = opts.chrome.transferControlToOffscreen();
|
|
311
320
|
const rect = opts.gl.getBoundingClientRect();
|
|
312
321
|
const dpr = window.devicePixelRatio || 1;
|
|
313
322
|
const themeVars = snapshotThemeVars(opts.gl);
|
|
@@ -365,14 +374,19 @@ export class RendererTransport {
|
|
|
365
374
|
// Worker mode: the bootstrap is triggered by posting the
|
|
366
375
|
// init message into the worker's scope (which the
|
|
367
376
|
// `if (IS_WORKER_SCOPE)` block in `renderer.worker.ts`
|
|
368
|
-
// listens for).
|
|
369
|
-
// renderer allocates its own
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
377
|
+
// listens for). All three canvases are omitted in blit mode
|
|
378
|
+
// (the renderer allocates its own offscreens and ships
|
|
379
|
+
// composited frames) — only include the transferred
|
|
380
|
+
// surfaces present on this mode's init message.
|
|
381
|
+
const transfer: Transferable[] = [this._proxyChannel!.port2];
|
|
382
|
+
if (chromeOC) {
|
|
383
|
+
transfer.unshift(chromeOC);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (gridlinesOC) {
|
|
387
|
+
transfer.unshift(gridlinesOC);
|
|
388
|
+
}
|
|
389
|
+
|
|
376
390
|
if (glOC) {
|
|
377
391
|
transfer.unshift(glOC);
|
|
378
392
|
}
|
package/src/ts/utils/css.ts
CHANGED
|
@@ -34,3 +34,15 @@ export function parseCSSColorToVec3(
|
|
|
34
34
|
|
|
35
35
|
return [0.5, 0.5, 0.5];
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Format a `[0, 1]`-range RGB triple as a plain `#rrggbb` string.
|
|
40
|
+
*/
|
|
41
|
+
export function vec3ToHexColor(c: [number, number, number]): string {
|
|
42
|
+
const channel = (x: number) =>
|
|
43
|
+
Math.round(Math.max(0, Math.min(1, x)) * 255)
|
|
44
|
+
.toString(16)
|
|
45
|
+
.padStart(2, "0");
|
|
46
|
+
|
|
47
|
+
return `#${channel(c[0])}${channel(c[1])}${channel(c[2])}`;
|
|
48
|
+
}
|
|
@@ -106,6 +106,20 @@ export class WorkerRenderer {
|
|
|
106
106
|
cssWidth: number;
|
|
107
107
|
cssHeight: number;
|
|
108
108
|
dpr: number;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Blit-mode compose surface: gridlines + GL frame + chrome are
|
|
112
|
+
* drawn here each `endFrame` and shipped as ONE `ImageBitmap`, so
|
|
113
|
+
* every layer rides the host's staged-present hold (see
|
|
114
|
+
* `InitMsg.gridlinesCanvas`). `null` in direct mode.
|
|
115
|
+
*/
|
|
116
|
+
private _composeCanvas: OffscreenCanvas | null = null;
|
|
117
|
+
private _composeCtx: OffscreenCanvasRenderingContext2D | null = null;
|
|
118
|
+
|
|
119
|
+
private _lastPlot: ImageBitmap | null = null;
|
|
120
|
+
|
|
121
|
+
private _frameSerial = 0;
|
|
122
|
+
private _overlayPresentQueued = false;
|
|
109
123
|
client: Client;
|
|
110
124
|
view: View;
|
|
111
125
|
|
|
@@ -182,20 +196,31 @@ export class WorkerRenderer {
|
|
|
182
196
|
});
|
|
183
197
|
}
|
|
184
198
|
|
|
199
|
+
// Blit mode: the 2D layers are worker-local (the host omits
|
|
200
|
+
// them from the init message) and every shipped frame is the
|
|
201
|
+
// full composite — gridlines under the GL plot under chrome,
|
|
202
|
+
// the same stacking as the host's canvas elements and the
|
|
203
|
+
// `snapshotPng` composite. Direct mode draws into the host's
|
|
204
|
+
// transferred surfaces and ships nothing.
|
|
205
|
+
const w = Math.max(1, Math.round(msg.cssWidth * msg.dpr));
|
|
206
|
+
const h = Math.max(1, Math.round(msg.cssHeight * msg.dpr));
|
|
207
|
+
this.gridlines = msg.gridlinesCanvas ?? new OffscreenCanvas(w, h);
|
|
208
|
+
this.chrome = msg.chromeCanvas ?? new OffscreenCanvas(w, h);
|
|
185
209
|
if (msg.renderMode === "blit") {
|
|
186
210
|
this.glManager.setFrameCallback((bitmap) => {
|
|
187
|
-
this.
|
|
211
|
+
this._frameSerial += 1;
|
|
212
|
+
const frame = this._composeFrame(bitmap);
|
|
213
|
+
this.post({ kind: "frameBitmap", bitmap: frame }, [frame]);
|
|
188
214
|
});
|
|
215
|
+
this.chartImpl.setOverlayPresenter?.(() => this._presentOverlay());
|
|
189
216
|
}
|
|
190
217
|
|
|
191
|
-
this.gridlines = msg.gridlinesCanvas;
|
|
192
|
-
this.chrome = msg.chromeCanvas;
|
|
193
218
|
this.cssWidth = msg.cssWidth;
|
|
194
219
|
this.cssHeight = msg.cssHeight;
|
|
195
220
|
this.dpr = msg.dpr;
|
|
196
221
|
|
|
197
|
-
this.chartImpl.setGridlineCanvas?.(
|
|
198
|
-
this.chartImpl.setChromeCanvas?.(
|
|
222
|
+
this.chartImpl.setGridlineCanvas?.(this.gridlines);
|
|
223
|
+
this.chartImpl.setChromeCanvas?.(this.chrome);
|
|
199
224
|
this.chartImpl.setTheme?.(msg.themeVars);
|
|
200
225
|
|
|
201
226
|
if (msg.defaultChartType) {
|
|
@@ -588,6 +613,87 @@ export class WorkerRenderer {
|
|
|
588
613
|
return tt ?? null;
|
|
589
614
|
}
|
|
590
615
|
|
|
616
|
+
/**
|
|
617
|
+
* Blit-mode frame composite: draw the gridlines (bottom), the GL
|
|
618
|
+
* plot bitmap (middle) and the chrome/axes (top) into the compose
|
|
619
|
+
* surface and transfer the result — the single `ImageBitmap` the
|
|
620
|
+
* host blits, and the unit the staged-present hold stages. Runs in
|
|
621
|
+
* `endFrame`, which the scheduler orders after `render2D`'s 2D
|
|
622
|
+
* flush, so both layers hold this frame's content.
|
|
623
|
+
*
|
|
624
|
+
* A layer whose buffer differs from the plot's dimensions is
|
|
625
|
+
* skipped rather than scaled: its content is a stale-sized frame
|
|
626
|
+
* mid-resize (the 2D draws re-size their canvas to `css × dpr` on
|
|
627
|
+
* the next flush), and scaling it would re-introduce exactly the
|
|
628
|
+
* warp this composite exists to prevent.
|
|
629
|
+
*/
|
|
630
|
+
private _composeFrame(plot: ImageBitmap): ImageBitmap {
|
|
631
|
+
const w = plot.width;
|
|
632
|
+
const h = plot.height;
|
|
633
|
+
if (!this._composeCanvas) {
|
|
634
|
+
this._composeCanvas = new OffscreenCanvas(w, h);
|
|
635
|
+
this._composeCtx = this._composeCanvas.getContext("2d");
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const canvas = this._composeCanvas;
|
|
639
|
+
const ctx = this._composeCtx;
|
|
640
|
+
if (!ctx) {
|
|
641
|
+
return plot;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (canvas.width !== w || canvas.height !== h) {
|
|
645
|
+
canvas.width = w;
|
|
646
|
+
canvas.height = h;
|
|
647
|
+
} else {
|
|
648
|
+
ctx.clearRect(0, 0, w, h);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (this.gridlines.width === w && this.gridlines.height === h) {
|
|
652
|
+
ctx.drawImage(this.gridlines, 0, 0);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
ctx.drawImage(plot, 0, 0);
|
|
656
|
+
if (this._lastPlot !== plot) {
|
|
657
|
+
this._lastPlot?.close();
|
|
658
|
+
this._lastPlot = plot;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (this.chrome.width === w && this.chrome.height === h) {
|
|
662
|
+
ctx.drawImage(this.chrome, 0, 0);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return canvas.transferToImageBitmap();
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
private _presentOverlay(): void {
|
|
669
|
+
if (this._overlayPresentQueued) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
this._overlayPresentQueued = true;
|
|
674
|
+
const mark = this._frameSerial;
|
|
675
|
+
queueMicrotask(() => {
|
|
676
|
+
this._overlayPresentQueued = false;
|
|
677
|
+
const plot = this._lastPlot;
|
|
678
|
+
if (this._frameSerial !== mark || !plot) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const w = Math.max(1, Math.round(this.cssWidth * this.dpr));
|
|
683
|
+
const h = Math.max(1, Math.round(this.cssHeight * this.dpr));
|
|
684
|
+
if (plot.width !== w || plot.height !== h) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const frame = this._composeFrame(plot);
|
|
689
|
+
if (frame === plot) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
this.post({ kind: "frameBitmap", bitmap: frame }, [frame]);
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
591
697
|
/**
|
|
592
698
|
* Composite the three layers into a single PNG `Blob`.
|
|
593
699
|
*/
|
|
@@ -649,6 +755,8 @@ export class WorkerRenderer {
|
|
|
649
755
|
// next-RAF `drain()` can't paint/present against a dead
|
|
650
756
|
// context (the "scheduler: present failed" path).
|
|
651
757
|
unregister(this.glManager);
|
|
758
|
+
this._lastPlot?.close();
|
|
759
|
+
this._lastPlot = null;
|
|
652
760
|
this.chartImpl.destroy();
|
|
653
761
|
this.glManager.destroy();
|
|
654
762
|
}
|