@vitreajs/vitrea 0.2.0 → 0.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.
@@ -17,6 +17,7 @@ var WGSL_ANALYSIS_PASS = `struct AnalysisUniforms {
17
17
  @group(0) @binding(3) var<storage, read_write> stats : array<f32>;
18
18
 
19
19
  var<workgroup> partialLum : array<f32, ${ANALYSIS_WORKGROUP}>;
20
+ var<workgroup> partialTone : array<f32, ${ANALYSIS_WORKGROUP}>;
20
21
  var<workgroup> partialSq : array<f32, ${ANALYSIS_WORKGROUP}>;
21
22
  var<workgroup> partialEdge : array<f32, ${ANALYSIS_WORKGROUP}>;
22
23
 
@@ -32,6 +33,7 @@ fn lum_at(uv : vec2f) -> f32 {
32
33
  fn cs_analysis(@builtin(local_invocation_index) lane : u32) {
33
34
  let total = u32(au.grid.x * au.grid.y);
34
35
  var sumLum = 0.0;
36
+ var sumTone = 0.0;
35
37
  var sumSq = 0.0;
36
38
  var sumEdge = 0.0;
37
39
  var n = 0.0;
@@ -47,7 +49,15 @@ fn cs_analysis(@builtin(local_invocation_index) lane : u32) {
47
49
  let dx = lum_at(uv + vec2f(au.texel.x, 0.0)) - lum_at(uv - vec2f(au.texel.x, 0.0));
48
50
  let dy = lum_at(uv + vec2f(0.0, au.texel.y)) - lum_at(uv - vec2f(0.0, au.texel.y));
49
51
 
52
+ // The published luminance accumulates ENCODED \u2014 the W9 model (claims
53
+ // 5.31): the reference's tone responses track the encoded-space mean of a
54
+ // structured backdrop, not its linear mean. The host decodes the mean once
55
+ // on readback, so every consumer downstream still speaks linear light.
56
+ // The linear sum stays, because the variance fold below needs a mean in
57
+ // the same space as its squares; variance and edge density stay linear \u2014
58
+ // nothing tonal reads them.
50
59
  sumLum = sumLum + c;
60
+ sumTone = sumTone + linear_to_srgb(vec3f(c, c, c)).x;
51
61
  sumSq = sumSq + c * c;
52
62
  sumEdge = sumEdge + length(vec2f(dx, dy)) * 0.5;
53
63
  n = n + 1.0;
@@ -56,6 +66,7 @@ fn cs_analysis(@builtin(local_invocation_index) lane : u32) {
56
66
  }
57
67
 
58
68
  partialLum[lane] = sumLum;
69
+ partialTone[lane] = sumTone;
59
70
  partialSq[lane] = sumSq;
60
71
  partialEdge[lane] = sumEdge;
61
72
  workgroupBarrier();
@@ -66,6 +77,7 @@ fn cs_analysis(@builtin(local_invocation_index) lane : u32) {
66
77
  if (stride == 0u) { break; }
67
78
  if (lane < stride) {
68
79
  partialLum[lane] = partialLum[lane] + partialLum[lane + stride];
80
+ partialTone[lane] = partialTone[lane] + partialTone[lane + stride];
69
81
  partialSq[lane] = partialSq[lane] + partialSq[lane + stride];
70
82
  partialEdge[lane] = partialEdge[lane] + partialEdge[lane + stride];
71
83
  }
@@ -76,7 +88,8 @@ fn cs_analysis(@builtin(local_invocation_index) lane : u32) {
76
88
  if (lane == 0u) {
77
89
  let count = max(f32(total), 1.0);
78
90
  let mean = partialLum[0] / count;
79
- stats[0] = mean;
91
+ // stats[0] is the ENCODED-space mean; the host decodes it once (W9).
92
+ stats[0] = partialTone[0] / count;
80
93
  stats[1] = max(partialSq[0] / count - mean * mean, 0.0);
81
94
  stats[2] = partialEdge[0] / count;
82
95
  stats[3] = count;
@@ -233,7 +246,12 @@ var DEFAULT_MATERIAL_PROFILE = {
233
246
  * the same 44 px span, which is the lerp-versus-multiply question C9a
234
247
  * recorded and nobody has acted on. Claims §5.13.
235
248
  */
236
- blurSigma: 3,
249
+ // 1.25 since W11c G1 (claims §5.41): the reference's interior over
250
+ // structured content is a sharp component near σ 1.25 plus a heavy one
251
+ // the scatter facet now supplies (`sizeScatterFloor`); the cascade's 3
252
+ // (claims §5.16) was the one Gaussian that best split the difference on
253
+ // a bed with no pitch axis to tell the two apart.
254
+ blurSigma: 1.25,
237
255
  tint: srgbToLinear(SRGB_WHITE_TINT),
238
256
  tintAlpha: 0.46,
239
257
  rimWidth: 1.5,
@@ -405,41 +423,45 @@ var DEFAULT_MATERIAL_PROFILE = {
405
423
  * on. The grid 1.0/1.4/2.2 is flat to 0.2%. Carrying a fitted-looking 1.4 whose
406
424
  * measurement no longer exists would be the worse of the two errors.
407
425
  */
408
- sizeScatterGainMax: 1,
426
+ // MEASURED (W11c G1, claims §5.41): the scatter facet off the identity, on
427
+ // its own curve — see `MaterialProfile.sizeScatterFloor`. The paragraph
428
+ // above records why the identity was the right answer while `blurSigma`
429
+ // was 3 and the bed had no pitch axis; the W9 probe's pitch axis is what
430
+ // identified the two-component interior this expresses.
431
+ sizeScatterGainMax: 8,
432
+ sizeScatterFloor: 0.4,
433
+ sizeScatterSpanMax: 256,
409
434
  sizeOcclusionGain: 0.05,
410
435
  sizeShadowGainMax: 1,
411
- lensBodyLodPerPx: 0.16,
412
- lensRimLodBias: 2.5,
436
+ // MEASURED (W11c G2, claims §5.43): the reference band's inward source
437
+ // displacement is 33 px at the contour on a 20.8 px lens depth — 1.6 lens
438
+ // depths — following the shader's own (1 − depth)² profile (per-shell fits
439
+ // on the probe's pitch-32/64 cells, spans 96/128 fitted, 160 held out; the
440
+ // gain's sweep 1.4/1.6/1.8/2.4 reads 0.0487/0.0478/0.0516/0.0567 at the
441
+ // shader's depth and profile). The band folds the plate from 1.6 lens depths
442
+ // inward; nothing in it is sharper or darker than the interior.
443
+ lensRefractionGain: 1.6,
413
444
  reducedTransparencyFrost: 1.75,
414
445
  increasedOcclusionLift: INCREASED_OCCLUSION_LIFT,
415
446
  strongBorderRim: { rimWidth: 2, rimAlpha: 0.95 },
416
447
  reducedTintAdaptation: 0.35,
417
- // ADVISORY (W3). The span 0.02 … 0.65 covers most of the canonical backdrop
418
- // range (0.003 … 0.891 linear), and the two ends are deliberately symmetric —
419
- // 0.45 of the seed's brightness at the dark end, 0.45 of the way to white at
420
- // the bright end — so the seed itself sits mid-range and the excursion reads
421
- // as glass rather than as two different colours.
422
448
  /*
423
- * FITTED (2026-08-31, active bed) — and the fit is that THE CURVE IS THE
424
- * IDENTITY. `floor` → 1 and `ceilMix` → 0 make `tintTone` return the seed
425
- * unchanged at every backdrop, which leaves `low` and `high` describing
426
- * nothing at all; they keep their provisional values so a bed that can
427
- * identify them has somewhere to land, exactly as `adaptiveTint` keeps two
428
- * equal ends.
449
+ * MEASURED (W10, 2026-09-02) — per-pixel least squares of the reference's
450
+ * tinted pixel against its own untinted pixel on the five W9-probe tinted
451
+ * checkerboard cells (pitch 4…64 px, 17 700 px): shade = 0.5289 + 0.4886·u,
452
+ * RMS 0.0035. Out of sample on the canonical bed the orange cells fit at
453
+ * RMS 0.003 with zero bias; blue sits 0.011 darker (the second hue's
454
+ * residual, recorded and unmodelled). The light end extrapolates past 1 and
455
+ * is clamped in `tintShade`. Claims §5.36.
429
456
  *
430
- * Monotone and uncontested over floor ∈ {0.45, 0.7, 0.85, 1} × ceilMix ∈
431
- * {0, 0.2, 0.45}: objective 0.26523 → 0.12676, a 2.09× spread, with ΔE
432
- * (0.01331 → 0.00831) and SSIM (0.9749 → 0.9788) agreeing. The reference
433
- * says the same thing directly — on three of the five calibration backdrops
434
- * its tinted interior is the declared seed EXACTLY in linear light, at a
435
- * per-channel standard deviation of 0.000. Apple's tint is the author's
436
- * colour, not a range of tones mapped to it; S219's wording described the
437
- * inactive material's residue. Claims §5.13.
457
+ * §5.13's earlier "the curve is the identity" fit was made on the material's
458
+ * MEAN over solid backdrops, where u is either ~0.97 (shade 1.0) or the
459
+ * collapse has already folded the shade out — the identity was the law's
460
+ * two endpoints, seen without anything between them.
438
461
  */
439
- tintToneFloor: 1,
440
- tintToneCeilMix: 0,
441
- tintToneLow: 0.02,
442
- tintToneHigh: 0.65,
462
+ tintShadeDark: 0.5289,
463
+ tintShadeLight: 1.0175,
464
+ tintShadeStrength: 1,
443
465
  /*
444
466
  * MEASURED (W7), against the settled apple-macos-26.5 bed, on the light and
445
467
  * dark standard profiles jointly and at both scales.
@@ -472,9 +494,21 @@ var DEFAULT_MATERIAL_PROFILE = {
472
494
  * is a VALIDATION cell and was not fitted to: the calibration set predicts
473
495
  * 0.34 there and the cell reads 0.356.
474
496
  */
497
+ /*
498
+ * RE-SCOPED (W9, claims §5.33). The four mix constants now own TEXTURE
499
+ * COLLAPSE alone — the interior mean moved to the response law below — and
500
+ * their band contracts to the one domain the collapse is real in: full at
501
+ * `dark-solid` (0.0117) for thin surfaces including the 44 px capsule
502
+ * (byte-identical collapse, arg 0.0164 ≤ low with margin), zero by
503
+ * `mid-dark-solid` (0.0595), where the reference's small surface keeps a
504
+ * textured body at 0.4561 and the old band's partial collapse (k = 0.81)
505
+ * was the measured 0.1375-vs-0.4561 overshoot. `impulse__rrect-md`
506
+ * (arg 0.0539) keeps k ≈ 0.003 against the old band's 0.008 — the same
507
+ * unadapted render that cell validated.
508
+ */
475
509
  backdropToneMax: 1,
476
- backdropToneLow: 0.03,
477
- backdropToneHigh: 0.14,
510
+ backdropToneLow: 0.02,
511
+ backdropToneHigh: 0.055,
478
512
  /*
479
513
  * REFITTED 0.09 → 0.13 (2026-08-31, active bed). The law's SHAPE is untouched
480
514
  * (Decision Log 13 stands; the two-axis rework is next wave) and `max`, `low`
@@ -493,8 +527,26 @@ var DEFAULT_MATERIAL_PROFILE = {
493
527
  * estimator's algebra assumes the two colour schemes share one tint alpha,
494
528
  * and this profile pair does not (0.46 against 0.97) — recorded in §5.13 as
495
529
  * the reason it is no longer the primary evidence for this constant.
530
+ *
531
+ * RE-SCOPED 0.13 → 0.05 with the band above (W9): the bias's one remaining
532
+ * job is keeping the thick surface's collapse argument above `high` at the
533
+ * dark anchor (0.0117 + 0.05 = 0.0617 > 0.055) while the thin capsule's
534
+ * stays below `low` — the near-binary size snap the probe measured
535
+ * (claims §5.33). The smooth size trend on structured backdrops, which the
536
+ * old wide band tried and failed to carry, belongs to the response law's
537
+ * thickness axis now.
496
538
  */
497
- backdropToneSizeBias: 0.13,
539
+ backdropToneSizeBias: 0.05,
540
+ /*
541
+ * MEASURED (W9 probe, claims §5.30–§5.33): the anchors are the probe bed's
542
+ * settled reference levels, frequency-settled over seven attested runs,
543
+ * under the probe's own native-mask interior. Thick rows pool the 96 px
544
+ * and 160 px rrects (±0.012). Not tuned; re-measured only by a new probe.
545
+ */
546
+ backdropToneAnchorX: [0.1104, 0.2706, 0.9505],
547
+ backdropToneResponseThin: [0.0126, 0.4561, 0.9713],
548
+ backdropToneResponseThick: [0.4953, 0.5744, 0.9358],
549
+ backdropToneResponseStrength: 1,
498
550
  /*
499
551
  * FITTED (recalibration cascade, 2026-08-31). W8's geometry SURVIVES the fit
500
552
  * unchanged — `sigmaPx` 15.55, `offsetPx` 7.95, `spreadPx` 3.10 — and only the
@@ -587,14 +639,19 @@ var SIZE_SPAN_MIN = DEFAULT_MATERIAL_PROFILE.sizeSpanMin;
587
639
  var SIZE_SPAN_MAX = DEFAULT_MATERIAL_PROFILE.sizeSpanMax;
588
640
  var LENS_SIZE_GAIN_MAX = DEFAULT_MATERIAL_PROFILE.lensSizeGainMax;
589
641
  var SIZE_SCATTER_GAIN_MAX = DEFAULT_MATERIAL_PROFILE.sizeScatterGainMax;
642
+ var SIZE_SCATTER_FLOOR = DEFAULT_MATERIAL_PROFILE.sizeScatterFloor;
643
+ var SIZE_SCATTER_SPAN_MAX = DEFAULT_MATERIAL_PROFILE.sizeScatterSpanMax;
590
644
  var SIZE_OCCLUSION_GAIN = DEFAULT_MATERIAL_PROFILE.sizeOcclusionGain;
591
645
  var SIZE_SHADOW_GAIN_MAX = DEFAULT_MATERIAL_PROFILE.sizeShadowGainMax;
592
- var LENS_BODY_LOD_PER_PX = DEFAULT_MATERIAL_PROFILE.lensBodyLodPerPx;
593
- var LENS_RIM_LOD_BIAS = DEFAULT_MATERIAL_PROFILE.lensRimLodBias;
646
+ var LENS_REFRACTION_GAIN = DEFAULT_MATERIAL_PROFILE.lensRefractionGain;
594
647
  var BACKDROP_TONE_MAX = DEFAULT_MATERIAL_PROFILE.backdropToneMax;
595
648
  var BACKDROP_TONE_LOW = DEFAULT_MATERIAL_PROFILE.backdropToneLow;
596
649
  var BACKDROP_TONE_HIGH = DEFAULT_MATERIAL_PROFILE.backdropToneHigh;
597
650
  var BACKDROP_TONE_SIZE_BIAS = DEFAULT_MATERIAL_PROFILE.backdropToneSizeBias;
651
+ DEFAULT_MATERIAL_PROFILE.backdropToneAnchorX;
652
+ DEFAULT_MATERIAL_PROFILE.backdropToneResponseThin;
653
+ DEFAULT_MATERIAL_PROFILE.backdropToneResponseThick;
654
+ DEFAULT_MATERIAL_PROFILE.backdropToneResponseStrength;
598
655
  var OUTER_SHADOW = DEFAULT_MATERIAL_PROFILE.outerShadow;
599
656
  function withMaterialOverrides(base, patch) {
600
657
  const optics = {};
@@ -616,22 +673,26 @@ function withMaterialOverrides(base, patch) {
616
673
  sizeSpanMax: patch.sizeSpanMax ?? base.sizeSpanMax,
617
674
  lensSizeGainMax: patch.lensSizeGainMax ?? base.lensSizeGainMax,
618
675
  sizeScatterGainMax: patch.sizeScatterGainMax ?? base.sizeScatterGainMax,
676
+ sizeScatterFloor: patch.sizeScatterFloor ?? base.sizeScatterFloor,
677
+ sizeScatterSpanMax: patch.sizeScatterSpanMax ?? base.sizeScatterSpanMax,
619
678
  sizeOcclusionGain: patch.sizeOcclusionGain ?? base.sizeOcclusionGain,
620
679
  sizeShadowGainMax: patch.sizeShadowGainMax ?? base.sizeShadowGainMax,
621
- lensBodyLodPerPx: patch.lensBodyLodPerPx ?? base.lensBodyLodPerPx,
622
- lensRimLodBias: patch.lensRimLodBias ?? base.lensRimLodBias,
680
+ lensRefractionGain: patch.lensRefractionGain ?? base.lensRefractionGain,
623
681
  reducedTransparencyFrost: patch.reducedTransparencyFrost ?? base.reducedTransparencyFrost,
624
682
  increasedOcclusionLift: patch.increasedOcclusionLift ?? base.increasedOcclusionLift,
625
683
  strongBorderRim: { ...base.strongBorderRim, ...patch.strongBorderRim },
626
684
  reducedTintAdaptation: patch.reducedTintAdaptation ?? base.reducedTintAdaptation,
627
- tintToneFloor: patch.tintToneFloor ?? base.tintToneFloor,
628
- tintToneCeilMix: patch.tintToneCeilMix ?? base.tintToneCeilMix,
629
- tintToneLow: patch.tintToneLow ?? base.tintToneLow,
630
- tintToneHigh: patch.tintToneHigh ?? base.tintToneHigh,
685
+ tintShadeDark: patch.tintShadeDark ?? base.tintShadeDark,
686
+ tintShadeLight: patch.tintShadeLight ?? base.tintShadeLight,
687
+ tintShadeStrength: patch.tintShadeStrength ?? base.tintShadeStrength,
631
688
  backdropToneMax: patch.backdropToneMax ?? base.backdropToneMax,
632
689
  backdropToneLow: patch.backdropToneLow ?? base.backdropToneLow,
633
690
  backdropToneHigh: patch.backdropToneHigh ?? base.backdropToneHigh,
634
691
  backdropToneSizeBias: patch.backdropToneSizeBias ?? base.backdropToneSizeBias,
692
+ backdropToneAnchorX: patch.backdropToneAnchorX ?? base.backdropToneAnchorX,
693
+ backdropToneResponseThin: patch.backdropToneResponseThin ?? base.backdropToneResponseThin,
694
+ backdropToneResponseThick: patch.backdropToneResponseThick ?? base.backdropToneResponseThick,
695
+ backdropToneResponseStrength: patch.backdropToneResponseStrength ?? base.backdropToneResponseStrength,
635
696
  outerShadow: { ...base.outerShadow, ...patch.outerShadow },
636
697
  lightDirection: patch.lightDirection ?? base.lightDirection,
637
698
  sweepBandRadians: patch.sweepBandRadians ?? base.sweepBandRadians,
@@ -677,28 +738,29 @@ function tintToneAdaptation(policy, profile = DEFAULT_MATERIAL_PROFILE) {
677
738
  return 0;
678
739
  }
679
740
  }
680
- function tintTone(seed, backdropLuminance, toneAdaptation, profile = DEFAULT_MATERIAL_PROFILE) {
681
- const t = smoothstep(profile.tintToneLow, profile.tintToneHigh, backdropLuminance);
682
- const k = Math.min(1, Math.max(0, toneAdaptation));
741
+ function tintShade(materialLuminance, grip, profile = DEFAULT_MATERIAL_PROFILE) {
742
+ const u = Math.min(1, Math.max(0, materialLuminance));
743
+ const shade = Math.min(1, Math.max(0, profile.tintShadeDark + (profile.tintShadeLight - profile.tintShadeDark) * u));
744
+ const k = Math.min(1, Math.max(0, grip));
745
+ return 1 + (shade - 1) * k;
746
+ }
747
+ function tintShadeLayer(seed, materialLuminance, grip, profile = DEFAULT_MATERIAL_PROFILE) {
748
+ const shade = tintShade(materialLuminance, grip, profile);
749
+ return [seed[0] * shade, seed[1] * shade, seed[2] * shade];
750
+ }
751
+ function tintedMaterialColour(material, tint, grip, profile = DEFAULT_MATERIAL_PROFILE) {
752
+ if (tint === void 0 || tint.strength <= 0)
753
+ return material;
754
+ const s = Math.min(1, Math.max(0, tint.strength));
755
+ const layer = tintShadeLayer(tint.color, relativeLuminance(material), grip, profile);
756
+ const clamp01 = (value) => Math.min(1, Math.max(0, value));
683
757
  const channel = (index) => {
684
- const s = seed[index];
685
- const low = s * profile.tintToneFloor;
686
- const high = s + (1 - s) * profile.tintToneCeilMix;
687
- return s + (low + (high - low) * t - s) * k;
758
+ const from = linearToSrgbChannel(clamp01(material[index]));
759
+ const to = linearToSrgbChannel(clamp01(layer[index]));
760
+ return srgbToLinearChannel(from + (to - from) * s);
688
761
  };
689
762
  return [channel(0), channel(1), channel(2)];
690
763
  }
691
- function tintedTintColour(neutral, tint, backdropLuminance, toneAdaptation, profile = DEFAULT_MATERIAL_PROFILE) {
692
- if (tint === void 0)
693
- return neutral;
694
- const tone = tintTone(tint.color, backdropLuminance, toneAdaptation, profile);
695
- const k = Math.min(1, Math.max(0, tint.strength));
696
- return [
697
- neutral[0] + (tone[0] - neutral[0]) * k,
698
- neutral[1] + (tone[1] - neutral[1]) * k,
699
- neutral[2] + (tone[2] - neutral[2]) * k
700
- ];
701
- }
702
764
  function backdropToneAdaptation(backdropLuminance, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
703
765
  const x = backdropLuminance + profile.backdropToneSizeBias * Math.min(1, Math.max(0, thickness));
704
766
  const span = Math.max(profile.backdropToneHigh - profile.backdropToneLow, 1e-6);
@@ -749,10 +811,14 @@ function lensSizeGainFromThickness(thickness, profile = DEFAULT_MATERIAL_PROFILE
749
811
  return 1 + (profile.lensSizeGainMax - 1) * thickness;
750
812
  }
751
813
  function sizeScatterSigma(sigmaPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
752
- return sizeScatterSigmaAt(sigmaPx, sizeThickness(spanPx, profile), profile);
814
+ return sizeScatterSigmaAt(sigmaPx, scatterThickness(spanPx, 1, profile), profile);
753
815
  }
754
- function sizeScatterSigmaAt(sigmaPx, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
755
- return sigmaPx * (1 + (profile.sizeScatterGainMax - 1) * thickness);
816
+ function scatterThickness(spanPx, fold, profile = DEFAULT_MATERIAL_PROFILE) {
817
+ const floor = Math.min(1, Math.max(0, profile.sizeScatterFloor));
818
+ return floor + (1 - floor) * smoothstep(profile.sizeSpanMin, profile.sizeScatterSpanMax, spanPx) * fold;
819
+ }
820
+ function sizeScatterSigmaAt(sigmaPx, scatter, profile = DEFAULT_MATERIAL_PROFILE) {
821
+ return sigmaPx * (1 + (profile.sizeScatterGainMax - 1) * scatter);
756
822
  }
757
823
  function sizeOcclusionAlpha(alpha, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
758
824
  return sizeOcclusionAlphaAt(alpha, sizeThickness(spanPx, profile), profile);
@@ -813,8 +879,11 @@ function lensDepthPx(thicknessPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
813
879
  const gain = lensSizeGain(spanPx, profile);
814
880
  return Math.min(Math.max(thicknessPx, 0) * gain, spanPx * 0.5);
815
881
  }
816
- function bodyLod(lensDepth, maxLod, profile = DEFAULT_MATERIAL_PROFILE) {
817
- return Math.min(Math.max(lensDepth * profile.lensBodyLodPerPx, 0), maxLod);
882
+ function lensDisplacementPx(depthPx, lensDepthPx2, refractionScale, profile = DEFAULT_MATERIAL_PROFILE) {
883
+ if (lensDepthPx2 <= 0)
884
+ return 0;
885
+ const depth = Math.min(Math.max(depthPx / lensDepthPx2, 0), 1);
886
+ return lensDepthPx2 * (1 - depth) * (1 - depth) * refractionScale * profile.lensRefractionGain;
818
887
  }
819
888
 
820
889
  // ../renderer-webgpu/dist/analysis.js
@@ -826,7 +895,11 @@ var ZERO_STATS = {
826
895
  };
827
896
  function statsFromBuffer(values) {
828
897
  return {
829
- luminance: values[0] ?? 0,
898
+ // The reduction publishes the ENCODED-space mean (the W9 model, claims
899
+ // §5.31) and this is its one decode: every consumer downstream — the
900
+ // drivers, `adaptiveTint`'s fitted thresholds — still speaks linear light,
901
+ // and a solid backdrop reads identically under either convention.
902
+ luminance: srgbToLinearChannel(values[0] ?? 0),
830
903
  variance: values[1] ?? 0,
831
904
  edgeDensity: values[2] ?? 0,
832
905
  sampleCount: values[3] ?? 0
@@ -1300,6 +1373,44 @@ function createAppTextureProvider(options) {
1300
1373
  };
1301
1374
  }
1302
1375
 
1376
+ // ../renderer-webgpu/dist/backdrop-fit.js
1377
+ var IDENTITY_FIT = [1, 1, 0, 0];
1378
+ function isUsablePlacement(placement) {
1379
+ return placement !== void 0 && Number.isFinite(placement.x) && Number.isFinite(placement.y) && placement.width > 0 && placement.height > 0;
1380
+ }
1381
+ function coverFit(sourceWidth, sourceHeight, viewportWidthCss, viewportHeightCss) {
1382
+ const viewportAspect = viewportHeightCss > 0 ? viewportWidthCss / viewportHeightCss : 1;
1383
+ const sourceAspect = sourceHeight > 0 ? sourceWidth / sourceHeight : 1;
1384
+ if (sourceAspect > viewportAspect) {
1385
+ const scaleX = viewportAspect / sourceAspect;
1386
+ return [scaleX, 1, (1 - scaleX) / 2, 0];
1387
+ }
1388
+ const scaleY = sourceAspect / viewportAspect;
1389
+ return [1, scaleY, 0, (1 - scaleY) / 2];
1390
+ }
1391
+ function placementFit(placement, viewportWidthCss, viewportHeightCss) {
1392
+ return [
1393
+ viewportWidthCss / placement.width,
1394
+ viewportHeightCss / placement.height,
1395
+ -placement.x / placement.width,
1396
+ -placement.y / placement.height
1397
+ ];
1398
+ }
1399
+ function backdropFit(sourceWidth, sourceHeight, placement, viewportWidthCss, viewportHeightCss) {
1400
+ return isUsablePlacement(placement) ? placementFit(placement, viewportWidthCss, viewportHeightCss) : coverFit(sourceWidth, sourceHeight, viewportWidthCss, viewportHeightCss);
1401
+ }
1402
+ function texelsPerCssPx(sourceWidth, sourceHeight, placement, viewportWidthCss, viewportHeightCss) {
1403
+ if (isUsablePlacement(placement)) {
1404
+ return sourceWidth > 0 ? sourceWidth / placement.width : 1;
1405
+ }
1406
+ return viewportWidthCss > 0 && viewportHeightCss > 0 ? Math.max(sourceWidth / viewportWidthCss, sourceHeight / viewportHeightCss) : 1;
1407
+ }
1408
+ function samePlacement(a, b) {
1409
+ if (a === void 0 || b === void 0)
1410
+ return a === b;
1411
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
1412
+ }
1413
+
1303
1414
  // ../renderer-webgpu/dist/device.js
1304
1415
  function createDeviceHost(options = {}) {
1305
1416
  let status = {
@@ -1834,7 +1945,7 @@ function packInstances(surfaces, origin, into) {
1834
1945
  data[o + 13] = s.channels.glow;
1835
1946
  data[o + 14] = s.lensDepthPx * Math.min(1, Math.max(0, s.channels.lensStrength));
1836
1947
  data[o + 15] = s.tintStrength;
1837
- data[o + 16] = s.sizeThickness;
1948
+ data[o + 16] = s.spanPx;
1838
1949
  data[o + 17] = 0;
1839
1950
  }
1840
1951
  return { data, count: surfaces.length };
@@ -2112,7 +2223,7 @@ var WGSL_INSTANCE_STRUCT = `struct Instance {
2112
2223
  glow : f32, // 52 interaction channel value, 0..1
2113
2224
  lensDepth : f32, // 56 CPU-resolved lens depth in CSS px (material.ts), already scaled by the lensStrength channel
2114
2225
  tintK : f32, // 60 author tint strength, 0..1 \u2014 the seed itself is a group uniform
2115
- sizeK : f32, // 64 the size law's thickness factor, 0..1 \u2014 material.ts's sizeThickness(span)
2226
+ span : f32, // 64 the surface's shorter extent, CSS px \u2014 the size law's input, both curves (W11c)
2116
2227
  _pad : f32, // 68 stride padding: vec2f aligns the struct to 8, so 72 is the next legal size
2117
2228
  };`;
2118
2229
  var WGSL_FIELD_SAMPLE = `struct FieldSample {
@@ -2278,7 +2389,7 @@ var WGSL_FIELD_PASS = `struct FieldUniforms {
2278
2389
  // field but its members are not one size, and carrying them per pixel is what
2279
2390
  // lets a 40 px button and a 320 px platter share a field pass and still lens by
2280
2391
  // their own depth \u2014 parent acceptance #2 inside a GlassEffectContainer.
2281
- // aux = (lensDepthPx, glow, sizeK, tintStrength)
2392
+ // aux = (lensDepthPx, glow, spanPx, tintStrength)
2282
2393
  //
2283
2394
  // The third and fourth slots both changed hands, and neither of the values they
2284
2395
  // used to carry was ever read by a fragment stage. 'thick' left because
@@ -2292,15 +2403,26 @@ var WGSL_FIELD_PASS = `struct FieldUniforms {
2292
2403
  // because a group is one field pass and one optics pass while its members are
2293
2404
  // not one thing:
2294
2405
  //
2295
- // 'sizeK' \u2014 the size law's thickness factor (W2). A 40 px button and a
2296
- // 320 px platter in one container lens, scatter, occlude and
2297
- // shadow by their own size.
2406
+ // 'spanPx' \u2014 the size law's input (W2): the surface's shorter extent in
2407
+ // CSS px. A 40 px button and a 320 px platter in one container
2408
+ // lens, scatter, occlude and shadow by their own size. Until
2409
+ // W11c this slot carried the folded thickness factor itself;
2410
+ // the scatter facet now rides a curve with a different band
2411
+ // top, so the span travels and each consumer evaluates its own
2412
+ // curve from it with the bands and the fold as uniforms.
2298
2413
  // 'tintStrength' \u2014 the author tint's strength (W3). One emphasised control
2299
2414
  // among plain ones is the composition Apple's guidance
2300
2415
  // describes; the seed itself is a group uniform.
2301
2416
  //
2302
2417
  // They compose rather than compete: the size law moves what the material does to
2303
2418
  // the backdrop, the tint moves what colour it does it in.
2419
+ //
2420
+ // One consequence of carrying the span rather than the thickness, stated so it
2421
+ // is not mistaken for a drift: the union below blends 'aux' linearly between two
2422
+ // members, so a pixel in the neck between a button and a platter now blends
2423
+ // their SPANS and evaluates the curves on the blend, where it used to blend the
2424
+ // evaluated thicknesses. Inside either member the two are identical; only the
2425
+ // smooth-min neck differs, and nothing measured claims a value there.
2304
2426
 
2305
2427
  @group(0) @binding(0) var<uniform> fu : FieldUniforms;
2306
2428
  @group(0) @binding(1) var<storage, read> instances : array<Instance>;
@@ -2321,7 +2443,7 @@ fn eval_instance(i : u32, p : vec2f) -> Member {
2321
2443
  var m : Member;
2322
2444
  m.d = f.d + s.inset;
2323
2445
  m.g = f.g;
2324
- m.aux = vec4f(s.lensDepth, s.glow, s.sizeK, s.tintK);
2446
+ m.aux = vec4f(s.lensDepth, s.glow, s.span, s.tintK);
2325
2447
  return m;
2326
2448
  }
2327
2449
 
@@ -2409,7 +2531,10 @@ var WGSL_HIGHLIGHT_PASS = `struct HighlightUniforms {
2409
2531
  /// accessibility refraction cap, and the resolved strength \u2014 zero where there
2410
2532
  /// is no measured backdrop tone, which stands the fade down
2411
2533
  toneAdapt : vec4f,
2412
- /// the backdrop source's own average luminance (x); yzw unused
2534
+ /// the backdrop source's own average luminance (x), and the size law's band
2535
+ /// and fold \u2014 sizeSpanMin (y), sizeSpanMax (z), the accessibility fold (w) \u2014
2536
+ /// so this pass evaluates the thickness curve off the per-pixel span exactly
2537
+ /// as the optics pass does (W11c: the span rides the field, not the factor)
2413
2538
  toneLevel : vec4f,
2414
2539
  /// this pass's uv into the FIELD texture's uv: scale (xy), offset (zw).
2415
2540
  ///
@@ -2489,7 +2614,14 @@ fn fs_highlight(in : FullscreenOut) -> @location(0) vec4f {
2489
2614
  * around a surface the optics pass had made invisible \u2014 measured at 60 pixels
2490
2615
  * past 2/255, peaking at 97/255, on exactly that cell.
2491
2616
  */
2492
- let sizeK = clamp(aux.z, 0.0, 1.0);
2617
+ // The thickness curve off the per-pixel span, written out as the optics pass
2618
+ // writes it, so the two passes fade on one curve and not on two copies.
2619
+ let spanT = clamp(
2620
+ (aux.z - hu.toneLevel.y) / max(hu.toneLevel.z - hu.toneLevel.y, 1e-6),
2621
+ 0.0,
2622
+ 1.0,
2623
+ );
2624
+ let sizeK = clamp(spanT * spanT * (3.0 - 2.0 * spanT) * hu.toneLevel.w, 0.0, 1.0);
2493
2625
  var toneAdapt = 0.0;
2494
2626
  if (hu.toneAdapt.w > 0.0) {
2495
2627
  let toneX = hu.toneLevel.x + hu.toneAdapt.z * sizeK;
@@ -2514,7 +2646,7 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
2514
2646
  screen : vec4f,
2515
2647
  /// backdrop uv transform on viewport-normalised coords: scale (xy), offset (zw)
2516
2648
  fit : vec4f,
2517
- /// refractionScale, bodyLodPerPx, rimLodBias, chainMaxLod
2649
+ /// refractionScale, lensRefractionGain (W11c G2), unused, chainMaxLod
2518
2650
  lens : vec4f,
2519
2651
  /// fixed tint colour, linear light (xyz), tint alpha (w)
2520
2652
  tint : vec4f,
@@ -2522,7 +2654,10 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
2522
2654
  adapt : vec4f,
2523
2655
  /// author tint seed, linear light (xyz), tone adaptation under the contrast regime (w)
2524
2656
  seed : vec4f,
2525
- /// tintToneFloor, tintToneCeilMix, tintToneLow, tintToneHigh
2657
+ /// the author tint's shade law (W10): tintShadeDark, tintShadeLight,
2658
+ /// tintShadeStrength (the profile's provenance gate); and (w) the size law's
2659
+ /// accessibility fold \u2014 the refraction ladder read at the preference's cap,
2660
+ /// which every facet's span-dependent rise is multiplied by (W11c)
2526
2661
  tone : vec4f,
2527
2662
  /// rimWidthPx, rimAlpha, specularPower, specularGain
2528
2663
  rim : vec4f,
@@ -2551,9 +2686,23 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
2551
2686
  shadow : vec4f,
2552
2687
  /// the outer shadow's size-law gain (x), read against the casting surface's own
2553
2688
  /// thickness, and the field rect's height in CSS px (y) \u2014 the conversion the
2554
- /// shadow's shift needs when it lands outside the texture; z and w are the
2555
- /// padding a uniform's vec4 alignment requires
2689
+ /// shadow's shift needs when it lands outside the texture; zw are the padding
2690
+ /// a uniform's vec4 alignment requires
2556
2691
  shadowSize : vec4f,
2692
+ /// the backdrop tone response's anchors (W9): the three solid anchors'
2693
+ /// ENCODED-space means (xyz), and the backdrop's linear-space mean (w) \u2014 the
2694
+ /// quantity the response solve composites against
2695
+ toneAnchor : vec4f,
2696
+ /// the response law's thin row: the reference's settled interior levels at the
2697
+ /// three anchors for a surface of sizeThickness 0 (xyz); w is the law's
2698
+ /// per-profile authority \u2014 0 on dark profiles, whose response is unmeasured
2699
+ toneRowThin : vec4f,
2700
+ /// the thick row (sizeThickness saturated), same layout
2701
+ toneRowThick : vec4f,
2702
+ /// the size law's bands (W11c): the scatter facet's floor (x) and its own
2703
+ /// band top (y), and the thickness curve's sizeSpanMin (z) and sizeSpanMax
2704
+ /// (w) \u2014 both curves start at z; the fold they multiply by is tone.w
2705
+ scatter : vec4f,
2557
2706
  };
2558
2707
 
2559
2708
  @group(0) @binding(0) var<uniform> ou : OpticsUniforms;
@@ -2564,6 +2713,42 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
2564
2713
  @group(0) @binding(5) var backdropBody : texture_2d<f32>;
2565
2714
  @group(0) @binding(6) var fieldSampler : sampler;
2566
2715
 
2716
+ /// One encoded sRGB channel from a linear one \u2014 the space the backdrop tone
2717
+ /// response's anchors live in (W9). Mirrors material.ts's 'linearToSrgbChannel'.
2718
+ fn srgb_encode(c : f32) -> f32 {
2719
+ let x = clamp(c, 0.0, 1.0);
2720
+ return select(1.055 * pow(x, 1.0 / 2.4) - 0.055, x * 12.92, x <= 0.0031308);
2721
+ }
2722
+
2723
+ /// The backdrop tone response R(encodedInput, sizeK) (W9): monotone
2724
+ /// (Fritsch\u2013Carlson) interpolation through the three anchors, clamped to their
2725
+ /// span; smoothstep between the thin and thick rows. Mirrors material.ts's
2726
+ /// 'backdropToneResponse' term for term \u2014 the constants are authored there.
2727
+ fn tone_response(x : f32, sizeK : f32) -> f32 {
2728
+ let f = sizeK * sizeK * (3.0 - 2.0 * sizeK);
2729
+ let ys = mix(ou.toneRowThin.xyz, ou.toneRowThick.xyz, vec3f(f));
2730
+ let xs = ou.toneAnchor.xyz;
2731
+ let xc = clamp(x, xs.x, xs.z);
2732
+ let h0 = max(xs.y - xs.x, 1e-4);
2733
+ let h1 = max(xs.z - xs.y, 1e-4);
2734
+ let d0 = (ys.y - ys.x) / h0;
2735
+ let d1 = (ys.z - ys.y) / h1;
2736
+ // The interior slope: the harmonic mean where the secants agree, 0 across a
2737
+ // sign change \u2014 what keeps the curve monotone between monotone anchors.
2738
+ var m1 = 0.0;
2739
+ if (d0 * d1 > 0.0) { m1 = 2.0 * d0 * d1 / (d0 + d1); }
2740
+ var h = h0; var t = (xc - xs.x) / h0;
2741
+ var y0 = ys.x; var y1 = ys.y; var s0 = d0; var s1 = m1;
2742
+ if (xc > xs.y) {
2743
+ h = h1; t = (xc - xs.y) / h1;
2744
+ y0 = ys.y; y1 = ys.z; s0 = m1; s1 = d1;
2745
+ }
2746
+ return y0 * (1.0 + 2.0 * t) * (1.0 - t) * (1.0 - t)
2747
+ + s0 * h * t * (1.0 - t) * (1.0 - t)
2748
+ + y1 * t * t * (3.0 - 2.0 * t)
2749
+ + s1 * h * t * t * (t - 1.0);
2750
+ }
2751
+
2567
2752
  /// Rim proximity: 1 exactly on the contour, falling to 0 by 'width' on either
2568
2753
  /// side. Symmetric, so the rim is a band on the boundary rather than a plateau
2569
2754
  /// that keeps burning outward where coverage has already faded.
@@ -2639,7 +2824,14 @@ fn outer_shadow_alpha(uv : vec2f, upsampled : f32, fieldSize : vec2f) -> f32 {
2639
2824
  // the remaining transparency in LINEAR light ('sizeOuterShadowOcclusionAt'),
2640
2825
  // and (1 - occ) is exactly what raising to 1/2.4 turns into (1 - alpha), so the
2641
2826
  // two forms are the same law and neither needs the other's space.
2642
- let sizeK = clamp(shadowAux.z, 0.0, 1.0);
2827
+ // The thickness curve off the casting surface's span (W11c: the span rides
2828
+ // the field), written as the body's own is below.
2829
+ let castT = clamp(
2830
+ (shadowAux.z - ou.scatter.z) / max(ou.scatter.w - ou.scatter.z, 1e-6),
2831
+ 0.0,
2832
+ 1.0,
2833
+ );
2834
+ let sizeK = clamp(castT * castT * (3.0 - 2.0 * castT) * clamp(ou.tone.w, 0.0, 1.0), 0.0, 1.0);
2643
2835
  let sizeFold = pow(max(1.0 - ou.shadowSize.x * sizeK, 0.0), 1.0 / 2.4);
2644
2836
  let alpha = 1.0 - (1.0 - ou.shadow.x) * sizeFold;
2645
2837
 
@@ -2687,51 +2879,66 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
2687
2879
 
2688
2880
  // Per-pixel, unioned through the field pass. See the module note.
2689
2881
  let lensDepth = max(aux.x, 1e-4);
2690
- // The size law's thickness factor for whichever surface owns this pixel, 0..1.
2691
- // Zero on anything at or below the profile's 'sizeSpanMin', and every term
2692
- // below multiplies by it \u2014 so a small control takes the pre-law path exactly.
2693
- let sizeK = clamp(aux.z, 0.0, 1.0);
2882
+ // The size law's two curves off the span of whichever surface owns this pixel
2883
+ // (W11c). 'sizeK' is the thickness factor, 0..1: zero on anything at or below
2884
+ // the profile's 'sizeSpanMin', saturated at 'sizeSpanMax', folded under the
2885
+ // preference \u2014 the occlusion, the inner shadow and the tone response multiply
2886
+ // by it, so a small control takes the pre-law path exactly. 'kScatter' is the
2887
+ // scatter facet's own mix, floor + (1 - floor) \xB7 smoothstep to
2888
+ // 'sizeScatterSpanMax' \xB7 fold: the floor is the material's frost at any size
2889
+ // and is not folded, the rise is depth and is. Both written out as
2890
+ // smoothsteps with a guarded denominator, so a profile that collapses a band
2891
+ // degrades to a step rather than to NaN.
2892
+ let span = aux.z;
2893
+ let fold = clamp(ou.tone.w, 0.0, 1.0);
2894
+ let thickT = clamp((span - ou.scatter.z) / max(ou.scatter.w - ou.scatter.z, 1e-6), 0.0, 1.0);
2895
+ let sizeK = clamp(thickT * thickT * (3.0 - 2.0 * thickT) * fold, 0.0, 1.0);
2896
+ let scatterFloor = clamp(ou.scatter.x, 0.0, 1.0);
2897
+ let scatterT = clamp((span - ou.scatter.z) / max(ou.scatter.y - ou.scatter.z, 1e-6), 0.0, 1.0);
2898
+ let kScatter = clamp(
2899
+ scatterFloor + (1.0 - scatterFloor) * scatterT * scatterT * (3.0 - 2.0 * scatterT) * fold,
2900
+ 0.0,
2901
+ 1.0,
2902
+ );
2694
2903
  // '-d' is depth inside the surface, so the profile runs 1 at the contour to 0
2695
2904
  // at 'lensDepth' inward, and the square makes the falloff read as curvature
2696
2905
  // rather than as a linear ramp.
2697
2906
  let depth = clamp(-d / lensDepth, 0.0, 1.0);
2698
2907
  let profile = (1.0 - depth) * (1.0 - depth);
2699
2908
 
2700
- let displaceCss = -normal * lensDepth * profile * ou.lens.x;
2909
+ // W11c G2: the lens is the body read from further inside. 'lens.y' is the
2910
+ // profile's refraction gain, the contour displacement in lens depths.
2911
+ let displaceCss = -normal * lensDepth * profile * ou.lens.x * ou.lens.y;
2701
2912
  let refracted01 = viewport01 + displaceCss / viewportCss;
2702
2913
 
2703
- let straightUv = clamp(viewport01 * ou.fit.xy + ou.fit.zw, vec2f(0.0), vec2f(1.0));
2704
2914
  let refractedUv = clamp(refracted01 * ou.fit.xy + ou.fit.zw, vec2f(0.0), vec2f(1.0));
2705
2915
 
2706
- let bodyLod = clamp(lensDepth * ou.lens.y, 0.0, ou.lens.w);
2707
- let lod = clamp(bodyLod - ou.lens.z * profile, 0.0, ou.lens.w);
2708
-
2709
2916
  /*
2710
2917
  * The scattering facet of the size law: "a softer scattering of light".
2711
2918
  *
2712
2919
  * The body texture is one blur for the whole backdrop source \u2014 it is built once
2713
2920
  * per source per frame, so it cannot be per-surface. The chain beside it can:
2714
2921
  * 'size.w' is the chain level whose blur already matches that body, so
2715
- * 'size.w + log2(scatterGain)' is the level whose blur is 'scatterGain' times
2716
- * wider, and lerping the body toward it by 'sizeK' widens the kernel with the
2717
- * surface. At sizeK = 0 the weight is zero and the body sample stands alone,
2718
- * which is what makes the whole facet inert on a small control.
2922
+ * 'size.w + log2(sizeScatterGainMax)' is the level whose blur is the gain times
2923
+ * wider \u2014 the HEAVY component, at one fixed level \u2014 and lerping the body toward
2924
+ * it by 'kScatter' is what the reference's interior measures (W11c, claims
2925
+ * \xA75.41): a sharp component near the body's \u03C3 and a heavy one near \u03C3 10, mixed
2926
+ * by a share that is \u2248 0.4 on a small control and rises with the span. The
2927
+ * mix, not the level, is what the span moves.
2719
2928
  */
2720
- let scatterGain = 1.0 + (ou.size.x - 1.0) * sizeK;
2721
- let scatterLod = clamp(ou.size.w + log2(max(scatterGain, 1e-4)), 0.0, ou.lens.w);
2929
+ let scatterLod = clamp(ou.size.w + log2(max(ou.size.x, 1e-4)), 0.0, ou.lens.w);
2722
2930
 
2723
2931
  var backdrop = vec3f(0.0);
2724
2932
  if (ou.flags.x > 0.5) {
2725
- let lensSample = textureSampleLevel(backdropChain, backdropSampler, refractedUv, lod);
2726
- let bodySample = textureSampleLevel(backdropBody, backdropSampler, straightUv, 0.0);
2727
- let scatterSample = textureSampleLevel(backdropChain, backdropSampler, straightUv, scatterLod);
2933
+ // Both components at the refracted position (W11c G2): the band and the
2934
+ // interior are one body, and the displacement alone is the lens.
2935
+ let bodySample = textureSampleLevel(backdropBody, backdropSampler, refractedUv, 0.0);
2936
+ let scatterSample = textureSampleLevel(backdropChain, backdropSampler, refractedUv, scatterLod);
2728
2937
  // Premultiplied linear in, straight colour out: the material composites over
2729
2938
  // whatever is behind it, so a partially transparent backdrop must not darken
2730
2939
  // the glass.
2731
- let lensColour = lensSample.rgb / max(lensSample.a, 1e-6);
2732
2940
  let scatterColour = scatterSample.rgb / max(scatterSample.a, 1e-6);
2733
- let bodyColour = mix(bodySample.rgb / max(bodySample.a, 1e-6), scatterColour, sizeK);
2734
- backdrop = mix(bodyColour, lensColour, profile);
2941
+ backdrop = mix(bodySample.rgb / max(bodySample.a, 1e-6), scatterColour, kScatter);
2735
2942
  }
2736
2943
 
2737
2944
  // Adaptive tint. 'adapt.w' is the strength the accessibility policy and the
@@ -2739,8 +2946,6 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
2739
2946
  // is what a 'hint' or 'none' group gets.
2740
2947
  let neutral = mix(ou.tint.rgb, ou.adapt.rgb, ou.adapt.w);
2741
2948
 
2742
- let backdropLuma = dot(backdrop, vec3f(0.2126, 0.7152, 0.0722));
2743
-
2744
2949
  /*
2745
2950
  * Backdrop tone adaptation (W7) \u2014 step two of the composition contract, between
2746
2951
  * the colour scheme's neutral and the author's tint.
@@ -2785,57 +2990,148 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
2785
2990
  * and 'adaptedTintAlpha' in material.ts.
2786
2991
  */
2787
2992
  let sizedAlpha = ou.tint.w + ou.size.y * sizeK * (1.0 - ou.tint.w);
2788
- let adaptedAlpha = sizedAlpha + toneAdapt * (1.0 - sizedAlpha);
2789
- var adapted = neutral;
2993
+
2994
+ /*
2995
+ * The backdrop tone response solve (W9) \u2014 the law that owns the interior
2996
+ * MEAN, where the collapse below owns texture and nothing else (claims
2997
+ * \xA75.33). The composite under this mechanism reduces exactly to
2998
+ * mean = (1 \u2212 k)\xB7M\u2080 + k\xB7toneLuma with M\u2080 = (1 \u2212 \u03B1)\xB7bgLinear + \u03B1\xB7L(neutral),
2999
+ * so the neutral's tone is solved in closed form: shift its luma so the
3000
+ * post-collapse mean lands on R(encodedInput, sizeK), the reference's own
3001
+ * measured response. Chroma is untouched \u2014 the shift is achromatic \u2014 and
3002
+ * the author tint still displaces the result per the composition contract.
3003
+ *
3004
+ * Three stand-downs, each measured rather than defensive: the whole axis is
3005
+ * off where no backdrop tone was measured (same gate as the collapse); the
3006
+ * solve's authority fades to zero below the dark anchor, where the only
3007
+ * evidence is the impulse cell the collapse constants were fitted on; and
3008
+ * at k \u2192 1 the collapse owns the pixel outright, so the solve's
3009
+ * extrapolation is never evaluated against a vanishing (1 \u2212 k).
3010
+ */
3011
+ var solvedNeutral = neutral;
3012
+ var solvedAlpha = sizedAlpha;
3013
+ if (ou.flags.x > 0.5 && ou.toneAdapt.w > 0.0 && ou.toneRowThin.w > 0.0 &&
3014
+ sizedAlpha > 1e-3 && toneAdapt < 0.995) {
3015
+ let encodedInput = srgb_encode(ou.toneColour.w);
3016
+ let anchor = max(ou.toneAnchor.x, 1e-4);
3017
+ let authority =
3018
+ smoothstep(anchor * 0.5, anchor, encodedInput) * clamp(ou.toneRowThin.w, 0.0, 1.0);
3019
+ if (authority > 0.0) {
3020
+ let response = tone_response(encodedInput, sizeK);
3021
+ // The collapse's mean pull is toward L(toneColour.rgb) \u2014 the LINEAR
3022
+ // mean, which toneAnchor.w carries \u2014 not toward the encoded level.
3023
+ let preCollapse = (response - toneAdapt * ou.toneAnchor.w) / (1.0 - toneAdapt);
3024
+ let neutralLuma = dot(neutral, vec3f(0.2126, 0.7152, 0.0722));
3025
+ let nominal = (1.0 - sizedAlpha) * ou.toneAnchor.w + sizedAlpha * neutralLuma;
3026
+ let shift = (preCollapse - nominal) / sizedAlpha * authority * ou.toneAdapt.w;
3027
+ solvedNeutral = clamp(neutral + vec3f(shift), vec3f(0.0), vec3f(1.0));
3028
+ /*
3029
+ * The light attractor needs OPACITY. The light scheme's neutral is
3030
+ * already at white, so an upward shift clamps to nothing \u2014 and the
3031
+ * reference's light-adapted state is the material gone opaque-bright,
3032
+ * the same "an adapting material stops transmitting" the collapse's
3033
+ * alpha half was measured on. Whatever the clamp truncated is carried
3034
+ * by the alpha, solved against the same composite and folded by the
3035
+ * same authority. One-sided by design: darkward opacity is the
3036
+ * collapse's own axis with its own fitted constants.
3037
+ */
3038
+ let solvedLuma = dot(solvedNeutral, vec3f(0.2126, 0.7152, 0.0722));
3039
+ let achieved = (1.0 - sizedAlpha) * ou.toneAnchor.w + sizedAlpha * solvedLuma;
3040
+ if (preCollapse > achieved + 1e-4 && solvedLuma > ou.toneAnchor.w + 1e-3) {
3041
+ let alphaTarget = clamp(
3042
+ (preCollapse - ou.toneAnchor.w) / (solvedLuma - ou.toneAnchor.w),
3043
+ sizedAlpha,
3044
+ 1.0,
3045
+ );
3046
+ solvedAlpha = mix(sizedAlpha, alphaTarget, authority * ou.toneAdapt.w);
3047
+ }
3048
+ }
3049
+ }
3050
+
3051
+ let adaptedAlpha = solvedAlpha + toneAdapt * (1.0 - solvedAlpha);
3052
+ var adapted = solvedNeutral;
2790
3053
  if (toneAdapt > 0.0 && adaptedAlpha > 0.0) {
2791
3054
  adapted =
2792
- (neutral * ((1.0 - toneAdapt) * sizedAlpha) + ou.toneColour.rgb * toneAdapt) / adaptedAlpha;
3055
+ (solvedNeutral * ((1.0 - toneAdapt) * solvedAlpha) + ou.toneColour.rgb * toneAdapt) /
3056
+ adaptedAlpha;
2793
3057
  }
2794
3058
 
2795
- // The author tint. 'aux.w' is the per-pixel strength, unioned in the field pass,
2796
- // so a toolbar can carry one tinted control among plain ones; the seed is a
2797
- // group uniform. At strength 0 this is the identity and the material is the one
2798
- // the calibration bed measures, byte for byte.
2799
- //
2800
- // Apple's mechanism, not a fill: the seed is the middle of a range of tones
2801
- // 'mapped to content brightness underneath'. 'backdrop' is what this pixel is
2802
- // actually looking through \u2014 lens-displaced, LOD-sampled, already the thing the
2803
- // material transmits \u2014 so the tone is taken against the same light the tint is
2804
- // about to be mixed into, and a tinted surface over a dark backdrop settles to a
2805
- // shade of the author's colour rather than sitting on it as paint.
2806
- //
2807
- // That 'backdrop' is also where the size law's scattering has already been
2808
- // applied (above), and that is the right order rather than a coincidence: a
2809
- // large surface diffuses more of what is behind it, so the tone the tint maps
2810
- // to should be the diffused light, not the sharp light nobody sees through it.
2811
- //
2812
- // It displaces the ADAPTED neutral, not the raw one: a full-strength tint is its
2813
- // own adaptation and replaces this axis outright, while a partial one moves with
2814
- // it. That order is the wave's composition contract, in one line.
2815
- var tintColour = adapted;
2816
- if (aux.w > 0.0) {
2817
- let t = smoothstep(ou.tone.z, ou.tone.w, backdropLuma);
2818
- let low = ou.seed.rgb * ou.tone.x;
2819
- let high = ou.seed.rgb + (vec3f(1.0) - ou.seed.rgb) * ou.tone.y;
2820
- // 'seed.w' is the contrast regime's grip on the excursion, never on the hue:
2821
- // at 0 the material shows the author's colour flat and stops responding.
2822
- let tone = mix(ou.seed.rgb, mix(low, high, t), clamp(ou.seed.w, 0.0, 1.0));
2823
- tintColour = mix(adapted, tone, clamp(aux.w, 0.0, 1.0));
3059
+ // The material, untinted: the adapted neutral over what this pixel looks
3060
+ // through, at the material's own occlusion. That alpha is what reduced
3061
+ // transparency lifts and what the size law thickens ("a larger size is more
3062
+ // opaque" is a statement about how much material there is), and the backdrop
3063
+ // adaptation moved it above. The author's colour never touches it \u2014 but not
3064
+ // for the reason the composition contract first gave (claims \xA75.36).
3065
+ var colour = mix(backdrop, adapted, adaptedAlpha);
3066
+ /*
3067
+ * How much of this pixel the SURFACE owns, as the canvas will composite it.
3068
+ *
3069
+ * With a backdrop the composite above is the whole pixel \u2014 the pass sampled
3070
+ * what is behind the surface and mixed it in, so the output is opaque inside
3071
+ * the contour and the page beneath the canvas never shows. With no backdrop
3072
+ * the composite is not here to make: a 'css-backdrop' group's blurred
3073
+ * backdrop is a DOM proxy under this canvas, a 'none' group's is the page
3074
+ * itself, and the browser is the compositor. So the material leaves as a
3075
+ * LAYER \u2014 the adapted colour at the material's own alpha, premultiplied on
3076
+ * the way out \u2014 and every term below that would have shaped the composite
3077
+ * shapes the layer instead, in the form that composites to the same thing.
3078
+ *
3079
+ * Before W11a this path mixed the material over a black backdrop and wrote
3080
+ * it opaque: a nested surface over glass rendered as a flat 0.468 where the
3081
+ * reference reads 0.89 (claims \xA75.38 \xA75). The rows that floored were that
3082
+ * grey. The pair the layer is written at is the host's
3083
+ * ('GroupRenderInput.unsampledMaterial'): the browser composites this canvas
3084
+ * in encoded sRGB, so the alpha is the CSS tier's, not the linear profile's.
3085
+ */
3086
+ var bodyAlpha = 1.0;
3087
+ if (ou.flags.x <= 0.5) {
3088
+ colour = adapted;
3089
+ bodyAlpha = adaptedAlpha;
2824
3090
  }
2825
3091
 
2826
- // The tint layer's ALPHA is untouched by the author's colour. It is the
2827
- // material's occlusion \u2014 what reduced transparency lifts, and the axis the
2828
- // system's own Clear-to-Tinted preference runs on \u2014 and an author choosing a
2829
- // colour does not get to move it.
3092
+ // The author tint (W10). 'aux.w' is the per-pixel strength, unioned in the
3093
+ // field pass, so a toolbar can carry one tinted control among plain ones; the
3094
+ // seed is a group uniform. At strength 0 this is the identity and the material
3095
+ // is the one the calibration bed measures, byte for byte.
2830
3096
  //
2831
- // The size law does move it, and the two do not collide: "a larger size is more
2832
- // opaque, a smaller size is clearer" is a statement about how much material
2833
- // there is, not about what colour it is. A fraction of whatever transparency
2834
- // the resolved alpha still has, for the reason 'increasedOcclusionLift' is a
2835
- // fraction \u2014 see material.ts.
3097
+ // Apple's mechanism, measured per pixel: the tinted material is an OPAQUE
3098
+ // layer of the seed at a shade that is linear in the luminance the untinted
3099
+ // material shows at this pixel \u2014 about half the seed's light over black
3100
+ // content, the seed itself over white, the seed's chromaticity intact
3101
+ // throughout. That layer composites over the material at the AUTHOR's
3102
+ // opacity in the encoded space, which is what a CALayer with 'opacity' does
3103
+ // and how the reference's half-strength cell measures. So a tinted surface
3104
+ // over a checkerboard shows the checker as light and dark ORANGE, not as
3105
+ // orange glass with the checker behind it \u2014 the material's own alpha is not
3106
+ // what a tinted surface shows.
2836
3107
  //
2837
- // The backdrop adaptation moves it, above, and the author's colour does not.
2838
- var colour = mix(backdrop, tintColour, adaptedAlpha);
3108
+ // 'seed.w' is the contrast regime's grip on the excursion, never on the hue;
3109
+ // 'tone.z' is the profile's provenance gate (the dark scheme renders the pure
3110
+ // seed); and '1 \u2212 toneAdapt' folds the shade out where the collapse has made
3111
+ // the material a dark body, where the reference renders the pure seed too.
3112
+ // At zero grip the layer is the bare seed \u2014 the author's colour, flat.
3113
+ if (aux.w > 0.0) {
3114
+ // The untinted material's luminance at this pixel. Over a backdrop that is
3115
+ // the composite; as a layer it is the layer over the tone the host measured
3116
+ // for the group \u2014 zero where nothing was measured, the same reference-level
3117
+ // convention the CSS tier's 'materialLuminance' takes (within its 0.02).
3118
+ let u = bodyAlpha * dot(colour, vec3f(0.2126, 0.7152, 0.0722)) + (1.0 - bodyAlpha) * ou.toneColour.w;
3119
+ let grip = clamp(ou.seed.w, 0.0, 1.0) * clamp(ou.tone.z, 0.0, 1.0) * (1.0 - toneAdapt);
3120
+ let shade = mix(1.0, clamp(mix(ou.tone.x, ou.tone.y, clamp(u, 0.0, 1.0)), 0.0, 1.0), grip);
3121
+ let layer = ou.seed.rgb * shade;
3122
+ let s = clamp(aux.w, 0.0, 1.0);
3123
+ let encodedMaterial = linear_to_srgb(clamp(colour, vec3f(0.0), vec3f(1.0)));
3124
+ let encodedLayer = linear_to_srgb(layer);
3125
+ if (ou.flags.x > 0.5) {
3126
+ colour = srgb_to_linear(mix(encodedMaterial, encodedLayer, vec3f(s)));
3127
+ } else {
3128
+ // The opaque layer at the author's opacity over the material's own layer,
3129
+ // premultiplied: the fold 'tintedCssOptics' makes into one rgba.
3130
+ let premultiplied = (1.0 - s) * bodyAlpha * encodedMaterial + s * encodedLayer;
3131
+ bodyAlpha = 1.0 - (1.0 - s) * (1.0 - bodyAlpha);
3132
+ colour = srgb_to_linear(premultiplied / max(bodyAlpha, 1e-6));
3133
+ }
3134
+ }
2839
3135
 
2840
3136
  /*
2841
3137
  * Everything below is the surface's own APPEARANCE \u2014 the marks that say a
@@ -2861,21 +3157,48 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
2861
3157
  // strongest, which is what makes a thicker surface read as heavier \u2014 and the
2862
3158
  // shadow facet of the size law deepens it further with the span.
2863
3159
  let shadowDepth = ou.light.z * (1.0 + (ou.size.z - 1.0) * sizeK);
2864
- colour = colour * (1.0 - profile * shadowDepth * ou.light.w * present);
3160
+ // A multiplicative occlusion of the whole composite. As a layer that is the
3161
+ // colour scaled and the alpha raised \u2014 (k\xB7a\xB7c, 1 \u2212 k\xB7(1 \u2212 a)) composites to
3162
+ // k times what (a\xB7c, a) would \u2014 and at bodyAlpha 1 it is the plain product.
3163
+ let shadowKeep = 1.0 - profile * shadowDepth * ou.light.w * present;
3164
+ let shadowedAlpha = 1.0 - shadowKeep * (1.0 - bodyAlpha);
3165
+ colour = colour * (shadowKeep * bodyAlpha / max(shadowedAlpha, 1e-6));
3166
+ bodyAlpha = shadowedAlpha;
2865
3167
 
2866
3168
  // Rim and specular from the gradient. The rim is unlit ambient edge brightness;
2867
3169
  // the specular term is the same edge lit from 'light.xy'.
2868
3170
  let rw = rim_weight(d, ou.rim.x);
2869
3171
  let facing = dot(normal, ou.light.xy);
2870
3172
  let spec = pow(clamp(facing, 0.0, 1.0), max(ou.rim.z, 1e-3)) * ou.rim.w;
2871
- colour = colour + vec3f(rw * (ou.rim.y + spec) * present);
3173
+ let rim = rw * (ou.rim.y + spec) * present;
3174
+ if (ou.flags.x > 0.5) {
3175
+ colour = colour + vec3f(rim);
3176
+ } else {
3177
+ // Added light has no premultiplied form of its own \u2014 a canvas colour may
3178
+ // not exceed its alpha \u2014 so the layer carries the light in its opacity:
3179
+ // (a\xB7c + rim, a + rim) composites to dst\xB7(1 \u2212 a) + a\xB7c + rim \u2212 rim\xB7dst,
3180
+ // the additive term short of rim \xD7 dst. Exact wherever the layer is
3181
+ // opaque (an author tint at full strength, or a lit contour pixel whose
3182
+ // alpha the rim fills), and never further off than the rim times what
3183
+ // shows through \u2014 where the mix form ("white over the layer at the rim's
3184
+ // weight") is short by the rim times the whole composite, which halved
3185
+ // the rim on a tinted panel.
3186
+ let carried = clamp(bodyAlpha + rim, 0.0, 1.0);
3187
+ colour = min((colour * bodyAlpha + vec3f(rim)) / max(carried, 1e-6), vec3f(1.0));
3188
+ bodyAlpha = carried;
3189
+ }
2872
3190
 
2873
3191
  // The material over its own shadow over the page, in one premultiplied vector:
2874
- // the colour is the surface's, weighted by its coverage, and the alpha is what
2875
- // the two of them together leave the page \u2014 'coverage' from the surface, and
2876
- // the shadow filling whatever transparency is left.
2877
- let body = encode_output(max(colour, vec3f(0.0)), coverage);
2878
- return vec4f(body.rgb, body.a + shadowAlpha * (1.0 - body.a));
3192
+ // the colour is the surface's, weighted by its coverage and by however much of
3193
+ // the pixel the layer owns, and the alpha is what the two of them together
3194
+ // leave the page. The shadow fills only what the surface's COVERAGE leaves \u2014
3195
+ // clipped out of the silhouette exactly as a 'box-shadow' is clipped out of
3196
+ // its border box \u2014 never what the layer's own transparency leaves: a
3197
+ // translucent surface shows the page through it, not its own shadow. (With
3198
+ // an opaque body the two are the same quantity, which is how the shadow was
3199
+ // first written and why the difference only surfaced with the layer form.)
3200
+ let body = encode_output(max(colour, vec3f(0.0)), coverage * bodyAlpha);
3201
+ return vec4f(body.rgb, body.a + shadowAlpha * (1.0 - coverage));
2879
3202
  }`;
2880
3203
 
2881
3204
  // ../renderer-webgpu/dist/wgsl/prelude.js
@@ -3122,7 +3445,7 @@ function createPassRunner(context) {
3122
3445
  };
3123
3446
  },
3124
3447
  opticsPass(encoder, args) {
3125
- const slot = uniformSlot(`optics:${args.groupId}`, 60);
3448
+ const slot = uniformSlot(`optics:${args.groupId}`, 76);
3126
3449
  const d = slot.data;
3127
3450
  d[0] = args.viewportDevice[0];
3128
3451
  d[1] = args.viewportDevice[1];
@@ -3133,8 +3456,8 @@ function createPassRunner(context) {
3133
3456
  d[6] = args.fit[2];
3134
3457
  d[7] = args.fit[3];
3135
3458
  d[8] = args.refractionScale;
3136
- d[9] = args.bodyLodPerPx;
3137
- d[10] = args.rimLodBias;
3459
+ d[9] = args.lensRefractionGain;
3460
+ d[10] = 0;
3138
3461
  d[11] = args.chainMaxLod;
3139
3462
  d[12] = args.tint[0];
3140
3463
  d[13] = args.tint[1];
@@ -3148,10 +3471,10 @@ function createPassRunner(context) {
3148
3471
  d[21] = args.tintSeed[1];
3149
3472
  d[22] = args.tintSeed[2];
3150
3473
  d[23] = args.tintToneAdaptation;
3151
- d[24] = args.tintTone[0];
3152
- d[25] = args.tintTone[1];
3153
- d[26] = args.tintTone[2];
3154
- d[27] = Math.max(args.tintTone[3], args.tintTone[2] + 1e-4);
3474
+ d[24] = args.tintShade[0];
3475
+ d[25] = args.tintShade[1];
3476
+ d[26] = args.tintShade[2];
3477
+ d[27] = args.sizeFold;
3155
3478
  d[28] = args.rimWidth;
3156
3479
  d[29] = args.rimAlpha;
3157
3480
  d[30] = args.specularPower;
@@ -3184,6 +3507,22 @@ function createPassRunner(context) {
3184
3507
  d[57] = args.outerShadowRectCssHeight;
3185
3508
  d[58] = 0;
3186
3509
  d[59] = 0;
3510
+ d[60] = args.backdropToneAnchorX[0];
3511
+ d[61] = args.backdropToneAnchorX[1];
3512
+ d[62] = args.backdropToneAnchorX[2];
3513
+ d[63] = args.backdropToneLinearMean;
3514
+ d[64] = args.backdropToneResponseThin[0];
3515
+ d[65] = args.backdropToneResponseThin[1];
3516
+ d[66] = args.backdropToneResponseThin[2];
3517
+ d[67] = args.backdropToneResponseStrength;
3518
+ d[68] = args.backdropToneResponseThick[0];
3519
+ d[69] = args.backdropToneResponseThick[1];
3520
+ d[70] = args.backdropToneResponseThick[2];
3521
+ d[71] = 0;
3522
+ d[72] = args.sizeScatterFloor;
3523
+ d[73] = args.sizeScatterSpanMax;
3524
+ d[74] = args.sizeSpanMin;
3525
+ d[75] = args.sizeSpanMax;
3187
3526
  slot.write();
3188
3527
  const chain = args.backdrop?.chain ?? placeholderView;
3189
3528
  const body = args.backdrop?.body ?? placeholderView;
@@ -3240,9 +3579,9 @@ function createPassRunner(context) {
3240
3579
  d[22] = args.backdropTone[2];
3241
3580
  d[23] = args.backdropTone[3];
3242
3581
  d[24] = args.backdropToneLevel;
3243
- d[25] = 0;
3244
- d[26] = 0;
3245
- d[27] = 0;
3582
+ d[25] = args.sizeSpanMin;
3583
+ d[26] = args.sizeSpanMax;
3584
+ d[27] = args.sizeFold;
3246
3585
  const fieldRect = args.fieldRectDevice;
3247
3586
  d[28] = args.rectDevice.width / Math.max(fieldRect.width, 1e-6);
3248
3587
  d[29] = args.rectDevice.height / Math.max(fieldRect.height, 1e-6);
@@ -3433,6 +3772,11 @@ function createRebuildLedger() {
3433
3772
 
3434
3773
  // ../renderer-webgpu/dist/pyramid.js
3435
3774
  var chainUsage = () => GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING;
3775
+ var densityOf = (source, request) => texelsPerCssPx(source.width, source.height, request.placement, request.viewportCss[0], request.viewportCss[1]);
3776
+ var sameDensity = (existing, request) => {
3777
+ const next = densityOf({ width: existing.sourceWidth, height: existing.sourceHeight }, request);
3778
+ return Math.abs(next - existing.texelsPerCss) <= 1e-6 * Math.max(1, existing.texelsPerCss);
3779
+ };
3436
3780
  function createPyramidStore(context) {
3437
3781
  const { device, pool, cache } = context;
3438
3782
  const resources = /* @__PURE__ */ new Map();
@@ -3508,7 +3852,7 @@ function createPyramidStore(context) {
3508
3852
  return target;
3509
3853
  };
3510
3854
  const mipView = (texture, level) => texture.createView({ baseMipLevel: level, mipLevelCount: 1, dimension: "2d" });
3511
- function allocate(sourceId, plan, bodyLevel, sizeEpoch, builtEpoch, bodySigmaTexels) {
3855
+ function allocate(sourceId, plan, bodyLevel, sizeEpoch, builtEpoch, bodySigmaTexels, density) {
3512
3856
  const existing = resources.get(sourceId);
3513
3857
  const bodyWidth = (plan.levels[bodyLevel] ?? plan.levels[0]).width;
3514
3858
  const bodyHeight = (plan.levels[bodyLevel] ?? plan.levels[0]).height;
@@ -3546,7 +3890,10 @@ function createPyramidStore(context) {
3546
3890
  stats,
3547
3891
  sizeEpoch,
3548
3892
  builtEpoch,
3549
- bodySigmaTexels
3893
+ bodySigmaTexels,
3894
+ texelsPerCss: density.texelsPerCss,
3895
+ sourceWidth: density.sourceWidth,
3896
+ sourceHeight: density.sourceHeight
3550
3897
  };
3551
3898
  resources.set(sourceId, next);
3552
3899
  return next;
@@ -3734,7 +4081,7 @@ function createPyramidStore(context) {
3734
4081
  },
3735
4082
  build(request, provider, encoder) {
3736
4083
  const existing = liveResources(request.sourceId);
3737
- if (existing !== void 0 && existing.builtEpoch >= request.epoch && !provider.isDirty()) {
4084
+ if (existing !== void 0 && existing.builtEpoch >= request.epoch && !provider.isDirty() && sameDensity(existing, request)) {
3738
4085
  ledger.recordClean();
3739
4086
  return { status: "clean", resources: existing };
3740
4087
  }
@@ -3752,12 +4099,11 @@ function createPyramidStore(context) {
3752
4099
  }
3753
4100
  pendingRelease.push(provider);
3754
4101
  const plan = planPyramid(frame.width, frame.height, request.resolution);
3755
- const [viewportW, viewportH] = request.viewportCss;
3756
- const cover = viewportW > 0 && viewportH > 0 ? Math.max(frame.width / viewportW, frame.height / viewportH) : 1;
4102
+ const texelsPerCss = densityOf(frame, request);
3757
4103
  const planScale = frame.width > 0 ? plan.width / frame.width : 1;
3758
- const bodySigmaTexels = request.bodySigmaCss * cover * planScale;
4104
+ const bodySigmaTexels = request.bodySigmaCss * texelsPerCss * planScale;
3759
4105
  const bodyPlan = bodyBlurPlan(bodySigmaTexels, plan);
3760
- const target = allocate(request.sourceId, plan, bodyPlan.level, frame.sizeEpoch, request.epoch, bodySigmaTexels);
4106
+ const target = allocate(request.sourceId, plan, bodyPlan.level, frame.sizeEpoch, request.epoch, bodySigmaTexels, { texelsPerCss, sourceWidth: frame.width, sourceHeight: frame.height });
3761
4107
  runImport(encoder, request.sourceId, frame, target.chain);
3762
4108
  runChain(encoder, request.sourceId, plan, target.chain);
3763
4109
  runBodyBlur(encoder, request.sourceId, plan, target.chain, target.body, bodyPlan.level, bodyPlan.residualSigmaTexels);
@@ -3873,8 +4219,11 @@ var RENDERER_PASS_IDS = [
3873
4219
  OPTICS_PASS_ID,
3874
4220
  HIGHLIGHT_PASS_ID
3875
4221
  ];
4222
+ var STANDALONE_RESOLUTION = { scale: 1, maxDimension: 2048 };
3876
4223
  function createWebGPURenderer(options = {}) {
3877
4224
  const providers = /* @__PURE__ */ new Map();
4225
+ const placements = /* @__PURE__ */ new Map();
4226
+ const lastResolution = /* @__PURE__ */ new Map();
3878
4227
  const groups = /* @__PURE__ */ new Map();
3879
4228
  const adaptation = /* @__PURE__ */ new Map();
3880
4229
  const lastReadbackAt = /* @__PURE__ */ new Map();
@@ -3963,15 +4312,30 @@ function createWebGPURenderer(options = {}) {
3963
4312
  }
3964
4313
  return state;
3965
4314
  };
3966
- const coverFit = (sourceWidth, sourceHeight) => {
3967
- const viewportAspect = viewport.heightCss > 0 ? viewport.widthCss / viewport.heightCss : 1;
3968
- const sourceAspect = sourceHeight > 0 ? sourceWidth / sourceHeight : 1;
3969
- if (sourceAspect > viewportAspect) {
3970
- const scaleX = viewportAspect / sourceAspect;
3971
- return [scaleX, 1, (1 - scaleX) / 2, 0];
4315
+ const fitFor = (sourceId, pyramid) => {
4316
+ const placement = placements.get(sourceId);
4317
+ return isUsablePlacement(placement) ? placementFit(placement, viewport.widthCss, viewport.heightCss) : coverFit(pyramid.plan.width, pyramid.plan.height, viewport.widthCss, viewport.heightCss);
4318
+ };
4319
+ const densityRebuilds = (explicit, pyramids) => {
4320
+ const requests = [];
4321
+ const named = new Set(explicit.map((request) => request.sourceId));
4322
+ for (const sourceId of providers.keys()) {
4323
+ if (named.has(sourceId))
4324
+ continue;
4325
+ const existing = pyramids.resources(sourceId);
4326
+ if (existing === void 0)
4327
+ continue;
4328
+ const next = texelsPerCssPx(existing.sourceWidth, existing.sourceHeight, placements.get(sourceId), viewport.widthCss, viewport.heightCss);
4329
+ if (Math.abs(next - existing.texelsPerCss) <= 1e-6 * Math.max(1, existing.texelsPerCss))
4330
+ continue;
4331
+ requests.push({
4332
+ sourceId,
4333
+ epoch: existing.builtEpoch,
4334
+ resolution: lastResolution.get(sourceId) ?? STANDALONE_RESOLUTION,
4335
+ groupIds: [...groups.values()].filter((entry) => entry.input.backdropSourceId === sourceId).map((entry) => entry.input.groupId)
4336
+ });
3972
4337
  }
3973
- const scaleY = sourceAspect / viewportAspect;
3974
- return [1, scaleY, 0, (1 - scaleY) / 2];
4338
+ return requests;
3975
4339
  };
3976
4340
  const unionOf = (input) => input.union ?? DEFAULT_GROUP_UNION;
3977
4341
  const variantOf = (input) => input.variant ?? "regular";
@@ -3987,8 +4351,12 @@ function createWebGPURenderer(options = {}) {
3987
4351
  };
3988
4352
  };
3989
4353
  function rebuildRequests(explicit) {
3990
- if (explicit !== void 0)
3991
- return explicit;
4354
+ const base = explicit ?? standaloneRequests();
4355
+ const { store: pyramids } = ensureContext();
4356
+ const density = densityRebuilds(base, pyramids);
4357
+ return density.length === 0 ? base : [...base, ...density];
4358
+ }
4359
+ function standaloneRequests() {
3992
4360
  const sampled = /* @__PURE__ */ new Set();
3993
4361
  for (const entry of groups.values()) {
3994
4362
  const id = entry.input.backdropSourceId;
@@ -4006,7 +4374,7 @@ function createWebGPURenderer(options = {}) {
4006
4374
  // keeping the books, so a monotonically rising number is the honest
4007
4375
  // stand-in and it keeps `build`'s clean-skip from firing spuriously.
4008
4376
  epoch: framesDrawn + 1,
4009
- resolution: { scale: 1, maxDimension: 2048 },
4377
+ resolution: STANDALONE_RESOLUTION,
4010
4378
  groupIds: [...groups.values()].filter((entry) => entry.input.backdropSourceId === sourceId).map((entry) => entry.input.groupId)
4011
4379
  });
4012
4380
  }
@@ -4024,12 +4392,15 @@ function createWebGPURenderer(options = {}) {
4024
4392
  }
4025
4393
  const variant = variantOf([...groups.values()].find((entry) => entry.input.backdropSourceId === request.sourceId)?.input ?? { });
4026
4394
  const optics = opticsUnderPolicy(material.optics[variant], accessibility, material);
4395
+ lastResolution.set(request.sourceId, request.resolution);
4396
+ const placement = placements.get(request.sourceId);
4027
4397
  const outcome = pyramids.build({
4028
4398
  sourceId: request.sourceId,
4029
4399
  epoch: request.epoch,
4030
4400
  resolution: request.resolution,
4031
4401
  bodySigmaCss: optics.blurSigma,
4032
- viewportCss: [viewport.widthCss, viewport.heightCss]
4402
+ viewportCss: [viewport.widthCss, viewport.heightCss],
4403
+ ...isUsablePlacement(placement) ? { placement } : {}
4033
4404
  }, provider, encoder);
4034
4405
  if (outcome.status === "built")
4035
4406
  built += 1;
@@ -4107,19 +4478,24 @@ function createWebGPURenderer(options = {}) {
4107
4478
  });
4108
4479
  const state = stateOf(input, resolution);
4109
4480
  const variant = variantOf(input);
4110
- const optics = opticsUnderPolicy(material.optics[variant], policy, material);
4481
+ const sourceId = input.backdropSourceId;
4482
+ const pyramid = sourceId === void 0 ? void 0 : pyramids.resources(sourceId);
4483
+ const adapt = sourceId === void 0 ? void 0 : adaptationFor(sourceId).values;
4484
+ const nominalOptics = material.optics[variant];
4485
+ const optics = opticsUnderPolicy(pyramid === void 0 && input.unsampledMaterial !== void 0 ? {
4486
+ ...nominalOptics,
4487
+ tint: input.unsampledMaterial.tint,
4488
+ tintAlpha: input.unsampledMaterial.tintAlpha
4489
+ } : nominalOptics, policy, material);
4111
4490
  const backdropTone = [
4112
4491
  material.backdropToneLow,
4113
4492
  material.backdropToneHigh,
4114
4493
  backdropToneSizeBiasUnderPolicy(policy, material),
4115
4494
  input.backdropTone === void 0 ? 0 : backdropToneUnderPolicy(policy, material) * material.backdropToneMax
4116
4495
  ];
4117
- const backdropToneLevel = input.backdropTone === void 0 ? 0 : relativeLuminance(input.backdropTone);
4496
+ const backdropToneLevel = input.backdropTone === void 0 ? 0 : input.backdropToneLevel ?? relativeLuminance(input.backdropTone);
4118
4497
  const refraction = effectiveRefraction(accessibilityRefractionCap(policy), state.refraction);
4119
4498
  const refractionScale = material.refractionScale[refraction];
4120
- const sourceId = input.backdropSourceId;
4121
- const pyramid = sourceId === void 0 ? void 0 : pyramids.resources(sourceId);
4122
- const adapt = sourceId === void 0 ? void 0 : adaptationFor(sourceId).values;
4123
4499
  passes.opticsPass(encoder, {
4124
4500
  groupId: input.groupId,
4125
4501
  target: active.optics,
@@ -4129,10 +4505,9 @@ function createWebGPURenderer(options = {}) {
4129
4505
  viewportDevice,
4130
4506
  cssPerDevice,
4131
4507
  coverageRampCss: cssPerDevice,
4132
- fit: pyramid === void 0 ? [1, 1, 0, 0] : coverFit(pyramid.plan.width, pyramid.plan.height),
4508
+ fit: pyramid === void 0 || sourceId === void 0 ? [1, 1, 0, 0] : fitFor(sourceId, pyramid),
4133
4509
  refractionScale,
4134
- bodyLodPerPx: material.lensBodyLodPerPx,
4135
- rimLodBias: material.lensRimLodBias,
4510
+ lensRefractionGain: material.lensRefractionGain,
4136
4511
  chainMaxLod: pyramid?.plan.maxLod ?? 0,
4137
4512
  tint: optics.tint,
4138
4513
  tintAlpha: optics.tintAlpha,
@@ -4152,12 +4527,7 @@ function createWebGPURenderer(options = {}) {
4152
4527
  */
4153
4528
  tintSeed: (policy.glass === "none" ? void 0 : groupTintSeed(input)) ?? optics.tint,
4154
4529
  tintToneAdaptation: tintToneAdaptation(policy, material),
4155
- tintTone: [
4156
- material.tintToneFloor,
4157
- material.tintToneCeilMix,
4158
- material.tintToneLow,
4159
- material.tintToneHigh
4160
- ],
4530
+ tintShade: [material.tintShadeDark, material.tintShadeLight, material.tintShadeStrength],
4161
4531
  rimWidth: optics.rimWidth,
4162
4532
  rimAlpha: optics.rimAlpha,
4163
4533
  specularPower: optics.specularPower,
@@ -4174,6 +4544,14 @@ function createWebGPURenderer(options = {}) {
4174
4544
  sizeOcclusionGain: material.sizeOcclusionGain,
4175
4545
  sizeShadowGainMax: material.sizeShadowGainMax,
4176
4546
  bodyChainLod: chainLodForSigma(pyramid?.bodySigmaTexels ?? 0),
4547
+ // The size law's curves (W11c): the shader evaluates the thickness and
4548
+ // the scatter mix per pixel from the span the field carries, under the
4549
+ // same fold `sizeThicknessUnderPolicy` applies on the CPU.
4550
+ sizeSpanMin: material.sizeSpanMin,
4551
+ sizeSpanMax: material.sizeSpanMax,
4552
+ sizeScatterFloor: material.sizeScatterFloor,
4553
+ sizeScatterSpanMax: material.sizeScatterSpanMax,
4554
+ sizeFold: material.refractionScale[accessibilityRefractionCap(policy)],
4177
4555
  backdropTone,
4178
4556
  backdropToneColour: [
4179
4557
  input.backdropTone?.[0] ?? 0,
@@ -4181,6 +4559,25 @@ function createWebGPURenderer(options = {}) {
4181
4559
  input.backdropTone?.[2] ?? 0,
4182
4560
  backdropToneLevel
4183
4561
  ],
4562
+ // The response law's anchors (W9) and the linear backdrop mean its
4563
+ // solve composites against. The shader re-derives the encoded input
4564
+ // from `backdropToneLevel`, so the fourth slot carries the linear
4565
+ // mean — the one quantity the solve needs that the tone colour does
4566
+ // not already hold.
4567
+ backdropToneAnchorX: material.backdropToneAnchorX,
4568
+ backdropToneResponseThin: material.backdropToneResponseThin,
4569
+ backdropToneResponseThick: material.backdropToneResponseThick,
4570
+ // The response law rides only the UN-DEGRADED regime, for the same
4571
+ // provenance reason the dark profiles set its strength to 0: the
4572
+ // anchors are standard-light-reference measurements, and the
4573
+ // accessibility references behave differently (nearly opaque, flat in
4574
+ // span — W2). Measured before this gate existed: reduced-transparency
4575
+ // photo ΔE p95 0.007 → 0.048 and cross-tier ratio 0.99 → 0.86. Where
4576
+ // any policy fold touches the tone axis, the solve stands down and the
4577
+ // collapse alone runs, exactly the pre-W9 behaviour those profiles
4578
+ // were fitted on.
4579
+ backdropToneResponseStrength: backdropToneUnderPolicy(policy, material) >= 0.999 ? material.backdropToneResponseStrength : 0,
4580
+ backdropToneLinearMean: input.backdropToneLinearLuminance ?? backdropToneLevel,
4184
4581
  outerShadow: [
4185
4582
  outerShadowAlpha(shadow.occlusion),
4186
4583
  shadow.sigmaPx,
@@ -4219,9 +4616,13 @@ function createWebGPURenderer(options = {}) {
4219
4616
  glowGain: material.glowGain,
4220
4617
  colour: optics.highlight,
4221
4618
  // The same four numbers the optics pass took, so the two passes fade
4222
- // the surface on one curve rather than on two copies of it.
4619
+ // the surface on one curve rather than on two copies of it — and the
4620
+ // same band and fold, since the curve is evaluated off the span.
4223
4621
  backdropTone,
4224
- backdropToneLevel
4622
+ backdropToneLevel,
4623
+ sizeSpanMin: material.sizeSpanMin,
4624
+ sizeSpanMax: material.sizeSpanMax,
4625
+ sizeFold: material.refractionScale[accessibilityRefractionCap(policy)]
4225
4626
  });
4226
4627
  }
4227
4628
  drawn += 1;
@@ -4286,6 +4687,8 @@ function createWebGPURenderer(options = {}) {
4286
4687
  providers.set(provider.id, provider);
4287
4688
  },
4288
4689
  unregisterBackdrop(sourceId) {
4690
+ placements.delete(sourceId);
4691
+ lastResolution.delete(sourceId);
4289
4692
  const provider = providers.get(sourceId);
4290
4693
  if (provider === void 0)
4291
4694
  return;
@@ -4298,6 +4701,17 @@ function createWebGPURenderer(options = {}) {
4298
4701
  backdrop(sourceId) {
4299
4702
  return providers.get(sourceId);
4300
4703
  },
4704
+ setBackdropPlacement(sourceId, placement) {
4705
+ if (samePlacement(placements.get(sourceId), placement))
4706
+ return;
4707
+ if (placement === void 0)
4708
+ placements.delete(sourceId);
4709
+ else
4710
+ placements.set(sourceId, { ...placement });
4711
+ },
4712
+ backdropPlacement(sourceId) {
4713
+ return placements.get(sourceId);
4714
+ },
4301
4715
  setViewport(next) {
4302
4716
  const changed = next.widthCss !== viewport.widthCss || next.heightCss !== viewport.heightCss || next.devicePixelRatio !== viewport.devicePixelRatio;
4303
4717
  viewport = next;
@@ -4454,6 +4868,6 @@ function createWebGPURenderer(options = {}) {
4454
4868
  };
4455
4869
  }
4456
4870
 
4457
- export { ADAPTIVE_LUMINANCE_HIGH, ADAPTIVE_LUMINANCE_LOW, ADAPTIVE_TINT_DARK, ADAPTIVE_TINT_LIGHT, ANALYSIS_DISPATCH, ANALYSIS_GRID, ANALYSIS_PASS_ID, ANALYSIS_STATS_FLOATS, ANALYSIS_TARGET_EXTENT, ANALYSIS_WORKGROUP, BACKDROP_ALPHA_MODES, BACKDROP_COLOR_SPACES, BACKDROP_KINDS, BACKDROP_PASS_ID, BACKDROP_TONE_HIGH, BACKDROP_TONE_LOW, BACKDROP_TONE_MAX, BACKDROP_TONE_SIZE_BIAS, CANVAS_FORMAT, CHAIN_SIGMA_AT_LEVEL_1, CROSS_CHECK_SHAPE_FLOATS, CROSS_CHECK_WORKGROUP, DEFAULT_MATERIAL_PROFILE, FAMILY_C_CROSS_CHECK, FIELD_PASS_ID, GOVERNOR_LADDER, HIGHLIGHT_PASS_ID, IDLE_CHANNELS, INCREASED_OCCLUSION_LIFT, INSTANCE_BYTES, INSTANCE_FLOATS, LENS_BODY_LOD_PER_PX, LENS_RIM_LOD_BIAS, LENS_SIZE_GAIN_MAX, LUMINANCE_WEIGHTS, MATERIAL_OPTICS, MATERIAL_VARIANTS, MAX_CHAIN_LEVELS, MIN_LEVEL_EXTENT, NOMINAL_GOVERNOR, NOMINAL_MATERIAL_POLICY, OPTICS_PASS_ID, OUTER_SHADOW, OUTPUT_TEXTURE_FORMAT, PASS_LABEL, REFRACTION_LADDER, REFRACTION_SCALE, RENDERER_ERROR_CODES, RENDERER_PASS_IDS, RendererError, SIZE_OCCLUSION_GAIN, SIZE_SCATTER_GAIN_MAX, SIZE_SHADOW_GAIN_MAX, SIZE_SPAN_MAX, SIZE_SPAN_MIN, SRGB_ENCODING_EXPONENT, SUPPORTED_APP_TEXTURE_FORMATS, WGSL_ANALYSIS_PASS, WGSL_CROSS_CHECK_PASS, WGSL_DOWNSAMPLE_PASS, WGSL_FIELD_KERNELS, WGSL_FIELD_PASS, WGSL_FIELD_SAMPLE, WGSL_HIGHLIGHT_PASS, WGSL_IMPORT_PASS, WGSL_INSTANCE_STRUCT, WGSL_OPTICS_PASS, WGSL_PRELUDE, WGSL_RSUPN_GRAD, WGSL_RSUP_GRAD, WGSL_SMOOTH_UNION, WORKING_TEXTURE_FORMAT, ZERO_STATS, accessibilityRefractionCap, adaptationStrength, adaptedTintAlpha, adaptedTintColour, adaptiveTint, allShaderSource, alphaNormalisationMode, analysisModule, backdropToneAdaptation, backdropToneSizeBiasUnderPolicy, backdropToneUnderPolicy, bodyBlurPlan, bodyLod, chainModule, clipFieldRectToCanvas, createAdaptationState, createAppTextureProvider, createCopyProvider, createDeviceHost, createGovernor, createGpuContext, createGradientProvider, createPassRunner, createPipelineCache, createPyramidStore, createRebuildLedger, createStorageSlot, createTexturePool, createTimingCollector, createUniformSlot, createVideoProvider, createWebGPURenderer, crossCheckKernelModule, displayP3ToSrgbLinear, effectiveRefraction, encodeOutput, encodeOutputBytes, fieldModule, fieldPassSource, groupFieldRect, highlightModule, importColorMatrix, importModule, importPassSource, lensDepthPx, lensSizeGain, lensSizeGainFromThickness, linearGradientStops, linearToSrgb, linearToSrgbChannel, occlusionAlphaUnderPolicy, opticsModule, opticsUnderPolicy, outerShadowAlpha, outerShadowFalloff, outerShadowReachPx, outerShadowUnderPolicy, packInstances, pipelineKey, planPyramid, poolKey, readbackDue, refractionRank, relativeLuminance, rendererError, resolveSurfaces, sizeOcclusionAlpha, sizeOcclusionAlphaAt, sizeOuterShadowOcclusion, sizeOuterShadowOcclusionAt, sizeScatterSigma, sizeScatterSigmaAt, sizeShadowDepth, sizeShadowDepthAt, sizeThickness, sizeThicknessUnderPolicy, snapRectToDevicePixels, srgbToLinear, srgbToLinearChannel, statsFromBuffer, supportsTimestamps, tintTone, tintToneAdaptation, tintedTintColour, validateAppTexture, withMaterialOverrides };
4458
- //# sourceMappingURL=dist-RQ4ZMA3D.js.map
4459
- //# sourceMappingURL=dist-RQ4ZMA3D.js.map
4871
+ export { ADAPTIVE_LUMINANCE_HIGH, ADAPTIVE_LUMINANCE_LOW, ADAPTIVE_TINT_DARK, ADAPTIVE_TINT_LIGHT, ANALYSIS_DISPATCH, ANALYSIS_GRID, ANALYSIS_PASS_ID, ANALYSIS_STATS_FLOATS, ANALYSIS_TARGET_EXTENT, ANALYSIS_WORKGROUP, BACKDROP_ALPHA_MODES, BACKDROP_COLOR_SPACES, BACKDROP_KINDS, BACKDROP_PASS_ID, BACKDROP_TONE_HIGH, BACKDROP_TONE_LOW, BACKDROP_TONE_MAX, BACKDROP_TONE_SIZE_BIAS, CANVAS_FORMAT, CHAIN_SIGMA_AT_LEVEL_1, CROSS_CHECK_SHAPE_FLOATS, CROSS_CHECK_WORKGROUP, DEFAULT_MATERIAL_PROFILE, FAMILY_C_CROSS_CHECK, FIELD_PASS_ID, GOVERNOR_LADDER, HIGHLIGHT_PASS_ID, IDENTITY_FIT, IDLE_CHANNELS, INCREASED_OCCLUSION_LIFT, INSTANCE_BYTES, INSTANCE_FLOATS, LENS_REFRACTION_GAIN, LENS_SIZE_GAIN_MAX, LUMINANCE_WEIGHTS, MATERIAL_OPTICS, MATERIAL_VARIANTS, MAX_CHAIN_LEVELS, MIN_LEVEL_EXTENT, NOMINAL_GOVERNOR, NOMINAL_MATERIAL_POLICY, OPTICS_PASS_ID, OUTER_SHADOW, OUTPUT_TEXTURE_FORMAT, PASS_LABEL, REFRACTION_LADDER, REFRACTION_SCALE, RENDERER_ERROR_CODES, RENDERER_PASS_IDS, RendererError, SIZE_OCCLUSION_GAIN, SIZE_SCATTER_FLOOR, SIZE_SCATTER_GAIN_MAX, SIZE_SCATTER_SPAN_MAX, SIZE_SHADOW_GAIN_MAX, SIZE_SPAN_MAX, SIZE_SPAN_MIN, SRGB_ENCODING_EXPONENT, SUPPORTED_APP_TEXTURE_FORMATS, WGSL_ANALYSIS_PASS, WGSL_CROSS_CHECK_PASS, WGSL_DOWNSAMPLE_PASS, WGSL_FIELD_KERNELS, WGSL_FIELD_PASS, WGSL_FIELD_SAMPLE, WGSL_HIGHLIGHT_PASS, WGSL_IMPORT_PASS, WGSL_INSTANCE_STRUCT, WGSL_OPTICS_PASS, WGSL_PRELUDE, WGSL_RSUPN_GRAD, WGSL_RSUP_GRAD, WGSL_SMOOTH_UNION, WORKING_TEXTURE_FORMAT, ZERO_STATS, accessibilityRefractionCap, adaptationStrength, adaptedTintAlpha, adaptedTintColour, adaptiveTint, allShaderSource, alphaNormalisationMode, analysisModule, backdropFit, backdropToneAdaptation, backdropToneSizeBiasUnderPolicy, backdropToneUnderPolicy, bodyBlurPlan, chainModule, clipFieldRectToCanvas, coverFit, createAdaptationState, createAppTextureProvider, createCopyProvider, createDeviceHost, createGovernor, createGpuContext, createGradientProvider, createPassRunner, createPipelineCache, createPyramidStore, createRebuildLedger, createStorageSlot, createTexturePool, createTimingCollector, createUniformSlot, createVideoProvider, createWebGPURenderer, crossCheckKernelModule, displayP3ToSrgbLinear, effectiveRefraction, encodeOutput, encodeOutputBytes, fieldModule, fieldPassSource, groupFieldRect, highlightModule, importColorMatrix, importModule, importPassSource, isUsablePlacement, lensDepthPx, lensDisplacementPx, lensSizeGain, lensSizeGainFromThickness, linearGradientStops, linearToSrgb, linearToSrgbChannel, occlusionAlphaUnderPolicy, opticsModule, opticsUnderPolicy, outerShadowAlpha, outerShadowFalloff, outerShadowReachPx, outerShadowUnderPolicy, packInstances, pipelineKey, placementFit, planPyramid, poolKey, readbackDue, refractionRank, relativeLuminance, rendererError, resolveSurfaces, samePlacement, scatterThickness, sizeOcclusionAlpha, sizeOcclusionAlphaAt, sizeOuterShadowOcclusion, sizeOuterShadowOcclusionAt, sizeScatterSigma, sizeScatterSigmaAt, sizeShadowDepth, sizeShadowDepthAt, sizeThickness, sizeThicknessUnderPolicy, snapRectToDevicePixels, srgbToLinear, srgbToLinearChannel, statsFromBuffer, supportsTimestamps, texelsPerCssPx, tintShade, tintShadeLayer, tintToneAdaptation, tintedMaterialColour, validateAppTexture, withMaterialOverrides };
4872
+ //# sourceMappingURL=dist-MVSXUP3K.js.map
4873
+ //# sourceMappingURL=dist-MVSXUP3K.js.map