@zumer/snapdom-plugins 4.0.2 → 4.2.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/CHANGELOG.md +16 -0
- package/README.md +39 -23
- package/ascii-export.d.ts +10 -0
- package/ascii-export.js +70 -27
- package/capture-frames.js +26 -14
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +8 -4
- package/pdf/README.md +2 -2
- package/pdf/src/mega/README.md +1 -1
- package/stream/LICENSE +21 -0
- package/stream/README.md +138 -0
- package/stream/src/index.js +41 -0
- package/stream/src/overlays.js +153 -0
- package/stream/src/recorder.js +41 -0
- package/stream/src/stream.js +484 -0
- package/stream/src/webgl.js +58 -0
- package/stream.d.ts +75 -0
- package/stream.js +1 -0
- package/vector/CONTRACT.md +7 -10
- package/vector/README.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Plugin changes
|
|
2
2
|
|
|
3
|
+
## 4.2.0
|
|
4
|
+
|
|
5
|
+
- Export asciiFromCanvas(source, options) from @zumer/snapdom-plugins/ascii-export: the ASCII conversion on its own, for a canvas, an image, a video frame or a stream frame. toAscii() keeps its output.
|
|
6
|
+
- Add asciiStream(element, options): stream() plus asciiFromCanvas() on every frame, with onText(text, frame).
|
|
7
|
+
|
|
8
|
+
Requires SnapDOM 3.4.0 or newer within 3.x.
|
|
9
|
+
|
|
10
|
+
## 4.1.0
|
|
11
|
+
|
|
12
|
+
- Add the stream plugin at @zumer/snapdom-plugins/stream: a live canvas that mirrors an element and repaints only the boxes that changed, for WebGL textures, video overlays and WebRTC tracks.
|
|
13
|
+
- Add streamTexture(gl), which uploads only the changed rectangles to a WebGL2 texture, and recordCanvas(canvas) to record a stream.
|
|
14
|
+
- Add streamExport(), which gives a capture result a toStream() method next to toMp4() and toGif().
|
|
15
|
+
- Add broadcast overlay templates (overlayStage, scoreboard, lowerThird, ticker and liveBadge), themed through CSS variables and built so that a score, a clock tick or a slide repaints as a patch.
|
|
16
|
+
|
|
17
|
+
Requires SnapDOM 3.4.0 or newer within 3.x.
|
|
18
|
+
|
|
3
19
|
## 4.0.1
|
|
4
20
|
|
|
5
21
|
- Add GitHub Sponsors funding metadata.
|
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# @zumer/snapdom-plugins
|
|
2
2
|
|
|
3
|
-
Official plugins for [SnapDOM](https://github.com/zumerlab/snapdom), a
|
|
4
|
-
|
|
5
|
-
element maps, PDF, editable vectors, ASCII,
|
|
3
|
+
Official plugins for [SnapDOM](https://github.com/zumerlab/snapdom), a high-performance DOM engine
|
|
4
|
+
that exports a rendered element as PNG, SVG or canvas in the browser. Core exports images and
|
|
5
|
+
canvases; this package adds HTML, structured context, element maps, PDF, editable vectors, ASCII,
|
|
6
|
+
visual transforms, live GIF/video recording and live DOM streaming.
|
|
6
7
|
|
|
7
8
|
Image, HTML and structured exports use captured state; GIF and video start a sequence of
|
|
8
9
|
live captures when their export method is called.
|
|
@@ -15,21 +16,7 @@ Install the plugins next to a core supported by their peer dependency:
|
|
|
15
16
|
npm install @zumer/snapdom@latest @zumer/snapdom-plugins@latest
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
Plugins 4.x in this checkout require SnapDOM 3.3.
|
|
19
|
-
|
|
20
|
-
## Releasing from the repository root
|
|
21
|
-
|
|
22
|
-
```sh
|
|
23
|
-
npm run bump:plugins:dry
|
|
24
|
-
npm run bump:plugins
|
|
25
|
-
npm run release:plugins
|
|
26
|
-
npm run publish:plugins -- --dry-run
|
|
27
|
-
npm run publish:plugins
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
`bump:plugins` runs `@zumerbox/bump` followed by `@zumerbox/changelog` inside the plugin workspace, matching the core's tools. The bump command requires a clean Git working tree and prompts for the next version. It updates the plugin version and root lockfile; the changelog command writes `packages/plugins/CHANGELOG.md`. `bump:plugins:dry` previews the bump without updating the version or changelog. Skip the bump if the version has already been prepared.
|
|
31
|
-
|
|
32
|
-
`release:plugins` runs the full cross-browser suite with required visual baselines, the PDF and Vector checks, and consumer packaging checks, then creates the plugin tarball. It does not publish. `publish:plugins` publishes only `@zumer/snapdom-plugins` under the `latest` tag.
|
|
19
|
+
Plugins 4.x in this checkout require SnapDOM 3.3.8 or newer within 3.x. The 4.x release replaces pdf-image with the full PDF exporter and adds Vector. Both are MIT-licensed and available on npm in `@zumer/snapdom-plugins` 4.x. The `@latest` command above installs the published release. See the [migration guide](https://github.com/zumerlab/snapdom#migrating-from-v2) when upgrading from v2.
|
|
33
20
|
|
|
34
21
|
## Usage
|
|
35
22
|
|
|
@@ -243,7 +230,7 @@ console.log(art);
|
|
|
243
230
|
### `pdf`
|
|
244
231
|
|
|
245
232
|
Adds `toPdf()`, returning a PDF Blob with the capture's appearance, selectable text,
|
|
246
|
-
links and pagination.
|
|
233
|
+
links and pagination. MIT licensed.
|
|
247
234
|
It replaces `pdf-image`; there is no second image-only PDF implementation.
|
|
248
235
|
|
|
249
236
|
```js
|
|
@@ -503,6 +490,23 @@ environments. Per-call `opts` override constructor options and accept `download`
|
|
|
503
490
|
(`boolean \| string`). Recording starts at the call and preserves capture options as GIF
|
|
504
491
|
recording does. MediaRecorder uses a real-time clock, so slow captures can stretch duration.
|
|
505
492
|
|
|
493
|
+
### `stream`
|
|
494
|
+
|
|
495
|
+
Streams a live element into a canvas that repaints only the boxes that changed, for WebGL
|
|
496
|
+
textures and video overlays. `stream(el, options)` is the entry; `streamExport()` adds the same
|
|
497
|
+
as `toStream()`. Includes `streamTexture(gl)`, which uploads only the changed rects, and
|
|
498
|
+
`recordCanvas(canvas)`.
|
|
499
|
+
|
|
500
|
+
```js
|
|
501
|
+
import { stream, streamTexture } from '@zumer/snapdom-plugins/stream';
|
|
502
|
+
|
|
503
|
+
const tex = streamTexture(gl);
|
|
504
|
+
const live = stream(panel, { scale: 2, onFrame: (frame) => tex.update(frame) });
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Each patched frame matches a full capture of the same DOM state. Full documentation, options
|
|
508
|
+
and limits: [stream/README.md](stream/README.md).
|
|
509
|
+
|
|
506
510
|
---
|
|
507
511
|
|
|
508
512
|
## Plugin registration
|
|
@@ -541,10 +545,22 @@ See [PLUGIN_SPEC.md](../../PLUGIN_SPEC.md) for the full hook specification and [
|
|
|
541
545
|
|
|
542
546
|
---
|
|
543
547
|
|
|
544
|
-
##
|
|
548
|
+
## Live demos
|
|
545
549
|
|
|
546
|
-
|
|
550
|
+
Run `npm run compile` and `npm run site` from the monorepo root, then open `/showcase/`, `/plugins/pdf/` or `/plugins/vector/`. The demos generate actual files from editable DOM samples.
|
|
547
551
|
|
|
548
|
-
##
|
|
552
|
+
## Releasing from the repository root
|
|
549
553
|
|
|
550
|
-
|
|
554
|
+
```sh
|
|
555
|
+
npm run bump:plugins:dry
|
|
556
|
+
npm run bump:plugins
|
|
557
|
+
# review packages/plugins/CHANGELOG.md
|
|
558
|
+
npm run release:plugins
|
|
559
|
+
npm run publish:plugins
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
`release:plugins` commits the plugin version, the lockfile and the changelog, then pushes. `publish:plugins` runs the PDF and vector checks, then publishes only `@zumer/snapdom-plugins` under `latest`. `bump:plugins` runs `@zumerbox/bump` followed by `@zumerbox/changelog` inside the plugin workspace, matching the core's tools. The bump command requires a clean Git working tree and prompts for the next version. It updates the plugin version and root lockfile; the changelog command writes `packages/plugins/CHANGELOG.md`. `bump:plugins:dry` previews the bump without updating the version or changelog. Skip the bump if the version has already been prepared.
|
|
563
|
+
|
|
564
|
+
## License
|
|
565
|
+
|
|
566
|
+
MIT
|
package/ascii-export.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* at capture time.
|
|
7
7
|
*/
|
|
8
8
|
import type { SnapdomPlugin } from '@zumer/snapdom';
|
|
9
|
+
import type { Stream, StreamOptions, StreamFrame } from './stream.js';
|
|
9
10
|
|
|
10
11
|
export interface AsciiExportOptions {
|
|
11
12
|
/** Character width of the output. */
|
|
@@ -23,8 +24,17 @@ export interface AsciiExportOptions {
|
|
|
23
24
|
/** Adds `result.toAscii()`. */
|
|
24
25
|
export declare function asciiExport(options?: AsciiExportOptions): SnapdomPlugin;
|
|
25
26
|
|
|
27
|
+
/** ASCII art from any drawable (a canvas, an image, a video frame, a `stream()` frame). No capture involved. */
|
|
28
|
+
export declare function asciiFromCanvas(source: CanvasImageSource & { width: number; height: number }, options?: AsciiExportOptions & { charset?: string | string[] }): string;
|
|
29
|
+
|
|
26
30
|
declare module '@zumer/snapdom' {
|
|
27
31
|
interface CaptureResult {
|
|
28
32
|
toAscii(options?: AsciiExportOptions): Promise<string>;
|
|
29
33
|
}
|
|
30
34
|
}
|
|
35
|
+
|
|
36
|
+
/** `stream()` plus `asciiFromCanvas()` on every frame; `onText` receives the text (or HTML when `format: 'html'`). */
|
|
37
|
+
export declare function asciiStream(
|
|
38
|
+
element: Element,
|
|
39
|
+
options?: AsciiExportOptions & { charset?: string | string[] } & StreamOptions & { onText?: (text: string, frame: StreamFrame) => void }
|
|
40
|
+
): Stream;
|
package/ascii-export.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* @param {number} [options.contrast=0] - Non-negative luminance contrast boost
|
|
13
13
|
* @returns {Object} SnapDOM plugin
|
|
14
14
|
*/
|
|
15
|
+
import { stream } from './stream.js';
|
|
16
|
+
|
|
15
17
|
export function asciiExport(options = {}) {
|
|
16
18
|
const {
|
|
17
19
|
width: charWidth = 80,
|
|
@@ -46,35 +48,76 @@ export function asciiExport(options = {}) {
|
|
|
46
48
|
...opts, width: ctx.width, height: ctx.height, scale: 1, dpr: 1, canvas: null,
|
|
47
49
|
backgroundColor: !opts.backgroundColor || opts.backgroundColor === 'transparent' ? '#ffffff' : opts.backgroundColor,
|
|
48
50
|
});
|
|
49
|
-
|
|
50
|
-
const canvas = document.createElement('canvas');
|
|
51
|
-
canvas.width = cols;
|
|
52
|
-
canvas.height = rows;
|
|
53
|
-
const c = canvas.getContext('2d');
|
|
54
|
-
// Squash the bitmap, not the SVG viewport: Firefox preserves its viewBox aspect ratio.
|
|
55
|
-
c.drawImage(source, 0, 0, cols, rows);
|
|
56
|
-
const data = c.getImageData(0, 0, cols, rows).data;
|
|
57
|
-
|
|
58
|
-
const colored = outputFormat === 'html';
|
|
59
|
-
let ascii = colored ? '<pre style="margin:0;font:7px/1 monospace;white-space:pre;overflow-wrap:normal;max-width:100%;overflow:auto;background:white">' : '';
|
|
60
|
-
for (let y = 0; y < rows; y++) {
|
|
61
|
-
for (let x = 0; x < cols; x++) {
|
|
62
|
-
const i = (y * cols + x) * 4;
|
|
63
|
-
let lum = 1 - (data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114) / 255;
|
|
64
|
-
lum = Math.max(0, Math.min(1, (1 + gain) * (lum - 0.5) + 0.5));
|
|
65
|
-
if (inv) lum = 1 - lum;
|
|
66
|
-
const ci = Math.min(Math.floor(lum * chars.length), chars.length - 1);
|
|
67
|
-
const char = chars[ci];
|
|
68
|
-
if (colored) {
|
|
69
|
-
const safe = char.replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
|
70
|
-
ascii += `<span style="color:rgb(${data[i]},${data[i + 1]},${data[i + 2]})">${safe}</span>`;
|
|
71
|
-
} else ascii += char;
|
|
72
|
-
}
|
|
73
|
-
ascii += '\n';
|
|
74
|
-
}
|
|
75
|
-
return colored ? ascii + '</pre>' : ascii;
|
|
51
|
+
return asciiFromCanvas(source, { width: cols, charset: chars, invert: inv, format: outputFormat, contrast: gain });
|
|
76
52
|
}
|
|
77
53
|
};
|
|
78
54
|
}
|
|
79
55
|
};
|
|
80
56
|
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Convert any drawable (canvas, image, video frame) to ASCII art. Pure pixel work, no capture:
|
|
60
|
+
* use it on a `stream()` frame, a video element or a canvas of your own.
|
|
61
|
+
* @param {CanvasImageSource & { width: number, height: number }} source
|
|
62
|
+
* @param {Object} [options]
|
|
63
|
+
* @param {number} [options.width=80] - Character width of output
|
|
64
|
+
* @param {string|string[]} [options.charset=' .:-=+*#%@'] - Characters from lightest to darkest
|
|
65
|
+
* @param {boolean} [options.invert=false]
|
|
66
|
+
* @param {'text'|'html'} [options.format='text']
|
|
67
|
+
* @param {number} [options.contrast=0]
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
export function asciiFromCanvas(source, options = {}) {
|
|
71
|
+
const cols = Math.max(1, Math.floor(options.width ?? 80));
|
|
72
|
+
const chars = Array.isArray(options.charset) ? options.charset : Array.from(options.charset ?? ' .:-=+*#%@');
|
|
73
|
+
const inv = options.invert ?? false;
|
|
74
|
+
const colored = options.format === 'html';
|
|
75
|
+
const gain = options.contrast ?? 0;
|
|
76
|
+
const srcW = source.videoWidth || source.naturalWidth || source.width;
|
|
77
|
+
const srcH = source.videoHeight || source.naturalHeight || source.height;
|
|
78
|
+
const rows = Math.max(1, Math.round(cols * srcH / srcW * 0.5));
|
|
79
|
+
const canvas = document.createElement('canvas');
|
|
80
|
+
canvas.width = cols;
|
|
81
|
+
canvas.height = rows;
|
|
82
|
+
const c = canvas.getContext('2d', { willReadFrequently: true });
|
|
83
|
+
// Squash the bitmap, not the SVG viewport: Firefox preserves its viewBox aspect ratio.
|
|
84
|
+
c.drawImage(source, 0, 0, cols, rows);
|
|
85
|
+
const data = c.getImageData(0, 0, cols, rows).data;
|
|
86
|
+
|
|
87
|
+
let ascii = colored ? '<pre style="margin:0;font:7px/1 monospace;white-space:pre;overflow-wrap:normal;max-width:100%;overflow:auto;background:white">' : '';
|
|
88
|
+
for (let y = 0; y < rows; y++) {
|
|
89
|
+
for (let x = 0; x < cols; x++) {
|
|
90
|
+
const i = (y * cols + x) * 4;
|
|
91
|
+
let lum = 1 - (data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114) / 255;
|
|
92
|
+
lum = Math.max(0, Math.min(1, (1 + gain) * (lum - 0.5) + 0.5));
|
|
93
|
+
if (inv) lum = 1 - lum;
|
|
94
|
+
const ci = Math.min(Math.floor(lum * chars.length), chars.length - 1);
|
|
95
|
+
const char = chars[ci];
|
|
96
|
+
if (colored) {
|
|
97
|
+
const safe = char.replace(/[&<>]/g, ch => ({ '&': '&', '<': '<', '>': '>' }[ch]));
|
|
98
|
+
ascii += `<span style="color:rgb(${data[i]},${data[i + 1]},${data[i + 2]})">${safe}</span>`;
|
|
99
|
+
} else ascii += char;
|
|
100
|
+
}
|
|
101
|
+
ascii += '\n';
|
|
102
|
+
}
|
|
103
|
+
return colored ? ascii + '</pre>' : ascii;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Stream a live element as ASCII: `stream()` keeps a canvas in step with the element and every
|
|
108
|
+
* frame is converted with `asciiFromCanvas()`. Returns the stream handle (`ready`, `stats`, `stop()`).
|
|
109
|
+
* @param {Element} element
|
|
110
|
+
* @param {Object} [options] - ascii options (width, charset, invert, format, contrast) plus stream
|
|
111
|
+
* options (fps, scale, plugins, ...) and `onText(text, frame)`, called after each repaint.
|
|
112
|
+
*/
|
|
113
|
+
export function asciiStream(element, options = {}) {
|
|
114
|
+
const { width, charset, invert, format, contrast, onText, onFrame, ...streamOptions } = options;
|
|
115
|
+
const ascii = { width, charset, invert, format, contrast };
|
|
116
|
+
return stream(element, {
|
|
117
|
+
...streamOptions,
|
|
118
|
+
onFrame(frame) {
|
|
119
|
+
onFrame?.(frame);
|
|
120
|
+
onText?.(asciiFromCanvas(frame.canvas, ascii), frame);
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
package/capture-frames.js
CHANGED
|
@@ -35,6 +35,31 @@ export function exportOption(ctx, opts, key, fallback) {
|
|
|
35
35
|
? (opts[key] ?? fallback) : fallback;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Rebuild the options a live recapture needs from an export context: capture inputs, not
|
|
40
|
+
* released stage artifacts or export-session internals. createContext splits exclusion
|
|
41
|
+
* predicates out of `exclude`; they are put back. Pinned by
|
|
42
|
+
* __tests__/plugin.recording.contract.test.js and __tests__/plugin.stream.test.js.
|
|
43
|
+
* @param {object} ctx - Export context
|
|
44
|
+
* @param {object} opts - Normalized per-export options
|
|
45
|
+
* @returns {object} options for snapdom(ctx.element, ...)
|
|
46
|
+
*/
|
|
47
|
+
export function captureInputs(ctx, opts) {
|
|
48
|
+
const capture = {};
|
|
49
|
+
for (const key of ['debug', 'width', 'height', 'dpr', 'backgroundColor', 'quality', 'format',
|
|
50
|
+
'exclude', 'excludeMode', 'filter', 'filterMode', 'placeholders', 'captureSelection', 'canvas', 'embedFonts',
|
|
51
|
+
'iconFonts', 'localFonts', 'excludeFonts', 'fontStylesheetDomains', 'fallbackURL', 'cache',
|
|
52
|
+
'useProxy', 'outerTransforms', 'outerShadows', 'reconcile', 'burst', 'engine', 'invalidate',
|
|
53
|
+
'clip', 'compress', 'excludeStyleProps', 'resolvePicturePlaceholders', 'plugins']) {
|
|
54
|
+
if (opts[key] !== undefined) capture[key] = opts[key];
|
|
55
|
+
else if (ctx[key] !== undefined) capture[key] = ctx[key];
|
|
56
|
+
}
|
|
57
|
+
if (capture.exclude === ctx.exclude && ctx.excludePredicates?.length) {
|
|
58
|
+
capture.exclude = [...(ctx.exclude || []), ...ctx.excludePredicates];
|
|
59
|
+
}
|
|
60
|
+
return capture;
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
/**
|
|
39
64
|
* Build a live frame reader that preserves capture options and validates finite pacing.
|
|
40
65
|
* Recombine v3's separated exclusion predicates before recapturing. Pinned by
|
|
@@ -57,20 +82,7 @@ export function frameOptions(ctx, opts, defaults, name) {
|
|
|
57
82
|
const count = frames ?? Math.max(1, Math.round(duration / 1000 * fps));
|
|
58
83
|
if (!Number.isFinite(count)) throw new RangeError(`[SnapDOM] ${name}: frame count must be finite`);
|
|
59
84
|
|
|
60
|
-
|
|
61
|
-
// particular createContext splits exclusion predicates out of exclude: put them back.
|
|
62
|
-
const capture = {};
|
|
63
|
-
for (const key of ['debug', 'width', 'height', 'dpr', 'backgroundColor', 'quality', 'format',
|
|
64
|
-
'exclude', 'excludeMode', 'filter', 'filterMode', 'placeholders', 'captureSelection', 'canvas', 'embedFonts',
|
|
65
|
-
'iconFonts', 'localFonts', 'excludeFonts', 'fontStylesheetDomains', 'fallbackURL', 'cache',
|
|
66
|
-
'useProxy', 'outerTransforms', 'outerShadows', 'reconcile', 'burst', 'engine', 'invalidate',
|
|
67
|
-
'clip', 'compress', 'excludeStyleProps', 'resolvePicturePlaceholders', 'plugins']) {
|
|
68
|
-
if (opts[key] !== undefined) capture[key] = opts[key];
|
|
69
|
-
else if (ctx[key] !== undefined) capture[key] = ctx[key];
|
|
70
|
-
}
|
|
71
|
-
if (capture.exclude === ctx.exclude && ctx.excludePredicates?.length) {
|
|
72
|
-
capture.exclude = [...(ctx.exclude || []), ...ctx.excludePredicates];
|
|
73
|
-
}
|
|
85
|
+
const capture = captureInputs(ctx, opts);
|
|
74
86
|
capture.scale = scale;
|
|
75
87
|
return {
|
|
76
88
|
fps, count,
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -19,3 +19,4 @@ export { htmlExport } from './html-export.js';
|
|
|
19
19
|
export { gifExport } from './gif-export.js';
|
|
20
20
|
export { videoExport } from './video-export.js';
|
|
21
21
|
export { vector } from './vector.js';
|
|
22
|
+
export { stream, streamExport, streamTexture, recordCanvas, overlayStage, scoreboard, lowerThird, ticker, liveBadge } from './stream.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zumer/snapdom-plugins",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "Official plugins for SnapDOM",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"./gif-export": "./gif-export.js",
|
|
22
22
|
"./video-export": "./video-export.js",
|
|
23
23
|
"./pdf": "./pdf.js",
|
|
24
|
-
"./vector": "./vector.js"
|
|
24
|
+
"./vector": "./vector.js",
|
|
25
|
+
"./stream": "./stream.js"
|
|
25
26
|
},
|
|
26
27
|
"files": [
|
|
27
28
|
"*.js",
|
|
@@ -37,7 +38,10 @@
|
|
|
37
38
|
"vector/svd/",
|
|
38
39
|
"vector/CONTRACT.md",
|
|
39
40
|
"vector/LICENSE",
|
|
40
|
-
"vector/README.md"
|
|
41
|
+
"vector/README.md",
|
|
42
|
+
"stream/src/",
|
|
43
|
+
"stream/README.md",
|
|
44
|
+
"stream/LICENSE"
|
|
41
45
|
],
|
|
42
46
|
"types": "./index.d.ts",
|
|
43
47
|
"keywords": [
|
|
@@ -62,7 +66,7 @@
|
|
|
62
66
|
"url": "https://github.com/sponsors/tinchox5"
|
|
63
67
|
},
|
|
64
68
|
"peerDependencies": {
|
|
65
|
-
"@zumer/snapdom": "^3.3.
|
|
69
|
+
"@zumer/snapdom": "^3.3.8"
|
|
66
70
|
},
|
|
67
71
|
"devDependencies": {
|
|
68
72
|
"@zumer/snapdom": "file:../.."
|
package/pdf/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# PDF plugin
|
|
2
2
|
|
|
3
|
-
MIT-licensed PDF export for SnapDOM, replacing the former
|
|
3
|
+
MIT-licensed PDF export for SnapDOM, replacing the former `pdf-image` implementation. This source is part of `@zumer/snapdom-plugins` 4.x and requires core 3.x. The 4.x release is available on npm.
|
|
4
4
|
|
|
5
5
|
```js
|
|
6
6
|
import { snapdom } from '@zumer/snapdom';
|
|
@@ -17,6 +17,6 @@ Put capture-time measurement options on `pdf()` and paper/output options on `toP
|
|
|
17
17
|
|
|
18
18
|
## Live demo and verification
|
|
19
19
|
|
|
20
|
-
From the monorepo root, run `npm run compile` and `npm run site`, then open `/
|
|
20
|
+
From the monorepo root, run `npm run compile` and `npm run site`, then open `/plugins/pdf/` or `/showcase/`. These demos generate real PDFs from editable DOM samples. The retained `/pro/` URL does not denote a paid product.
|
|
21
21
|
|
|
22
22
|
Run `npm run test:pdf` for the PDF engine suites. See [verification notes](test/README.md).
|
package/pdf/src/mega/README.md
CHANGED
package/stream/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ZumerLab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/stream/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# stream
|
|
2
|
+
|
|
3
|
+
A live element as a canvas that repaints only what changed. For WebGL textures, video overlays
|
|
4
|
+
and anything else that needs the DOM as pixels, many times a second.
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
import { stream } from '@zumer/snapdom-plugins/stream';
|
|
8
|
+
|
|
9
|
+
const live = stream(panel, {
|
|
10
|
+
scale: 2,
|
|
11
|
+
onFrame(frame) {
|
|
12
|
+
// frame.canvas is updated in place; frame.rects lists what changed, in canvas pixels.
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
await live.ready;
|
|
16
|
+
// ...
|
|
17
|
+
live.stop();
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## How it works
|
|
21
|
+
|
|
22
|
+
The first frame is a full capture. After that, every change (mutations, typing, focus, hover,
|
|
23
|
+
running animations) is traced to the nearest box that can be repainted on its own, and only
|
|
24
|
+
that subtree is captured and drawn into its rect. A frame that cannot be patched exactly is a
|
|
25
|
+
full frame, and `frame.reason` says why (`'root resized'`, `'no host for <div.x>'`, ...).
|
|
26
|
+
A keyframe in idle time heals anything the change tracking could not see, at most every 2 s
|
|
27
|
+
and never more than 2% of the main thread.
|
|
28
|
+
|
|
29
|
+
Every patched frame is meant to match a full capture of the same DOM state pixel for pixel.
|
|
30
|
+
The test suite checks exactly that on Chromium, Firefox and WebKit.
|
|
31
|
+
|
|
32
|
+
Measured on a 1000px grid with one card changing per frame, median per frame:
|
|
33
|
+
|
|
34
|
+
| cards | Chromium full / patch | Firefox | WebKit |
|
|
35
|
+
|---|---|---|---|
|
|
36
|
+
| 400 | 11.7 / 4.2 ms | 28 / 4 ms | 20 / 4 ms |
|
|
37
|
+
| 2000 | 52.9 / 4.9 ms | 119 / 5 ms | 90 / 4 ms |
|
|
38
|
+
|
|
39
|
+
## Options
|
|
40
|
+
|
|
41
|
+
All [capture options](https://snapdom.dev/) apply (`scale`, `dpr`, `embedFonts`, `exclude`,
|
|
42
|
+
...), plus:
|
|
43
|
+
|
|
44
|
+
| option | default | |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `onFrame(frame)` | | called after each repaint: `{ full, reason, rects, ms, canvas }` |
|
|
47
|
+
| `fps` | every animation frame | upper bound on repaints per second |
|
|
48
|
+
| `patches` | `true` | `false` repaints full frames only, for comparison |
|
|
49
|
+
|
|
50
|
+
`stream()` returns `{ canvas, ready, stats, stop() }`.
|
|
51
|
+
|
|
52
|
+
## WebGL
|
|
53
|
+
|
|
54
|
+
`streamTexture(gl)` keeps a texture in step with the stream. On WebGL2 it uploads only the
|
|
55
|
+
changed rects (4% of the texture per frame in the WebGL demo, against 100%).
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import { stream, streamTexture } from '@zumer/snapdom-plugins/stream';
|
|
59
|
+
|
|
60
|
+
const tex = streamTexture(gl);
|
|
61
|
+
stream(panel, { onFrame: (frame) => tex.update(frame) });
|
|
62
|
+
// draw with tex.texture
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Video
|
|
66
|
+
|
|
67
|
+
Composite `live.canvas` over anything with `drawImage`, and record the result:
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import { recordCanvas } from '@zumer/snapdom-plugins/stream';
|
|
71
|
+
|
|
72
|
+
const rec = recordCanvas(output, { fps: 30, duration: 10000 });
|
|
73
|
+
const blob = await rec.done; // MP4 or WebM, whichever the browser records
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## WebRTC
|
|
77
|
+
|
|
78
|
+
The composited canvas is a video track like a camera's, so an HTML overlay travels inside the
|
|
79
|
+
video to every viewer, with no desktop software and no server-side render:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
const track = output.captureStream(30).getVideoTracks()[0];
|
|
83
|
+
track.contentHint = 'detail'; // graphics are text: keep the resolution
|
|
84
|
+
peerConnection.addTrack(track, new MediaStream([track]));
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`demos/stream-webrtc.html` sends one tab's program to viewer tabs this way.
|
|
88
|
+
|
|
89
|
+
## Overlay templates
|
|
90
|
+
|
|
91
|
+
A stage and four broadcast graphics, as plain DOM with a small controller each. Every box that
|
|
92
|
+
changes is opaque and the lower third slides with `left`, so a score, a clock tick or a slide
|
|
93
|
+
costs a patch, not a full frame.
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
import { stream, overlayStage, scoreboard, lowerThird, ticker, liveBadge } from '@zumer/snapdom-plugins/stream';
|
|
97
|
+
|
|
98
|
+
const stage = overlayStage({ width: 1920, height: 1080, theme: { accent: '#facc15' } });
|
|
99
|
+
const score = scoreboard({ home: 'HOME', away: 'AWAY' });
|
|
100
|
+
const lower = lowerThird({ name: 'Alex Morgan', role: 'Commentator' });
|
|
101
|
+
stage.append(score.el, liveBadge().el, lower.el, ticker({ items: ['Kick-off at 18:00'] }).el);
|
|
102
|
+
document.body.append(stage);
|
|
103
|
+
|
|
104
|
+
score.clock.start();
|
|
105
|
+
score.add('home');
|
|
106
|
+
lower.set({ name: 'Jamie Rivera' });
|
|
107
|
+
lower.hide();
|
|
108
|
+
|
|
109
|
+
const live = stream(stage, { onFrame });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Theme through CSS variables on the stage: `accent`, `panel`, `panel-2`, `text`, `ink`, `live`,
|
|
113
|
+
`font`.
|
|
114
|
+
|
|
115
|
+
## As an export
|
|
116
|
+
|
|
117
|
+
`streamExport()` adds `toStream()` to a capture result, next to `toMp4()` and `toGif()`:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
import { streamExport } from '@zumer/snapdom-plugins/stream';
|
|
121
|
+
|
|
122
|
+
const result = await snapdom(panel, { plugins: [streamExport()] });
|
|
123
|
+
const live = await result.toStream({ onFrame });
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Limits
|
|
127
|
+
|
|
128
|
+
- Patches need a box that can be repainted standalone. A change inside a transparent box with
|
|
129
|
+
no solid backdrop, under a positioned element, or that moves its siblings is a full frame.
|
|
130
|
+
- Programmatic CSSOM edits (`insertRule`, `rule.style`) produce no signal; the keyframe heals
|
|
131
|
+
them.
|
|
132
|
+
- Text ink that spills past a box edge onto a pixel shared with a neighbour waits for the
|
|
133
|
+
keyframe.
|
|
134
|
+
- Needs SnapDOM 3.3.8 or newer, which has the `__subpixel` layout option. On an older core the
|
|
135
|
+
stream still runs, but patches at fractional positions are not pixel-exact.
|
|
136
|
+
|
|
137
|
+
Demos: `demos/stream-templates.html`, `demos/stream-overlay.html`, `demos/stream-webrtc.html`, `demos/stream-webgl.html`,
|
|
138
|
+
`demos/stream-lab.html`, `demos/stream-glass.html`, `demos/stream-fx.html`, `demos/stream-virtual.html`.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live DOM streaming for SnapDOM: an element as a canvas that repaints only what changed,
|
|
3
|
+
* plus the pieces that put it to work (a WebGL texture that uploads only dirty rects, a
|
|
4
|
+
* recorder, broadcast overlay templates). `stream(el)` is the entry; `streamExport()` adds the same thing to a capture
|
|
5
|
+
* result as `toStream()`, next to toMp4 and toGif.
|
|
6
|
+
* @module plugins/stream
|
|
7
|
+
*/
|
|
8
|
+
import { stream } from './stream.js'
|
|
9
|
+
import { captureInputs, exportOption } from '../../capture-frames.js'
|
|
10
|
+
|
|
11
|
+
export { stream } from './stream.js'
|
|
12
|
+
export { streamTexture } from './webgl.js'
|
|
13
|
+
export { recordCanvas } from './recorder.js'
|
|
14
|
+
export { overlayStage, scoreboard, lowerThird, ticker, liveBadge } from './overlays.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Plugin form: `result.toStream(options)` streams the captured element with the capture's
|
|
18
|
+
* own options. Returns the stream at once, so it never holds the result's export queue.
|
|
19
|
+
* Pinned by __tests__/plugin.stream.test.js.
|
|
20
|
+
* @param {object} [defaults] - stream options applied to every toStream()
|
|
21
|
+
* @returns {object} SnapDOM plugin
|
|
22
|
+
*/
|
|
23
|
+
export function streamExport(defaults = {}) {
|
|
24
|
+
return {
|
|
25
|
+
name: 'stream',
|
|
26
|
+
defineExports() {
|
|
27
|
+
return {
|
|
28
|
+
stream: (ctx, opts = {}) => {
|
|
29
|
+
if (!ctx.element) throw new Error('[SnapDOM] stream: there is no element to stream.')
|
|
30
|
+
const requested = ctx.export?.requestedOptions || opts
|
|
31
|
+
return stream(ctx.element, {
|
|
32
|
+
...captureInputs(ctx, opts),
|
|
33
|
+
scale: exportOption(ctx, opts, 'scale', ctx.scale ?? 1),
|
|
34
|
+
...defaults,
|
|
35
|
+
...requested,
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|