@koiosdigital/matrx-render 0.1.7 → 0.1.9

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.
@@ -1,4 +1,4 @@
1
- import { F as FrameEncoder, W as WebpDecoder } from '../webp-Cq1VWs8C.js';
1
+ import { F as FrameEncoder, W as WebpDecoder } from '../webp-DP8HPoWf.js';
2
2
  import '../widget-DABrpucj.js';
3
3
 
4
4
  /**
@@ -9,10 +9,13 @@ import '../widget-DABrpucj.js';
9
9
  * Determinism (§8): we bypass @jsquash's `init()`, which auto-selects a
10
10
  * SIMD or non-SIMD build at runtime — two different codec builds that can
11
11
  * emit different (equally valid) bitstreams, which would fracture the
12
- * sha256/ETag across environments. Instead the NON-SIMD factory is wired
13
- * explicitly and the caller must supply that exact wasm module
14
- * (`codec/enc/webp_enc.wasm`): a Workers wasm-module binding in prod, a
15
- * compiled module from disk in Node/tests.
12
+ * sha256/ETag across environments. Instead the SIMD factory is wired
13
+ * explicitly (~2× faster encode; Workers and Node ≥16 both support WASM
14
+ * SIMD) and the caller must supply that exact wasm module
15
+ * (`codec/enc/webp_enc_simd.wasm`): a Workers wasm-module binding in prod,
16
+ * a compiled module from disk in Node/tests. The invariant is ONE exact
17
+ * build everywhere, not which build — changing it is fine but shifts every
18
+ * ETag fleet-wide at once.
16
19
  *
17
20
  * Frames are encoded LOSSLESS. Note (documented divergence): the Go
18
21
  * renderer's libwebp path encodes lossy-by-default; for 64×32 pixel art
@@ -22,8 +25,8 @@ import '../widget-DABrpucj.js';
22
25
  */
23
26
 
24
27
  /**
25
- * Create a FrameEncoder backed by the non-SIMD @jsquash/webp codec.
26
- * `wasm` must be the compiled `@jsquash/webp/codec/enc/webp_enc.wasm`
28
+ * Create a FrameEncoder backed by the SIMD @jsquash/webp codec.
29
+ * `wasm` must be the compiled `@jsquash/webp/codec/enc/webp_enc_simd.wasm`
27
30
  * module (Workers binding import or WebAssembly.compile of the file).
28
31
  */
29
32
  declare function jsquashFrameEncoder(wasm: WebAssembly.Module): FrameEncoder;
@@ -1,8 +1,8 @@
1
1
  // src/encode/webp-jsquash.ts
2
- import webpEncFactory from "@jsquash/webp/codec/enc/webp_enc.js";
2
+ import webpEncFactory from "@jsquash/webp/codec/enc/webp_enc_simd.js";
3
3
  import webpDecFactory from "@jsquash/webp/codec/dec/webp_dec.js";
4
4
  var ENCODE_OPTIONS = {
5
- quality: 75,
5
+ quality: 30,
6
6
  target_size: 0,
7
7
  target_PSNR: 0,
8
8
  method: 4,
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { C as Color16, a as Canvas, W as Widget, b as Color, B as Bounds, D as DrawContext } from './widget-DABrpucj.js';
2
2
  export { c as BLACK, T as TRANSPARENT, d as WHITE, e as bounds, f as color16, m as maxFrameCount, g as modInt, p as parseColor } from './widget-DABrpucj.js';
3
3
  import { WidgetSpec, RootSpec } from '@koiosdigital/matrx-sdk';
4
- export { D as DEFAULT_SCREEN_DELAY_MILLIS, F as FrameEncoder, I as Image, W as WebpDecoder, e as encodeWebp, s as setWebpDecoder } from './webp-Cq1VWs8C.js';
4
+ export { D as DEFAULT_SCREEN_DELAY_MILLIS, F as FrameEncoder, I as Image, W as WebpDecoder, e as encodeWebp, s as setWebpDecoder } from './webp-DP8HPoWf.js';
5
5
 
6
6
  /**
7
7
  * BDF bitmap-font parser — a line-for-line port of zachomedia/go-bdf
package/dist/index.js CHANGED
@@ -2586,22 +2586,25 @@ async function renderRoot(spec, opts = {}) {
2586
2586
 
2587
2587
  // src/encode/webp.ts
2588
2588
  var DEFAULT_SCREEN_DELAY_MILLIS = 50;
2589
+ function straightPixel(pix, i, out, o) {
2590
+ const a = pix[i + 3];
2591
+ if (a === 255) {
2592
+ out[o] = pix[i];
2593
+ out[o + 1] = pix[i + 1];
2594
+ out[o + 2] = pix[i + 2];
2595
+ } else if (a !== 0) {
2596
+ const a16 = a * 257;
2597
+ out[o] = Math.floor(pix[i] * 257 * 65535 / a16) >> 8;
2598
+ out[o + 1] = Math.floor(pix[i + 1] * 257 * 65535 / a16) >> 8;
2599
+ out[o + 2] = Math.floor(pix[i + 2] * 257 * 65535 / a16) >> 8;
2600
+ }
2601
+ out[o + 3] = a;
2602
+ }
2589
2603
  function toStraightRGBA(canvas) {
2590
2604
  const { pix } = canvas;
2591
2605
  const out = new Uint8Array(pix.length);
2592
2606
  for (let i = 0; i < pix.length; i += 4) {
2593
- const a = pix[i + 3];
2594
- if (a === 255) {
2595
- out[i] = pix[i];
2596
- out[i + 1] = pix[i + 1];
2597
- out[i + 2] = pix[i + 2];
2598
- } else if (a !== 0) {
2599
- const a16 = a * 257;
2600
- out[i] = Math.floor(pix[i] * 257 * 65535 / a16) >> 8;
2601
- out[i + 1] = Math.floor(pix[i + 1] * 257 * 65535 / a16) >> 8;
2602
- out[i + 2] = Math.floor(pix[i + 2] * 257 * 65535 / a16) >> 8;
2603
- }
2604
- out[i + 3] = a;
2607
+ straightPixel(pix, i, out, i);
2605
2608
  }
2606
2609
  return out;
2607
2610
  }
@@ -2683,6 +2686,55 @@ function u24(dv, offset, v) {
2683
2686
  dv.setUint8(offset + 1, v >> 8 & 255);
2684
2687
  dv.setUint8(offset + 2, v >> 16 & 255);
2685
2688
  }
2689
+ function sameCanvas(a, b) {
2690
+ if (a === b) return true;
2691
+ const ap = a.pix;
2692
+ const bp = b.pix;
2693
+ for (let i = 0; i < ap.length; i++) {
2694
+ if (ap[i] !== bp[i]) return false;
2695
+ }
2696
+ return true;
2697
+ }
2698
+ function deltaFrame(prev, cur, durationMs) {
2699
+ const { width, height } = cur;
2700
+ const cp = cur.pix;
2701
+ const pp = prev.pix;
2702
+ let minX = width;
2703
+ let minY = height;
2704
+ let maxX = -1;
2705
+ let maxY = -1;
2706
+ let allOpaque = true;
2707
+ for (let y = 0; y < height; y++) {
2708
+ for (let x = 0; x < width; x++) {
2709
+ const i = (y * width + x) * 4;
2710
+ if (cp[i] === pp[i] && cp[i + 1] === pp[i + 1] && cp[i + 2] === pp[i + 2] && cp[i + 3] === pp[i + 3]) {
2711
+ continue;
2712
+ }
2713
+ if (x < minX) minX = x;
2714
+ if (x > maxX) maxX = x;
2715
+ if (y < minY) minY = y;
2716
+ if (y > maxY) maxY = y;
2717
+ if (cp[i + 3] !== 255) allOpaque = false;
2718
+ }
2719
+ }
2720
+ if (maxX < 0) return null;
2721
+ const x0 = minX & ~1;
2722
+ const y0 = minY & ~1;
2723
+ const w = maxX - x0 + 1;
2724
+ const h = maxY - y0 + 1;
2725
+ const rgba = new Uint8Array(w * h * 4);
2726
+ for (let y = 0; y < h; y++) {
2727
+ for (let x = 0; x < w; x++) {
2728
+ const i = ((y0 + y) * width + (x0 + x)) * 4;
2729
+ const o = (y * w + x) * 4;
2730
+ if (allOpaque && cp[i] === pp[i] && cp[i + 1] === pp[i + 1] && cp[i + 2] === pp[i + 2] && cp[i + 3] === pp[i + 3]) {
2731
+ continue;
2732
+ }
2733
+ straightPixel(cp, i, rgba, o);
2734
+ }
2735
+ }
2736
+ return { x: x0, y: y0, w, h, rgba, blend: allOpaque, durationMs };
2737
+ }
2686
2738
  async function encodeWebp(frames, opts) {
2687
2739
  if (frames.length === 0) {
2688
2740
  return new Uint8Array(0);
@@ -2702,10 +2754,49 @@ async function encodeWebp(frames, opts) {
2702
2754
  durations.push(frameDuration);
2703
2755
  if (maxDuration > 0 && remaining <= 0) break;
2704
2756
  }
2757
+ const specs = [
2758
+ {
2759
+ x: 0,
2760
+ y: 0,
2761
+ w: width,
2762
+ h: height,
2763
+ rgba: toStraightRGBA(frames[0]),
2764
+ blend: false,
2765
+ durationMs: durations[0]
2766
+ }
2767
+ ];
2768
+ const uniqueFrames = [frames[0]];
2769
+ for (let i = 1; i < durations.length; i++) {
2770
+ const prev = uniqueFrames[uniqueFrames.length - 1];
2771
+ const spec = sameCanvas(prev, frames[i]) ? null : deltaFrame(prev, frames[i], durations[i]);
2772
+ if (spec === null) {
2773
+ specs[specs.length - 1].durationMs += durations[i];
2774
+ } else {
2775
+ specs.push(spec);
2776
+ uniqueFrames.push(frames[i]);
2777
+ }
2778
+ }
2705
2779
  const encoded = [];
2706
2780
  let anyAlpha = false;
2707
- for (let i = 0; i < durations.length; i++) {
2708
- const file = await opts.encoder(toStraightRGBA(frames[i]), width, height);
2781
+ for (let i = 0; i < specs.length; i++) {
2782
+ const spec = specs[i];
2783
+ let file = await opts.encoder(spec.rgba, spec.w, spec.h);
2784
+ if (spec.blend || spec.w !== width || spec.h !== height) {
2785
+ const key = await opts.encoder(toStraightRGBA(uniqueFrames[i]), width, height);
2786
+ if (key.length < file.length) {
2787
+ file = key;
2788
+ specs[i] = {
2789
+ x: 0,
2790
+ y: 0,
2791
+ w: width,
2792
+ h: height,
2793
+ rgba: spec.rgba,
2794
+ // unused past this point
2795
+ blend: false,
2796
+ durationMs: spec.durationMs
2797
+ };
2798
+ }
2799
+ }
2709
2800
  const chunks = frameImageChunks(file);
2710
2801
  if (frameHasAlpha(chunks)) anyAlpha = true;
2711
2802
  encoded.push(chunks);
@@ -2728,19 +2819,20 @@ async function encodeWebp(frames, opts) {
2728
2819
  dv.setUint16(4, opts.loopCount ?? 0, true);
2729
2820
  w.chunk("ANIM", data);
2730
2821
  }
2731
- for (let i = 0; i < encoded.length; i++) {
2822
+ for (let i = 0; i < specs.length; i++) {
2823
+ const spec = specs[i];
2732
2824
  let inner = 16;
2733
2825
  for (const c of encoded[i]) {
2734
2826
  inner += 8 + c.data.length + (c.data.length & 1);
2735
2827
  }
2736
2828
  const data = new Uint8Array(inner);
2737
2829
  const dv = new DataView(data.buffer);
2738
- u24(dv, 0, 0);
2739
- u24(dv, 3, 0);
2740
- u24(dv, 6, width - 1);
2741
- u24(dv, 9, height - 1);
2742
- u24(dv, 12, durations[i]);
2743
- data[15] = 2;
2830
+ u24(dv, 0, spec.x / 2);
2831
+ u24(dv, 3, spec.y / 2);
2832
+ u24(dv, 6, spec.w - 1);
2833
+ u24(dv, 9, spec.h - 1);
2834
+ u24(dv, 12, spec.durationMs);
2835
+ data[15] = spec.blend ? 0 : 2;
2744
2836
  let o = 16;
2745
2837
  for (const c of encoded[i]) {
2746
2838
  for (let k = 0; k < 4; k++) data[o + k] = c.fourcc.charCodeAt(k);
@@ -47,12 +47,18 @@ declare class Image implements Widget {
47
47
  * (WASM libwebp — see webp-jsquash.ts); this module owns the deterministic
48
48
  * parts: frame-duration policy (delay + maxDuration cap, ported from
49
49
  * EncodeWebP) and the animation container mux (VP8X/ANIM/ANMF assembly,
50
- * the moral equivalent of libwebp's WebPAnimEncoderAssemble with
51
- * kmin=kmax=0 every frame an independent keyframe, no blending).
50
+ * the moral equivalent of libwebp's WebPAnimEncoder with frame diffing:
51
+ * frame 0 is a full keyframe; each later frame is cropped to the changed
52
+ * bounding rect vs the previous frame, with unchanged pixels inside the
53
+ * rect made transparent and alpha-blended over the canvas. Identical
54
+ * consecutive frames are merged into one longer-duration frame. Without
55
+ * this, a marquee over static album art re-encodes the art into every one
56
+ * of ~400 frames (spotify at 64x128: 868KB → ~100KB with deltas).
52
57
  *
53
- * Determinism: container assembly is pure; frame bitstreams are
54
- * deterministic for a pinned libwebp build + settings (handoff §8 — fold
55
- * the encoder version into the cache key at the render-plane layer).
58
+ * Determinism: container assembly and frame diffing are pure; frame
59
+ * bitstreams are deterministic for a pinned libwebp build + settings
60
+ * (handoff §8 — fold the encoder version into the cache key at the
61
+ * render-plane layer).
56
62
  */
57
63
 
58
64
  /** Encodes one straight-RGBA frame to a complete (still) WebP file. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koiosdigital/matrx-render",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -12,7 +12,7 @@
12
12
  "@jsquash/webp": "^1.5.0",
13
13
  "fflate": "^0.8.3",
14
14
  "jpeg-js": "^0.4.4",
15
- "@koiosdigital/matrx-sdk": "0.1.7"
15
+ "@koiosdigital/matrx-sdk": "0.1.9"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@types/node": "^26.1.0",