@glissade/backend-skia 0.36.0 → 0.37.0-pre.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/dist/index.d.ts +59 -2
- package/dist/index.js +114 -1
- package/package.json +3 -3
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/
|
|
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: total / windows,
|
|
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.
|
|
3
|
+
"version": "0.37.0-pre.0",
|
|
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.
|
|
23
|
-
"@glissade/scene": "0.
|
|
22
|
+
"@glissade/core": "0.37.0-pre.0",
|
|
23
|
+
"@glissade/scene": "0.37.0-pre.0"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|