@zumer/snapdom-plugins 4.1.0 → 4.2.1

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.1
4
+
5
+ - Fix the stream plugin: a box cut by an ancestor with overflow hidden was painted whole. Patches now stay inside every clipping ancestor.
6
+ - Export asciiFromCanvas and asciiStream from the package entry point, as the type declarations already did.
7
+ - Declare result.toStream() on the capture result type.
8
+ - Set the SnapDOM peer dependency to ^3.4.0 (3.3.8 was never published) and document asciiFromCanvas and asciiStream in the README.
9
+
10
+ Requires SnapDOM 3.4.0 or newer within 3.x.
11
+
12
+ ## 4.2.0
13
+
14
+ - 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.
15
+ - Add asciiStream(element, options): stream() plus asciiFromCanvas() on every frame, with onText(text, frame).
16
+
17
+ Requires SnapDOM 3.4.0 or newer within 3.x.
18
+
3
19
  ## 4.1.0
4
20
 
5
21
  - 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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @zumer/snapdom-plugins
2
2
 
3
- Official plugins for [SnapDOM](https://github.com/zumerlab/snapdom), a high-performance engine
4
- for capturing, modifying, and converting DOM elements into any format. Core exports images and
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
5
  canvases; this package adds HTML, structured context, element maps, PDF, editable vectors, ASCII,
6
6
  visual transforms, live GIF/video recording and live DOM streaming.
7
7
 
@@ -16,7 +16,7 @@ Install the plugins next to a core supported by their peer dependency:
16
16
  npm install @zumer/snapdom@latest @zumer/snapdom-plugins@latest
17
17
  ```
18
18
 
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.
19
+ Plugins 4.x in this checkout require SnapDOM 3.4.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.
20
20
 
21
21
  ## Usage
22
22
 
@@ -222,8 +222,22 @@ console.log(art);
222
222
  | `width` | `number` | `80` | Character width of output |
223
223
  | `charset` | `string` | `' .:-=+*#%@'` | Characters from lightest to darkest |
224
224
  | `invert` | `boolean` | `false` | Invert luminance mapping |
225
+ | `format` | `'text' \| 'html'` | `'text'` | Plain text, or colored HTML in a `<pre>` |
226
+ | `contrast` | `number` | `0` | Non-negative luminance contrast boost |
225
227
 
226
- `toAscii({ width, charset, invert })` overrides any of the three per call.
228
+ `toAscii({ width, charset, invert, format, contrast })` overrides any of them per call.
229
+
230
+ The conversion is also exported on its own. `asciiFromCanvas(source, options)` turns any
231
+ drawable (a canvas, an image, a video frame) into ASCII with the same options, and
232
+ `asciiStream(element, options)` runs it on every frame of a [`stream()`](stream/README.md):
233
+
234
+ ```js
235
+ import { asciiStream } from '@zumer/snapdom-plugins/ascii-export';
236
+
237
+ const live = asciiStream(el, { width: 100, fps: 15, onText: (text) => (pre.textContent = text) });
238
+ await live.ready;
239
+ live.stop();
240
+ ```
227
241
 
228
242
  ---
229
243
 
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/context-export.js CHANGED
@@ -228,7 +228,7 @@ function buildNode(el, rootRect, maxNodes, geometry, state) {
228
228
  Math.round(r.left - rootRect.left), Math.round(r.top - rootRect.top),
229
229
  Math.round(r.width), Math.round(r.height),
230
230
  ]
231
- } catch { }
231
+ } catch { /* a node without layout keeps no box */ }
232
232
  }
233
233
  const text = visibleText(el)
234
234
  if (text) node.text = text
package/index.js CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  export { timestampOverlay } from './timestamp-overlay.js';
10
- export { asciiExport } from './ascii-export.js';
10
+ export { asciiExport, asciiFromCanvas, asciiStream } from './ascii-export.js';
11
11
  export { replaceText } from './replace-text.js';
12
12
  export { redactInputs } from './redact-inputs.js';
13
13
  export { filter } from './filter.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zumer/snapdom-plugins",
3
- "version": "4.1.0",
3
+ "version": "4.2.1",
4
4
  "description": "Official plugins for SnapDOM",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -66,7 +66,7 @@
66
66
  "url": "https://github.com/sponsors/tinchox5"
67
67
  },
68
68
  "peerDependencies": {
69
- "@zumer/snapdom": "^3.3.8"
69
+ "@zumer/snapdom": "^3.4.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@zumer/snapdom": "file:../.."
package/stream/README.md CHANGED
@@ -131,7 +131,7 @@ const live = await result.toStream({ onFrame });
131
131
  them.
132
132
  - Text ink that spills past a box edge onto a pixel shared with a neighbour waits for the
133
133
  keyframe.
134
- - Needs SnapDOM 3.3.8 or newer, which has the `__subpixel` layout option. On an older core the
134
+ - Needs SnapDOM 3.4.0 or newer, which has the `__subpixel` layout option. On an older core the
135
135
  stream still runs, but patches at fractional positions are not pixel-exact.
136
136
 
137
137
  Demos: `demos/stream-templates.html`, `demos/stream-overlay.html`, `demos/stream-webrtc.html`, `demos/stream-webgl.html`,
@@ -215,27 +215,30 @@ export function stream(element, options = {}) {
215
215
  function hostFor(node) {
216
216
  let el = node?.nodeType === 1 ? node : node?.parentElement
217
217
  if (!el || el === element || !element.contains(el)) return null
218
- const chain = []
219
- for (let a = el; a && a !== element; a = a.parentElement) chain.push(a)
220
- // Search above the highest ancestor with an effect or a clip the box would escape.
218
+ const chain = [], styles = []
219
+ for (let a = el; a && a !== element; a = a.parentElement) { chain.push(a); styles.push(getComputedStyle(a)) }
220
+ // Search above the highest ancestor with an effect.
221
221
  let start = 0
222
- for (let i = 0; i < chain.length; i++) {
223
- const cs = getComputedStyle(chain[i])
224
- if (hasEffect(cs)) start = i + 1
225
- else if (i > 0 && clips(cs) && escapesClip(rectOf(chain[0]), rectOf(chain[i]), cs)) start = i
226
- }
222
+ for (let i = 0; i < chain.length; i++) if (hasEffect(styles[i])) start = i + 1
227
223
  for (let i = start; i < chain.length; i++) {
228
- const cs = getComputedStyle(chain[i])
224
+ const cs = styles[i]
229
225
  if (!opaqueColor(cs.backgroundColor)) continue
230
226
  // Visible overflow paints outside the box, over pixels the patch does not repaint.
231
227
  if (!clips(cs) && (chain[i].scrollWidth > chain[i].clientWidth + 1 || chain[i].scrollHeight > chain[i].clientHeight + 1)) continue
232
- return chain[i]
228
+ // A box a clipping ancestor cuts, past its edges or into a rounded corner, would be
229
+ // drawn whole: a row half out of a 40px scroller painted its hidden half (19200 px
230
+ // off). The host is the box itself, so the test is on its rect, not on the changed
231
+ // node's. Resume at the outermost ancestor that cuts it.
232
+ const r = rectOf(chain[i])
233
+ let cut = -1
234
+ for (let m = i + 1; m < chain.length; m++) if (clips(styles[m]) && escapesClip(r, rectOf(chain[m]), styles[m])) cut = m
235
+ if (cut < 0) return chain[i]
236
+ i = cut - 1
233
237
  }
234
238
  return null
235
239
  }
236
240
 
237
241
  async function full() {
238
- // A full frame is the one long task of a stream; 'auto' lets a long clone yield (#503).
239
242
  const t0 = performance.now()
240
243
  // A full frame is the one long task of a stream; 'auto' lets a long clone yield (#503).
241
244
  // Lab, 1000 cards under constant edits: worst frame gap 67 -> 33 ms. Under edits on
package/stream.d.ts CHANGED
@@ -35,6 +35,12 @@ export function stream(element: Element, options?: StreamOptions): Stream
35
35
  /** Plugin form: adds `result.toStream(options)` to a capture result. */
36
36
  export function streamExport(defaults?: StreamOptions): SnapdomPlugin
37
37
 
38
+ declare module '@zumer/snapdom' {
39
+ interface CaptureResult {
40
+ toStream(options?: StreamOptions): Promise<Stream>
41
+ }
42
+ }
43
+
38
44
  /** A WebGL texture kept in step with a stream; `update` uploads only the changed rects on WebGL2. */
39
45
  export function streamTexture(
40
46
  gl: WebGLRenderingContext | WebGL2RenderingContext,