vireglass 2.2.0 → 2.3.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/web/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { toGLSL, SURFACE_SHADER } from '../chunk-2XJJO7SC.js';
1
+ import { toGLSL, SURFACE_SHADER } from '../chunk-PM2SWCSF.js';
2
2
  import { LENS_SHADER, toLensProps, toSurfaceUniforms, toDeviceSurfaceUniforms, DYNAMIC_UNIFORMS, ICON_UNIFORMS, OVERLAY_UNIFORMS } from '../chunk-H5QTBF67.js';
3
3
  import { lensPadDp, surfacePadDp, shadowOpacity } from '../chunk-SLRPHDV7.js';
4
4
  import '../chunk-MMPK6DCK.js';
@@ -50,6 +50,7 @@ function createTexture(gl, options) {
50
50
  const internalFormat = options.internalFormat ?? gl.RGBA8;
51
51
  const format = options.format ?? gl.RGBA;
52
52
  const type = options.type ?? gl.UNSIGNED_BYTE;
53
+ const wrap = options.wrap ?? gl.CLAMP_TO_EDGE;
53
54
  gl.bindTexture(gl.TEXTURE_2D, texture);
54
55
  gl.texImage2D(
55
56
  gl.TEXTURE_2D,
@@ -64,11 +65,45 @@ function createTexture(gl, options) {
64
65
  );
65
66
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
66
67
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
67
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
68
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
68
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);
69
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);
69
70
  gl.bindTexture(gl.TEXTURE_2D, null);
70
71
  return texture;
71
72
  }
73
+ function locationCache(gl, program) {
74
+ const cache = /* @__PURE__ */ new Map();
75
+ return (name) => {
76
+ let loc = cache.get(name);
77
+ if (loc === void 0) {
78
+ loc = gl.getUniformLocation(program, name);
79
+ cache.set(name, loc);
80
+ }
81
+ return loc;
82
+ };
83
+ }
84
+ function setUniform(gl, loc, value) {
85
+ if (!loc) return;
86
+ if (typeof value === "number") {
87
+ gl.uniform1f(loc, value);
88
+ return;
89
+ }
90
+ switch (value.length) {
91
+ case 1:
92
+ gl.uniform1f(loc, value[0]);
93
+ break;
94
+ case 2:
95
+ gl.uniform2f(loc, value[0], value[1]);
96
+ break;
97
+ case 3:
98
+ gl.uniform3f(loc, value[0], value[1], value[2]);
99
+ break;
100
+ case 4:
101
+ gl.uniform4f(loc, value[0], value[1], value[2], value[3]);
102
+ break;
103
+ default:
104
+ throw new Error(`vireglass/web: unsupported uniform size (${value.length})`);
105
+ }
106
+ }
72
107
  function createFramebuffer(gl, texture) {
73
108
  const fbo = gl.createFramebuffer();
74
109
  if (!fbo) throw new Error("vireglass/web: gl.createFramebuffer returned null");
@@ -88,6 +123,62 @@ function bindTextureAt(gl, unit, texture, program, uniformName) {
88
123
  gl.uniform1i(loc, unit);
89
124
  }
90
125
 
126
+ // src/web/gpu-timer.ts
127
+ var GPU_TIMER_RING_SIZE = 3;
128
+ function createGpuTimer(gl) {
129
+ const ext = gl.getExtension("EXT_disjoint_timer_query_webgl2");
130
+ const slots = ext ? Array.from({ length: GPU_TIMER_RING_SIZE }, () => ({
131
+ query: gl.createQuery(),
132
+ pending: false
133
+ })) : [];
134
+ let cursor = 0;
135
+ let active = null;
136
+ let lastMs = null;
137
+ function poll() {
138
+ if (!ext) return;
139
+ for (const slot of slots) {
140
+ if (!slot.pending) continue;
141
+ if (!gl.getQueryParameter(slot.query, gl.QUERY_RESULT_AVAILABLE)) continue;
142
+ slot.pending = false;
143
+ if (gl.getParameter(ext.GPU_DISJOINT_EXT)) continue;
144
+ const ns = gl.getQueryParameter(slot.query, gl.QUERY_RESULT);
145
+ lastMs = ns / 1e6;
146
+ }
147
+ }
148
+ function begin() {
149
+ active = null;
150
+ if (!ext || slots.length === 0) return;
151
+ poll();
152
+ const slot = slots[cursor];
153
+ if (slot.pending) return;
154
+ gl.beginQuery(ext.TIME_ELAPSED_EXT, slot.query);
155
+ active = slot;
156
+ }
157
+ function end() {
158
+ if (!ext || !active) return;
159
+ gl.endQuery(ext.TIME_ELAPSED_EXT);
160
+ active.pending = true;
161
+ cursor = (cursor + 1) % slots.length;
162
+ active = null;
163
+ }
164
+ function time(fn) {
165
+ begin();
166
+ try {
167
+ return fn();
168
+ } finally {
169
+ end();
170
+ }
171
+ }
172
+ function getLastMs() {
173
+ poll();
174
+ return lastMs;
175
+ }
176
+ function destroy() {
177
+ for (const slot of slots) gl.deleteQuery(slot.query);
178
+ }
179
+ return { time, getLastMs, destroy };
180
+ }
181
+
91
182
  // src/web/probe.ts
92
183
  var PROBE_GRID_WIDTH = 48;
93
184
  var PROBE_GRID_HEIGHT = 96;
@@ -308,40 +399,6 @@ void main() {
308
399
  fragColor = texture(u_content, uv);
309
400
  }
310
401
  `;
311
- function locationCache(gl, program) {
312
- const cache = /* @__PURE__ */ new Map();
313
- return (name) => {
314
- let loc = cache.get(name);
315
- if (loc === void 0) {
316
- loc = gl.getUniformLocation(program, name);
317
- cache.set(name, loc);
318
- }
319
- return loc;
320
- };
321
- }
322
- function setUniform(gl, loc, value) {
323
- if (!loc) return;
324
- if (typeof value === "number") {
325
- gl.uniform1f(loc, value);
326
- return;
327
- }
328
- switch (value.length) {
329
- case 1:
330
- gl.uniform1f(loc, value[0]);
331
- break;
332
- case 2:
333
- gl.uniform2f(loc, value[0], value[1]);
334
- break;
335
- case 3:
336
- gl.uniform3f(loc, value[0], value[1], value[2]);
337
- break;
338
- case 4:
339
- gl.uniform4f(loc, value[0], value[1], value[2], value[3]);
340
- break;
341
- default:
342
- throw new Error(`vireglass/web: unsupported uniform size (${value.length})`);
343
- }
344
- }
345
402
  function applyChannel(gl, get, names, sizes, values) {
346
403
  let cursor = 0;
347
404
  for (let i = 0; i < names.length; i += 1) {
@@ -404,6 +461,12 @@ function createVireGlassRenderer(canvas) {
404
461
  width: Math.max(width, 1),
405
462
  height: Math.max(height, 1)
406
463
  });
464
+ let backdropFbo = null;
465
+ function ensureBackdropFbo() {
466
+ if (!backdropFbo) backdropFbo = createFramebuffer(gl, contentTexture);
467
+ return backdropFbo;
468
+ }
469
+ const gpuTimer = createGpuTimer(gl);
407
470
  const iconTexture = createTexture(gl, { width: 1, height: 1 });
408
471
  const colorTexture = createTexture(gl, { width: 1, height: 1 });
409
472
  gl.bindTexture(gl.TEXTURE_2D, iconTexture);
@@ -432,128 +495,164 @@ function createVireGlassRenderer(canvas) {
432
495
  if (!sceneCtx) throw new Error("vireglass/web: scene 2D context unavailable");
433
496
  gl.deleteTexture(contentTexture);
434
497
  contentTexture = createTexture(gl, { width, height });
498
+ if (backdropFbo) {
499
+ gl.deleteFramebuffer(backdropFbo);
500
+ backdropFbo = null;
501
+ }
435
502
  }
436
503
  function render(options) {
437
504
  if (!sceneCtx) throw new Error("vireglass/web: renderer not initialized (resize was never called)");
438
- sceneCtx.clearRect(0, 0, width, height);
439
- options.scene(sceneCtx, width, height, options.offsetX ?? 0, options.offsetY ?? 0, options.density);
440
- gl.bindTexture(gl.TEXTURE_2D, contentTexture);
441
- gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, sceneCanvas);
442
- gl.bindTexture(gl.TEXTURE_2D, null);
443
- if (options.colorLayer) {
444
- gl.bindTexture(gl.TEXTURE_2D, colorTexture);
445
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, options.colorLayer);
505
+ const ctx = sceneCtx;
506
+ if (options.backdrop && options.scene) {
507
+ throw new Error(
508
+ "vireglass/web: render() needs exactly one of options.scene or options.backdrop, not both"
509
+ );
446
510
  }
447
- if (options.iconMask) {
448
- gl.bindTexture(gl.TEXTURE_2D, iconTexture);
449
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, options.iconMask);
450
- gl.bindTexture(gl.TEXTURE_2D, null);
511
+ if (!options.backdrop && !options.scene) {
512
+ throw new Error("vireglass/web: render() needs either options.scene or options.backdrop");
451
513
  }
452
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
453
- gl.viewport(0, 0, width, height);
454
- gl.disable(gl.BLEND);
455
- gl.disable(gl.SCISSOR_TEST);
456
- gl.useProgram(blitProgram);
457
- bindTextureAt(gl, 0, contentTexture, blitProgram, "u_content");
458
- setUniform(gl, blitLoc("u_resolution"), [width, height]);
459
- drawFullscreenTriangle(gl);
460
- const probes = [];
461
- for (let index = 0; index < options.pieces.length; index += 1) {
462
- const piece = options.pieces[index];
463
- const halfWidth = piece.geometry.width * options.density / 2;
464
- const halfHeight = piece.geometry.height * options.density / 2;
465
- const rect = {
466
- centerX: piece.centerX,
467
- centerY: piece.centerY,
468
- halfWidth,
469
- halfHeight
470
- };
471
- const fresh = index === 0 ? probe.sample(contentTexture, width, height, rect) : probe.statsFor(width, height, rect);
472
- const stats = fresh ? settleStats(index, fresh) : null;
473
- probes.push(stats);
474
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
475
- gl.viewport(0, 0, width, height);
476
- gl.enable(gl.BLEND);
477
- gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
478
- const reachOf = (m) => m ? Math.hypot(m.offsetX, m.offsetY) + Math.max(m.width, m.height) / 2 + m.smoothing : 0;
479
- const morphReach = Math.max(reachOf(piece.morph), reachOf(piece.morph2));
480
- const touchReach = piece.touch ? Math.hypot(piece.touch.pullX, piece.touch.pullY) + piece.touch.waveAmp * 2 + Math.max(piece.geometry.width, piece.geometry.height) * 0.05 * piece.touch.press : 0;
481
- const padPx = (lensPadDp(piece.geometry, piece.optics) + surfacePadDp(piece.geometry, 0) + morphReach + touchReach) * options.density;
482
- const left = Math.max(0, Math.floor(piece.centerX - halfWidth - padPx));
483
- const right = Math.min(width, Math.ceil(piece.centerX + halfWidth + padPx));
484
- const top = Math.max(0, Math.floor(height - (piece.centerY + halfHeight + padPx)));
485
- const bottom = Math.min(height, Math.ceil(height - (piece.centerY - halfHeight - padPx)));
486
- gl.enable(gl.SCISSOR_TEST);
487
- gl.scissor(left, top, Math.max(0, right - left), Math.max(0, bottom - top));
488
- gl.useProgram(lensProgram);
489
- bindTextureAt(gl, 0, contentTexture, lensProgram, "content");
490
- setUniform(gl, lensLoc("u_contentSize"), [width, height]);
491
- setUniform(gl, lensLoc("u_resolution"), [width, height]);
492
- setUniform(gl, lensLoc("u_center"), [piece.centerX, piece.centerY]);
493
- setUniform(gl, lensLoc("u_reach"), Math.max(width, height));
494
- setUniform(gl, lensLoc("u_contentMin"), [1, 1]);
495
- setUniform(gl, lensLoc("u_contentMax"), [width - 1, height - 1]);
496
- if (stats) {
497
- setUniform(gl, lensLoc("u_probeLuma"), stats.luma);
498
- setUniform(gl, lensLoc("u_probeBusy"), stats.busy);
499
- setUniform(gl, lensLoc("u_probeRange"), [stats.lo, stats.hi]);
500
- setUniform(gl, lensLoc("u_probeSlope"), [stats.slopeX, stats.slopeY]);
501
- setUniform(gl, lensLoc("u_probe"), [stats.r, stats.g, stats.b]);
514
+ return gpuTimer.time(() => {
515
+ if (options.backdrop) {
516
+ const fbo = ensureBackdropFbo();
517
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
518
+ gl.viewport(0, 0, width, height);
519
+ options.backdrop(gl, { framebuffer: fbo, width, height });
520
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
521
+ gl.viewport(0, 0, width, height);
522
+ gl.disable(gl.BLEND);
523
+ gl.disable(gl.SCISSOR_TEST);
524
+ gl.disable(gl.DEPTH_TEST);
525
+ gl.disable(gl.CULL_FACE);
526
+ gl.disable(gl.STENCIL_TEST);
527
+ gl.colorMask(true, true, true, true);
528
+ gl.depthMask(true);
529
+ gl.bindVertexArray(null);
530
+ gl.activeTexture(gl.TEXTURE0);
531
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
532
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
533
+ gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.BROWSER_DEFAULT_WEBGL);
502
534
  } else {
503
- setUniform(gl, lensLoc("u_probeLuma"), -1);
535
+ ctx.clearRect(0, 0, width, height);
536
+ options.scene?.(ctx, width, height, options.offsetX ?? 0, options.offsetY ?? 0, options.density);
537
+ gl.bindTexture(gl.TEXTURE_2D, contentTexture);
538
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, sceneCanvas);
539
+ gl.bindTexture(gl.TEXTURE_2D, null);
504
540
  }
505
- const lens = toLensProps(piece.optics, piece.geometry, options.density, {
506
- debug: options.debug,
507
- morph: piece.morph,
508
- morph2: piece.morph2,
509
- touch: piece.touch,
510
- progress: piece.progress,
511
- light: piece.light,
512
- appear: piece.appear,
513
- accent: piece.accent
514
- });
515
- applyChannel(gl, lensLoc, lens.uniformNames, lens.uniformSizes, lens.uniformValues);
516
- drawFullscreenTriangle(gl);
517
- gl.useProgram(surfaceProgram);
518
- const rawSurface = toSurfaceUniforms(piece.optics, piece.geometry, {
519
- debug: options.debug,
520
- morph: piece.morph,
521
- morph2: piece.morph2,
522
- bodyInLens: true,
523
- touch: piece.touch,
524
- progress: piece.progress,
525
- // The shadow is denser over text, weaker over a flat backdrop (M 11:47). The rule lives
526
- // in the core: while it lived here, Android didn't have it at all.
527
- shadow: stats ? shadowOpacity(stats.busy) : 1,
528
- // The probe has already computed the ambient color — the shadow gets it for free. Taken
529
- // as-is: `settleStats` has already smoothed it, and rounding would bring back exactly the
530
- // steps the smoothing was written to remove.
531
- ambient: stats ? [stats.r, stats.g, stats.b] : void 0,
532
- appear: piece.appear,
533
- lift: piece.lift
534
- });
535
- applyObject(gl, surfaceLoc, {
536
- ...toDeviceSurfaceUniforms(rawSurface, options.density),
537
- u_center: [piece.centerX, piece.centerY]
538
- });
539
- setUniform(gl, surfaceLoc(DYNAMIC_UNIFORMS[0]), piece.press ?? 0);
540
- setUniform(gl, surfaceLoc(DYNAMIC_UNIFORMS[1]), piece.active ?? 0);
541
- const hasIcon = Boolean(piece.icon && options.iconMask);
542
- setUniform(gl, surfaceLoc(ICON_UNIFORMS[0]), hasIcon ? 1 : 0);
543
- setUniform(gl, surfaceLoc(ICON_UNIFORMS[1]), 1);
544
- setUniform(gl, surfaceLoc(ICON_UNIFORMS[2]), piece.inkIdle ?? [1, 1, 1, 1]);
545
- setUniform(gl, surfaceLoc(ICON_UNIFORMS[3]), piece.inkActive ?? [1, 1, 1, 1]);
546
- bindTextureAt(gl, 1, iconTexture, surfaceProgram, "u_icon");
547
- setUniform(gl, surfaceLoc("u_iconSize"), hasIcon ? [width, height] : [1, 1]);
548
- const hasColor = Boolean(piece.overlay && options.colorLayer);
549
- setUniform(gl, surfaceLoc(OVERLAY_UNIFORMS[0]), hasColor ? 1 : 0);
550
- bindTextureAt(gl, 2, colorTexture, surfaceProgram, "u_overlay");
551
- setUniform(gl, surfaceLoc("u_overlaySize"), hasColor ? [width, height] : [1, 1]);
552
- setUniform(gl, surfaceLoc("u_resolution"), [width, height]);
541
+ if (options.colorLayer) {
542
+ gl.bindTexture(gl.TEXTURE_2D, colorTexture);
543
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, options.colorLayer);
544
+ }
545
+ if (options.iconMask) {
546
+ gl.bindTexture(gl.TEXTURE_2D, iconTexture);
547
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, options.iconMask);
548
+ gl.bindTexture(gl.TEXTURE_2D, null);
549
+ }
550
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
551
+ gl.viewport(0, 0, width, height);
552
+ gl.disable(gl.BLEND);
553
+ gl.disable(gl.SCISSOR_TEST);
554
+ gl.useProgram(blitProgram);
555
+ bindTextureAt(gl, 0, contentTexture, blitProgram, "u_content");
556
+ setUniform(gl, blitLoc("u_resolution"), [width, height]);
553
557
  drawFullscreenTriangle(gl);
554
- }
555
- gl.disable(gl.SCISSOR_TEST);
556
- return { probes };
558
+ const probes = [];
559
+ for (let index = 0; index < options.pieces.length; index += 1) {
560
+ const piece = options.pieces[index];
561
+ const halfWidth = piece.geometry.width * options.density / 2;
562
+ const halfHeight = piece.geometry.height * options.density / 2;
563
+ const rect = {
564
+ centerX: piece.centerX,
565
+ centerY: piece.centerY,
566
+ halfWidth,
567
+ halfHeight
568
+ };
569
+ const fresh = index === 0 ? probe.sample(contentTexture, width, height, rect) : probe.statsFor(width, height, rect);
570
+ const stats = fresh ? settleStats(index, fresh) : null;
571
+ probes.push(stats);
572
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
573
+ gl.viewport(0, 0, width, height);
574
+ gl.enable(gl.BLEND);
575
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
576
+ const reachOf = (m) => m ? Math.hypot(m.offsetX, m.offsetY) + Math.max(m.width, m.height) / 2 + m.smoothing : 0;
577
+ const morphReach = Math.max(reachOf(piece.morph), reachOf(piece.morph2));
578
+ const touchReach = piece.touch ? Math.hypot(piece.touch.pullX, piece.touch.pullY) + piece.touch.waveAmp * 2 + Math.max(piece.geometry.width, piece.geometry.height) * 0.05 * piece.touch.press : 0;
579
+ const padPx = (lensPadDp(piece.geometry, piece.optics) + surfacePadDp(piece.geometry, 0) + morphReach + touchReach) * options.density;
580
+ const left = Math.max(0, Math.floor(piece.centerX - halfWidth - padPx));
581
+ const right = Math.min(width, Math.ceil(piece.centerX + halfWidth + padPx));
582
+ const top = Math.max(0, Math.floor(height - (piece.centerY + halfHeight + padPx)));
583
+ const bottom = Math.min(height, Math.ceil(height - (piece.centerY - halfHeight - padPx)));
584
+ gl.enable(gl.SCISSOR_TEST);
585
+ gl.scissor(left, top, Math.max(0, right - left), Math.max(0, bottom - top));
586
+ gl.useProgram(lensProgram);
587
+ bindTextureAt(gl, 0, contentTexture, lensProgram, "content");
588
+ setUniform(gl, lensLoc("u_contentSize"), [width, height]);
589
+ setUniform(gl, lensLoc("u_resolution"), [width, height]);
590
+ setUniform(gl, lensLoc("u_center"), [piece.centerX, piece.centerY]);
591
+ setUniform(gl, lensLoc("u_reach"), Math.max(width, height));
592
+ setUniform(gl, lensLoc("u_contentMin"), [1, 1]);
593
+ setUniform(gl, lensLoc("u_contentMax"), [width - 1, height - 1]);
594
+ if (stats) {
595
+ setUniform(gl, lensLoc("u_probeLuma"), stats.luma);
596
+ setUniform(gl, lensLoc("u_probeBusy"), stats.busy);
597
+ setUniform(gl, lensLoc("u_probeRange"), [stats.lo, stats.hi]);
598
+ setUniform(gl, lensLoc("u_probeSlope"), [stats.slopeX, stats.slopeY]);
599
+ setUniform(gl, lensLoc("u_probe"), [stats.r, stats.g, stats.b]);
600
+ } else {
601
+ setUniform(gl, lensLoc("u_probeLuma"), -1);
602
+ }
603
+ const lens = toLensProps(piece.optics, piece.geometry, options.density, {
604
+ debug: options.debug,
605
+ morph: piece.morph,
606
+ morph2: piece.morph2,
607
+ touch: piece.touch,
608
+ progress: piece.progress,
609
+ light: piece.light,
610
+ appear: piece.appear,
611
+ accent: piece.accent
612
+ });
613
+ applyChannel(gl, lensLoc, lens.uniformNames, lens.uniformSizes, lens.uniformValues);
614
+ drawFullscreenTriangle(gl);
615
+ gl.useProgram(surfaceProgram);
616
+ const rawSurface = toSurfaceUniforms(piece.optics, piece.geometry, {
617
+ debug: options.debug,
618
+ morph: piece.morph,
619
+ morph2: piece.morph2,
620
+ bodyInLens: true,
621
+ touch: piece.touch,
622
+ progress: piece.progress,
623
+ // The shadow is denser over text, weaker over a flat backdrop (M 11:47). The rule lives
624
+ // in the core: while it lived here, Android didn't have it at all.
625
+ shadow: stats ? shadowOpacity(stats.busy) : 1,
626
+ // The probe has already computed the ambient color — the shadow gets it for free. Taken
627
+ // as-is: `settleStats` has already smoothed it, and rounding would bring back exactly the
628
+ // steps the smoothing was written to remove.
629
+ ambient: stats ? [stats.r, stats.g, stats.b] : void 0,
630
+ appear: piece.appear,
631
+ lift: piece.lift
632
+ });
633
+ applyObject(gl, surfaceLoc, {
634
+ ...toDeviceSurfaceUniforms(rawSurface, options.density),
635
+ u_center: [piece.centerX, piece.centerY]
636
+ });
637
+ setUniform(gl, surfaceLoc(DYNAMIC_UNIFORMS[0]), piece.press ?? 0);
638
+ setUniform(gl, surfaceLoc(DYNAMIC_UNIFORMS[1]), piece.active ?? 0);
639
+ const hasIcon = Boolean(piece.icon && options.iconMask);
640
+ setUniform(gl, surfaceLoc(ICON_UNIFORMS[0]), hasIcon ? 1 : 0);
641
+ setUniform(gl, surfaceLoc(ICON_UNIFORMS[1]), 1);
642
+ setUniform(gl, surfaceLoc(ICON_UNIFORMS[2]), piece.inkIdle ?? [1, 1, 1, 1]);
643
+ setUniform(gl, surfaceLoc(ICON_UNIFORMS[3]), piece.inkActive ?? [1, 1, 1, 1]);
644
+ bindTextureAt(gl, 1, iconTexture, surfaceProgram, "u_icon");
645
+ setUniform(gl, surfaceLoc("u_iconSize"), hasIcon ? [width, height] : [1, 1]);
646
+ const hasColor = Boolean(piece.overlay && options.colorLayer);
647
+ setUniform(gl, surfaceLoc(OVERLAY_UNIFORMS[0]), hasColor ? 1 : 0);
648
+ bindTextureAt(gl, 2, colorTexture, surfaceProgram, "u_overlay");
649
+ setUniform(gl, surfaceLoc("u_overlaySize"), hasColor ? [width, height] : [1, 1]);
650
+ setUniform(gl, surfaceLoc("u_resolution"), [width, height]);
651
+ drawFullscreenTriangle(gl);
652
+ }
653
+ gl.disable(gl.SCISSOR_TEST);
654
+ return { probes };
655
+ });
557
656
  }
558
657
  function destroy() {
559
658
  probe.destroy();
@@ -563,10 +662,12 @@ function createVireGlassRenderer(canvas) {
563
662
  gl.deleteTexture(contentTexture);
564
663
  gl.deleteTexture(iconTexture);
565
664
  gl.deleteTexture(colorTexture);
665
+ if (backdropFbo) gl.deleteFramebuffer(backdropFbo);
666
+ gpuTimer.destroy();
566
667
  }
567
- return { resize, render, destroy };
668
+ return { resize, render, getLastGpuMs: gpuTimer.getLastMs, destroy };
568
669
  }
569
670
 
570
- export { PROBE_GRID_HEIGHT, PROBE_GRID_WIDTH, createProbe, createVireGlassRenderer };
671
+ export { FULLSCREEN_TRIANGLE_VERTEX_SOURCE, PROBE_GRID_HEIGHT, PROBE_GRID_WIDTH, bindTextureAt, compileShader, createFramebuffer, createProbe, createProgram, createTexture, createVireGlassRenderer, drawFullscreenTriangle, locationCache, setUniform };
571
672
  //# sourceMappingURL=index.js.map
572
673
  //# sourceMappingURL=index.js.map