@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 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 capture engine for
4
- web apps. Core exports images and canvases; this package adds HTML, structured context,
5
- element maps, PDF, editable vectors, ASCII, visual transforms and live GIF/video recording.
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.0 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.
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. This is the MIT-licensed full exporter formerly distributed as PDF Pro.
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
- ## License
548
+ ## Live demos
545
549
 
546
- MIT
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
- ## Live demos
552
+ ## Releasing from the repository root
549
553
 
550
- Run `npm run compile` and `npm run site` from the monorepo root, then open `/playground/`, `/pro/pdf/` or `/pro/vector/`. The demos generate actual files from editable DOM samples; the `/pro/` paths are retained for existing links and do not denote paid plugins.
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
- const rows = Math.max(1, Math.round(cols * source.height / source.width * 0.5));
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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[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
- // Copy capture inputs, not released stage artifacts or export-session internals. In
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
@@ -18,3 +18,4 @@ export * from './video-export.js';
18
18
  export * from './agent-map.js';
19
19
  export * from './context-export.js';
20
20
  export * from './vector.js';
21
+ export * from './stream.js';
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.2",
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.0"
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 PDF Pro and `pdf-image` implementations. This source is part of `@zumer/snapdom-plugins` 4.x and requires core 3.x. The 4.x release is available on npm.
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 `/pro/pdf/` or `/playground/`. These demos generate real PDFs from editable DOM samples. The retained `/pro/` URL does not denote a paid product.
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).
@@ -1,4 +1,4 @@
1
- # pdfmega — the "pro pro" layer
1
+ # pdfmega: the document-generator layer
2
2
 
3
3
  The modules that took `@zumer/snapdom-pdf` from *exact capture in a PDF* to
4
4
  *document generator*: identity, diagnostics, flow control, folios, fillable
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.
@@ -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
+ }