@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
|
@@ -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';
|
package/vector/CONTRACT.md
CHANGED
|
@@ -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
|
-
##
|
|
865
|
+
## PDF and Vector stay independent
|
|
866
866
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
-
|
|
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 `/
|
|
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.
|