@glissade/backend-skia 0.36.0 → 0.37.0-pre.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/dist/index.d.ts CHANGED
@@ -1,8 +1,65 @@
1
1
  import { Canvas, Image } from "@napi-rs/canvas";
2
2
  import { BackendCaps, DisplayList, FontSpec, LayerStore, RenderBackend, TextMeasurer, TextMetricsLite, TextMetricsLite as TextMetricsLite$1, VideoFrameSource } from "@glissade/scene";
3
3
 
4
- //#region src/index.d.ts
4
+ //#region src/perceptual.d.ts
5
5
 
6
+ /**
7
+ * Perceptual golden tier (0.37) — SSIM over 8×8 luma windows, promoted from the
8
+ * test-only §3.4 parity metric to a SHIPPED module so `gs repin` and any golden
9
+ * reviewer can consume it. Two shapes:
10
+ *
11
+ * ssim(a, b, w, h) → the scalar mean (the exact number the PARITY
12
+ * suite floors on — byte-identical to before).
13
+ * ssimMap(a, b, w, h) → the PER-TILE grid + the worst tile, so a reviewer
14
+ * can localize "where did it change" and render a
15
+ * heat-map.
16
+ * heatmapRgba(map, w, h) → a full-res thermal RGBA (unchanged = near-black
17
+ * ground, hotter = larger perceptual drop), ready
18
+ * for `putPixels()` → `encodePng()`.
19
+ *
20
+ * Pure over RGBA buffers — no PNG, no canvas, no I/O (the CLI owns decode/encode).
21
+ * This is the headless twin's metric; it never runs on the embed path.
22
+ */
23
+ /** Per-tile SSIM result — the heat-map data + the worst tile for a one-line cause. */
24
+ interface SsimMap {
25
+ /** mean SSIM over every full tile — identical to `ssim()`. */
26
+ readonly mean: number;
27
+ /** the single worst (lowest-SSIM) tile's value. */
28
+ readonly min: number;
29
+ /** grid coords of the worst tile (`tx` across, `ty` down). */
30
+ readonly minTile: {
31
+ readonly tx: number;
32
+ readonly ty: number;
33
+ };
34
+ /** tiles across / down (the fully-covered grid; edge remainder is excluded). */
35
+ readonly cols: number;
36
+ readonly rows: number;
37
+ /** tile edge in px. */
38
+ readonly win: number;
39
+ /** row-major `rows*cols` SSIM values, each in [-1, 1]. */
40
+ readonly tiles: Float64Array;
41
+ }
42
+ /**
43
+ * Per-tile SSIM. Iteration order (row-major, `wy` outer) and the window formula
44
+ * are IDENTICAL to the historical scalar `ssim()`, so `.mean` is bit-for-bit the
45
+ * old return value — the PARITY floors are unaffected.
46
+ */
47
+ declare function ssimMap(a: Uint8ClampedArray, b: Uint8ClampedArray, width: number, height: number): SsimMap;
48
+ /**
49
+ * Mean SSIM over 8×8 luma windows — the §3.4 parity metric. Delegates to
50
+ * `ssimMap` and returns `.mean`; the summation order is unchanged, so the value
51
+ * is bit-identical to the historical implementation.
52
+ */
53
+ declare function ssim(a: Uint8ClampedArray, b: Uint8ClampedArray, width: number, height: number): number;
54
+ /**
55
+ * Full-resolution thermal RGBA from a `SsimMap`: unchanged tiles (SSIM≈1) recede
56
+ * to the near-black golden ground; a larger perceptual drop reads hotter
57
+ * (red → yellow → white). A pixel takes its covering tile's value; the edge
58
+ * remainder (outside the full grid) clamps to the nearest tile. Opaque.
59
+ */
60
+ declare function heatmapRgba(map: SsimMap, width: number, height: number): Uint8ClampedArray;
61
+ //#endregion
62
+ //#region src/index.d.ts
6
63
  type Drawable = Canvas | Image;
7
64
  /**
8
65
  * Factory-time measurement (§3.6): component factories run before any scene
@@ -45,4 +102,4 @@ declare class SkiaBackend implements RenderBackend {
45
102
  dispose(): void;
46
103
  }
47
104
  //#endregion
48
- export { SkiaBackend, type TextMetricsLite, createMeasurer };
105
+ export { SkiaBackend, type SsimMap, type TextMetricsLite, createMeasurer, heatmapRgba, ssim, ssimMap };
package/dist/index.js CHANGED
@@ -1,5 +1,118 @@
1
1
  import { GlobalFonts, Path2D, createCanvas } from "@napi-rs/canvas";
2
2
  import { ALL_FILTER_KINDS, Raster2D, assertFiniteFontSize, fontString } from "@glissade/scene";
3
+ //#region src/perceptual.ts
4
+ const WIN = 8;
5
+ const C1 = (.01 * 255) ** 2;
6
+ const C2 = (.03 * 255) ** 2;
7
+ function toLuma(px, n) {
8
+ const luma = new Float64Array(n);
9
+ for (let i = 0; i < n; i++) {
10
+ const o = i * 4;
11
+ luma[i] = .2126 * px[o] + .7152 * px[o + 1] + .0722 * px[o + 2];
12
+ }
13
+ return luma;
14
+ }
15
+ /**
16
+ * Per-tile SSIM. Iteration order (row-major, `wy` outer) and the window formula
17
+ * are IDENTICAL to the historical scalar `ssim()`, so `.mean` is bit-for-bit the
18
+ * old return value — the PARITY floors are unaffected.
19
+ */
20
+ function ssimMap(a, b, width, height) {
21
+ if (a.length !== b.length || a.length !== width * height * 4) throw new Error("ssimMap: buffers must be RGBA of identical dimensions");
22
+ const lumaA = toLuma(a, width * height);
23
+ const lumaB = toLuma(b, width * height);
24
+ const cols = Math.floor(width / WIN);
25
+ const rows = Math.floor(height / WIN);
26
+ const tiles = new Float64Array(rows * cols);
27
+ let total = 0;
28
+ let windows = 0;
29
+ let min = Infinity;
30
+ let minTile = {
31
+ tx: 0,
32
+ ty: 0
33
+ };
34
+ for (let ty = 0, wy = 0; wy + WIN <= height; wy += WIN, ty++) for (let tx = 0, wx = 0; wx + WIN <= width; wx += WIN, tx++) {
35
+ let sumA = 0;
36
+ let sumB = 0;
37
+ let sumA2 = 0;
38
+ let sumB2 = 0;
39
+ let sumAB = 0;
40
+ for (let y = wy; y < wy + WIN; y++) for (let x = wx; x < wx + WIN; x++) {
41
+ const va = lumaA[y * width + x];
42
+ const vb = lumaB[y * width + x];
43
+ sumA += va;
44
+ sumB += vb;
45
+ sumA2 += va * va;
46
+ sumB2 += vb * vb;
47
+ sumAB += va * vb;
48
+ }
49
+ const n = WIN * WIN;
50
+ const muA = sumA / n;
51
+ const muB = sumB / n;
52
+ const varA = sumA2 / n - muA * muA;
53
+ const varB = sumB2 / n - muB * muB;
54
+ const cov = sumAB / n - muA * muB;
55
+ const s = (2 * muA * muB + C1) * (2 * cov + C2) / ((muA * muA + muB * muB + C1) * (varA + varB + C2));
56
+ tiles[ty * cols + tx] = s;
57
+ total += s;
58
+ windows++;
59
+ if (s < min) {
60
+ min = s;
61
+ minTile = {
62
+ tx,
63
+ ty
64
+ };
65
+ }
66
+ }
67
+ return {
68
+ mean: windows ? total / windows : 1,
69
+ min: windows ? min : 1,
70
+ minTile,
71
+ cols,
72
+ rows,
73
+ win: WIN,
74
+ tiles
75
+ };
76
+ }
77
+ /**
78
+ * Mean SSIM over 8×8 luma windows — the §3.4 parity metric. Delegates to
79
+ * `ssimMap` and returns `.mean`; the summation order is unchanged, so the value
80
+ * is bit-identical to the historical implementation.
81
+ */
82
+ function ssim(a, b, width, height) {
83
+ return ssimMap(a, b, width, height).mean;
84
+ }
85
+ /**
86
+ * Full-resolution thermal RGBA from a `SsimMap`: unchanged tiles (SSIM≈1) recede
87
+ * to the near-black golden ground; a larger perceptual drop reads hotter
88
+ * (red → yellow → white). A pixel takes its covering tile's value; the edge
89
+ * remainder (outside the full grid) clamps to the nearest tile. Opaque.
90
+ */
91
+ function heatmapRgba(map, width, height) {
92
+ const out = new Uint8ClampedArray(width * height * 4);
93
+ const { cols, rows, win, tiles } = map;
94
+ for (let y = 0; y < height; y++) {
95
+ const ty = Math.min(Math.floor(y / win), rows - 1);
96
+ for (let x = 0; x < width; x++) {
97
+ const tx = Math.min(Math.floor(x / win), cols - 1);
98
+ const s = tiles[ty * cols + tx];
99
+ const t = Math.max(0, Math.min(1, (1 - s) / .2));
100
+ const o = (y * width + x) * 4;
101
+ if (t <= 1e-4) {
102
+ out[o] = 10;
103
+ out[o + 1] = 12;
104
+ out[o + 2] = 20;
105
+ } else {
106
+ out[o] = Math.min(1, 3 * t) * 255;
107
+ out[o + 1] = Math.max(0, Math.min(1, 3 * t - 1)) * 255;
108
+ out[o + 2] = Math.max(0, Math.min(1, 3 * t - 2)) * 255;
109
+ }
110
+ out[o + 3] = 255;
111
+ }
112
+ }
113
+ return out;
114
+ }
115
+ //#endregion
3
116
  //#region src/index.ts
4
117
  /**
5
118
  * @glissade/backend-skia — headless DisplayList rasterizer over @napi-rs/canvas
@@ -99,4 +212,4 @@ var SkiaBackend = class {
99
212
  }
100
213
  };
101
214
  //#endregion
102
- export { SkiaBackend, createMeasurer };
215
+ export { SkiaBackend, createMeasurer, heatmapRgba, ssim, ssimMap };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/backend-skia",
3
- "version": "0.36.0",
3
+ "version": "0.37.0-pre.1",
4
4
  "description": "glissade headless render backend: DisplayList -> @napi-rs/canvas (Skia). Node-only; the CI-grade deterministic path.",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -19,8 +19,8 @@
19
19
  ],
20
20
  "dependencies": {
21
21
  "@napi-rs/canvas": "^0.1.65",
22
- "@glissade/core": "0.36.0",
23
- "@glissade/scene": "0.36.0"
22
+ "@glissade/core": "0.37.0-pre.1",
23
+ "@glissade/scene": "0.37.0-pre.1"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",