@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.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * A WebGL texture fed by a stream: uploads only the rects a frame changed.
3
+ *
4
+ * WebGL2 reads a sub-rectangle of a canvas source through the unpack parameters, so a patch
5
+ * frame costs its own pixels on the bus, not the texture's. demos/stream-webgl.html: 4% of
6
+ * a 1000x1976 texture per frame against 100% re-uploading it. WebGL1 has no row length, so
7
+ * there every frame uploads the whole canvas.
8
+ * @module plugins/stream/webgl
9
+ */
10
+
11
+ /**
12
+ * Create a texture and the `update(frame)` that keeps it in step with a stream.
13
+ * @param {WebGLRenderingContext|WebGL2RenderingContext} gl
14
+ * @param {{filter?: number}} [options] - TEXTURE_MIN/MAG_FILTER, default gl.LINEAR.
15
+ * @returns {{texture: WebGLTexture, width: number, height: number, update: (frame: {full: boolean, rects: {x:number,y:number,width:number,height:number}[], canvas: HTMLCanvasElement}) => number}}
16
+ * `update` returns the pixels it uploaded.
17
+ */
18
+ export function streamTexture(gl, options = {}) {
19
+ const texture = gl.createTexture()
20
+ const filter = options.filter ?? gl.LINEAR
21
+ const webgl2 = typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext
22
+ gl.bindTexture(gl.TEXTURE_2D, texture)
23
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter)
24
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter)
25
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
26
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
27
+ const state = { texture, width: 0, height: 0, update }
28
+
29
+ function update(frame) {
30
+ const c = frame.canvas
31
+ gl.bindTexture(gl.TEXTURE_2D, texture)
32
+ if (frame.full || !webgl2 || c.width !== state.width || c.height !== state.height) {
33
+ state.width = c.width
34
+ state.height = c.height
35
+ if (webgl2) {
36
+ gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 0)
37
+ gl.pixelStorei(gl.UNPACK_SKIP_PIXELS, 0)
38
+ gl.pixelStorei(gl.UNPACK_SKIP_ROWS, 0)
39
+ }
40
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c)
41
+ return c.width * c.height
42
+ }
43
+ gl.pixelStorei(gl.UNPACK_ROW_LENGTH, c.width)
44
+ let uploaded = 0
45
+ for (const r of frame.rects) {
46
+ gl.pixelStorei(gl.UNPACK_SKIP_PIXELS, r.x)
47
+ gl.pixelStorei(gl.UNPACK_SKIP_ROWS, r.y)
48
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, r.x, r.y, r.width, r.height, gl.RGBA, gl.UNSIGNED_BYTE, c)
49
+ uploaded += r.width * r.height
50
+ }
51
+ gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 0)
52
+ gl.pixelStorei(gl.UNPACK_SKIP_PIXELS, 0)
53
+ gl.pixelStorei(gl.UNPACK_SKIP_ROWS, 0)
54
+ return uploaded
55
+ }
56
+
57
+ return state
58
+ }
package/stream.d.ts ADDED
@@ -0,0 +1,75 @@
1
+ import type { SnapdomOptions, SnapdomPlugin } from '@zumer/snapdom'
2
+
3
+ /** One repaint of a stream: the rects of `canvas` that changed, in canvas pixels. */
4
+ export interface StreamFrame {
5
+ /** True when the whole canvas was repainted; `reason` says why. */
6
+ full: boolean
7
+ reason: string | null
8
+ rects: { x: number; y: number; width: number; height: number }[]
9
+ ms: number
10
+ canvas: HTMLCanvasElement
11
+ }
12
+
13
+ export interface StreamOptions extends SnapdomOptions {
14
+ onFrame?: (frame: StreamFrame) => void
15
+ /** Cap on frames per second; default every animation frame. */
16
+ fps?: number
17
+ /** false repaints full frames only. Default true. */
18
+ patches?: boolean
19
+ }
20
+
21
+ export interface Stream {
22
+ canvas: HTMLCanvasElement
23
+ ready: Promise<void>
24
+ stats: { full: number; patch: number; skipped: number; lastMs: number }
25
+ stop(): void
26
+ }
27
+
28
+ /**
29
+ * Stream a live element into a canvas that repaints only the boxes that changed: the
30
+ * first frame is a full capture, later frames capture and redraw just the changed
31
+ * subtrees, and anything that cannot be patched exactly is a full frame.
32
+ */
33
+ export function stream(element: Element, options?: StreamOptions): Stream
34
+
35
+ /** Plugin form: adds `result.toStream(options)` to a capture result. */
36
+ export function streamExport(defaults?: StreamOptions): SnapdomPlugin
37
+
38
+ /** A WebGL texture kept in step with a stream; `update` uploads only the changed rects on WebGL2. */
39
+ export function streamTexture(
40
+ gl: WebGLRenderingContext | WebGL2RenderingContext,
41
+ options?: { filter?: number }
42
+ ): { texture: WebGLTexture; width: number; height: number; update(frame: StreamFrame): number }
43
+
44
+ /** Record a canvas with MediaRecorder; `duration` (ms) stops it by itself. */
45
+ export function recordCanvas(
46
+ canvas: HTMLCanvasElement,
47
+ options?: { fps?: number; duration?: number; bitrate?: number }
48
+ ): { done: Promise<Blob>; stop(): Promise<Blob>; type: string }
49
+
50
+ /** The transparent stage every overlay template sits on; stream this element. */
51
+ export function overlayStage(options?: { width?: number; height?: number; theme?: Record<string, string> }): HTMLDivElement
52
+
53
+ export interface ScoreboardClock { start(): void; stop(): void; reset(seconds?: number): void; set(text: string): void }
54
+
55
+ /** Two teams, their points and a running clock. */
56
+ export function scoreboard(options?: { home?: string; away?: string; homeColor?: string; awayColor?: string }): {
57
+ el: HTMLElement
58
+ score(home: number, away: number): void
59
+ add(side: 'home' | 'away', n?: number): void
60
+ clock: ScoreboardClock
61
+ }
62
+
63
+ /** A name and a role that slide in and out. */
64
+ export function lowerThird(options?: { name?: string; role?: string; visible?: boolean }): {
65
+ el: HTMLElement
66
+ set(fields: { name?: string; role?: string }): void
67
+ show(): void
68
+ hide(): void
69
+ }
70
+
71
+ /** A scrolling news line; speed is seconds per loop. */
72
+ export function ticker(options?: { label?: string; items?: string[]; speed?: number }): { el: HTMLElement; set(items: string[]): void }
73
+
74
+ /** A pulsing LIVE bug. */
75
+ export function liveBadge(options?: { text?: string }): { el: HTMLElement; set(text: string): void }
package/stream.js ADDED
@@ -0,0 +1 @@
1
+ export { stream, streamExport, streamTexture, recordCanvas, overlayStage, scoreboard, lowerThird, ticker, liveBadge } from './stream/src/index.js';
@@ -862,12 +862,13 @@ lossless one.** For text metrics the Plugin API payload is strictly richer —
862
862
  `letter-spacing`, per-range small caps and a text stroke all survive it and none
863
863
  of them survives a paste. See the TEXT block under `emit/figma-json.js`.
864
864
 
865
- ## The two paid plugins are separate products, and now separate repositories
865
+ ## PDF and Vector stay independent
866
866
 
867
- `@zumer/snapdom-pdf` and `@zumer/snapdom-vector` are bought separately, and **neither
868
- may import the other.** This is a licence boundary before it is an architectural one:
869
- a vector customer who receives a slice of the PDF plugin has been shipped something
870
- nobody sold them.
867
+ Both exporters are MIT licensed and ship together in `@zumer/snapdom-plugins` 4.x
868
+ (`packages/plugins/pdf` and `packages/plugins/vector`), and **neither may import the
869
+ other.** The rule started as a licence boundary, when `@zumer/snapdom-pdf` and
870
+ `@zumer/snapdom-vector` were sold separately, and it stays because each exporter can
871
+ then be shipped and fixed on its own.
871
872
 
872
873
  The history is worth keeping because it is the reason for the shape. Until 2026-08-08
873
874
  `emit/pdf.js` imported the byte-level writer through `../../../pdf/src/pdf-writer.js`
@@ -883,16 +884,12 @@ more, and nothing can be fixed in both at once either: a correction here does no
883
884
  reach the PDF product, and the two will drift. That is the cost of the boundary being
884
885
  a repository rather than a rule.
885
886
 
886
- Three rules follow, and the first two are unchanged:
887
+ Two rules follow:
887
888
 
888
889
  - **Nothing in `src/writer/` may name a product or know what it is drawing.** The
889
890
  warning prefix is injected by whoever loaded it (`setWarnPrefix`).
890
891
  - **A module only this product needs belongs to this product.** There is no shared
891
892
  package left to put it in, and reaching into the other repository is not available.
892
- - **The build enforces this, not review.** `packages/vector/build.mjs` fails if the
893
- bundle names the other product, imports it, or leaves behind an import the buyer
894
- cannot resolve. A rule with no test is a rule that gets re-broken by the next
895
- person who needs one function.
896
893
 
897
894
  ## House rules
898
895
 
package/vector/README.md CHANGED
@@ -32,4 +32,4 @@ clipboard export does not require installing that plugin.
32
32
 
33
33
  ## Live demo
34
34
 
35
- Run `npm run compile` and `npm run site` from the monorepo root, then open `/pro/vector/` or `/playground/`. The demo exports real SVG and can copy artwork to Figma. The retained `/pro/` URL does not denote a paid product. See [the public contract](CONTRACT.md) for supported behavior and limits.
35
+ Run `npm run compile` and `npm run site` from the monorepo root, then open `/plugins/vector/` or `/showcase/`. The demo exports real SVG and can copy artwork to Figma. The retained `/pro/` URL does not denote a paid product. See [the public contract](CONTRACT.md) for supported behavior and limits.