@vitreajs/vitrea 0.3.0 → 0.5.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.
@@ -348,6 +348,8 @@ var DEFAULT_MATERIAL_PROFILE = {
348
348
  */
349
349
  sizeSpanMin: 32,
350
350
  sizeSpanMax: 96,
351
+ // The inner shadow's depth gain since W12 G2 (the lens reads its own law
352
+ // below); the value is W2's, unchanged.
351
353
  lensSizeGainMax: 2.6,
352
354
  /*
353
355
  * MEASURED (W2), each with its own status — see the fields' notes.
@@ -431,16 +433,79 @@ var DEFAULT_MATERIAL_PROFILE = {
431
433
  sizeScatterGainMax: 8,
432
434
  sizeScatterFloor: 0.4,
433
435
  sizeScatterSpanMax: 256,
436
+ // The second scale, FITTED by W15 G1's runtime sweep at dpr 2 and landed
437
+ // (claims §5.70 §2 and §8): the gain 4.8 — a heavy width of 6 device px, the
438
+ // sweep's own interior minimum and narrower than G0's Gaussian estimate — the
439
+ // deep value fully heavy, and the span top left at the 1x value because a
440
+ // floor of 1 leaves it nothing to rise to. Read only above dpr 1, so the 1x
441
+ // material is byte-identical to the W13 bed (see
442
+ // `MaterialProfile.sizeScatterFloor2x`).
443
+ sizeScatterGainMax2x: 4.8,
444
+ sizeScatterFloor2x: 1,
445
+ sizeScatterSpanMax2x: 256,
446
+ // The 2x gain's own span grading (W15 G1's re-form, claims §5.70 §8; W15
447
+ // Decision Log 3), set from G0's independent per-span reading — 8 device px
448
+ // at span 96 and 11 at 160 — and not from the holdout row it fixes.
449
+ sizeScatterGainFar2x: 9.9,
450
+ // The 1x three FITTED in the renderer (W13 G1's third sweep, 44 points over
451
+ // the calibration bed: `results/2026-09-03-w13-ramp/g1/sweep-3/g1-sweep-3.md`
452
+ // §3); the 2x three PROVISIONAL still, because at them the excursion is
453
+ // bit-exactly zero on the whole bed and a sweep cannot fit what does not
454
+ // move (§4). The thin anchor 0.72 is above G0's read-off of 0.637–0.642: the
455
+ // grid runs 0.60 / 0.64 / 0.68 and the refinement 0.72 / 0.76, and 0.72 is an
456
+ // interior optimum of both the interior objective and the interior gap. The
457
+ // thick anchor 0.52 is G0's `rrect-md` reading and is above that cell's deep
458
+ // sharp share of 0.481, where the joint thick fit of 0.47 would have clamped
459
+ // it to nothing. The 2x three are G0's readings, LANDED by W15 G1 (claims
460
+ // §5.70 §2): thin is G0's u 6 reading and stage 2b's best, thick and far are
461
+ // one number because G0 read the 2x start flat across the thick spans, and
462
+ // with the deep value now fully heavy at that ratio the ramp is the whole
463
+ // body above it rather than the null W13 recorded. See
464
+ // `MaterialProfile.sizeScatterRampStartThin1x`.
465
+ sizeScatterRampStartThin1x: 0.72,
466
+ sizeScatterRampStartThick1x: 0.52,
467
+ sizeScatterRampStartFar1x: 0.2,
468
+ sizeScatterRampStartThin2x: 0.46,
469
+ sizeScatterRampStartThick2x: 0.21,
470
+ sizeScatterRampStartFar2x: 0.21,
471
+ sizeScatterRampReach1xPx: 80,
472
+ sizeScatterRampReach2xPx: 100,
434
473
  sizeOcclusionGain: 0.05,
435
474
  sizeShadowGainMax: 1,
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,
475
+ /*
476
+ * The lens (W12 G2, claims §5.51; Decision Log 3 of the W12 spec).
477
+ *
478
+ * MEASURED from the reference's own layer tree (§5.50): the inner refraction
479
+ * amount and height laws, verified on spans 32 / 44 / 48 … 112 / 128 / 160
480
+ * and the nested base at 130, and the ovalization's knee (0 through span 64,
481
+ * 0.5 from 72). FITTED on the pixels (§5.51, a 2-D band renderer validated on
482
+ * this renderer's own capture): the gain, the extent and the exponent, on
483
+ * `rrect-md` + `-ml` at 1x pitches 16 and 32 with `rrect-lg` held out at both
484
+ * scales; the ovalization's effective value on the box-inscribed ellipse.
485
+ * Predicted before the landing capture: checkerboard SSIM 1x md / ml / lg
486
+ * 0.954 / 0.931 / 0.929 → 0.970 / 0.946 / 0.942, 2x 0.939 / 0.902 / 0.901 →
487
+ * 0.950 / 0.918 / 0.918, the capsule 0.977 → 0.985.
488
+ *
489
+ * What W11c G2 recorded here — 1.6 lens depths on the square profile, fitted
490
+ * per depth shell — was the band's mean, not its shape (§5.48, §5.49); it is
491
+ * kept in git history and in claims §5.43.
492
+ */
493
+ lensRefractionGain: 0.745,
494
+ lensHeightPerSpan: 0.25,
495
+ lensHeightMax: 20,
496
+ lensAmountPerSpan: 0.8,
497
+ lensAmountMax: 60,
498
+ lensThicknessReference: 8,
499
+ lensExtentGain: 1.337,
500
+ lensProfileExponent: 3.69,
501
+ // 0.8 by eye (W12 Decision Log 6). The pixels preferred 0.6 by a hair — every
502
+ // texture row that can see ω scores 0.001–0.002 lower at 0.8 (claims §5.54) —
503
+ // but the field's measured tilt sits at 0.8–1.0 (§5.49 §3) and the user read
504
+ // the 0.8 sheet as much closer to macOS; the eye overrides a margin that small,
505
+ // and §5.54 §1's rows are the recorded cost.
506
+ lensOvalization: 0.8,
507
+ lensOvalizationSpanMin: 64,
508
+ lensOvalizationSpanMax: 72,
444
509
  reducedTransparencyFrost: 1.75,
445
510
  increasedOcclusionLift: INCREASED_OCCLUSION_LIFT,
446
511
  strongBorderRim: { rimWidth: 2, rimAlpha: 0.95 },
@@ -568,14 +633,18 @@ var DEFAULT_MATERIAL_PROFILE = {
568
633
  * instrument's independent shadow axis agrees, so the geometry stands on those
569
634
  * two and this objective is not asked to re-decide it.
570
635
  *
571
- * `reducedTransparencyOcclusion` 0.70 is sharp where the amplitude is flat: a
572
- * 3.83× spread over 0.45…0.95 with a clear minimum. It also reconciles two
636
+ * `reducedTransparencyOcclusion` 0.70 was sharp where the amplitude was flat: a
637
+ * 3.83× spread over 0.45…0.95 with a clear minimum. It also reconciled two
573
638
  * routes — 0.285 × 0.70 = 0.1995 against the reference's directly measured
574
639
  * reduce-transparency amplitude of 0.203 at a 44 px span. W8's 0.566 was the
575
- * ratio of the reference's two amplitudes; this is the ratio that makes
640
+ * ratio of the reference's two amplitudes; that was the ratio that made
576
641
  * vitrea's shadow match the reference's under the preference, and the two
577
- * differ because the base amplitude is a compromise across scenes whose spread
578
- * is much wider in the standard profile than under reduced transparency.
642
+ * differed because the base amplitude is a compromise across scenes whose
643
+ * spread is much wider in the standard profile than under reduced transparency.
644
+ * **W14 G1 re-forms the constant as the LEVEL those routes were reconciling
645
+ * to** — 0.197 absolute, replacing both regimes rather than scaling them — for
646
+ * the reason its doc comment gives: a ratio and a level stopped being the same
647
+ * thing when the single amplitude became six unequal anchors.
579
648
  *
580
649
  * Extracted from the active bed's native fixtures directly —
581
650
  * `results/2026-08-31-active-bed-stage0.json` measured the gap, these numbers
@@ -614,13 +683,33 @@ var DEFAULT_MATERIAL_PROFILE = {
614
683
  * 44 px span — the dark material's shadow is nearly invisible) and lands as a
615
684
  * profile patch, not as a branch here. Both are stated in the claims doc as the
616
685
  * open question the cascade's fit inherits.
686
+ *
687
+ * **W14 G1 answers that open question and replaces `occlusion`.** "Over the
688
+ * flat near-white `light-solid` backdrop the same fit reads 0.123 rather than
689
+ * 0.33 … it is not a function of the backdrop's luminance" was read on a bed
690
+ * that had no backdrop between `hc-text` (linear 0.74) and `light-solid`
691
+ * (0.891): it IS a function of the backdrop's luminance, and the whole factor
692
+ * of 2.6 happens inside the gap the bed cannot see (claims §5.62 §5). The
693
+ * single amplitude becomes six anchors on two regimes — three in backdrop
694
+ * luminance below the knee, three in span above it — plus the lift's four
695
+ * constants. The three lengths are untouched; G0 re-read them free on both
696
+ * terms and they came back at W8's values.
617
697
  */
618
698
  outerShadow: {
619
699
  offsetPx: 7.95,
620
700
  sigmaPx: 15.55,
621
701
  spreadPx: 3.1,
622
- occlusion: 0.285,
623
- reducedTransparencyOcclusion: 0.7,
702
+ thinOcclusionDark: 0,
703
+ thinOcclusionMid: 0.33,
704
+ thinOcclusionBright: 0.127,
705
+ thickOcclusionAt96: 0.37,
706
+ thickOcclusionAt128: 0.448,
707
+ thickOcclusionAt160: 0.479,
708
+ liftAmplitude: 0.01,
709
+ liftSpanMin: 64,
710
+ liftSpanFull: 118,
711
+ liftBlurSigmaCss: 40,
712
+ reducedTransparencyOcclusion: 0.197,
624
713
  sizeGain: 0
625
714
  },
626
715
  lightDirection: [-0.3714, -0.9285],
@@ -641,9 +730,29 @@ var LENS_SIZE_GAIN_MAX = DEFAULT_MATERIAL_PROFILE.lensSizeGainMax;
641
730
  var SIZE_SCATTER_GAIN_MAX = DEFAULT_MATERIAL_PROFILE.sizeScatterGainMax;
642
731
  var SIZE_SCATTER_FLOOR = DEFAULT_MATERIAL_PROFILE.sizeScatterFloor;
643
732
  var SIZE_SCATTER_SPAN_MAX = DEFAULT_MATERIAL_PROFILE.sizeScatterSpanMax;
733
+ var SIZE_SCATTER_GAIN_MAX_2X = DEFAULT_MATERIAL_PROFILE.sizeScatterGainMax2x;
734
+ var SIZE_SCATTER_GAIN_FAR_2X = DEFAULT_MATERIAL_PROFILE.sizeScatterGainFar2x;
735
+ var SIZE_SCATTER_FLOOR_2X = DEFAULT_MATERIAL_PROFILE.sizeScatterFloor2x;
736
+ var SIZE_SCATTER_SPAN_MAX_2X = DEFAULT_MATERIAL_PROFILE.sizeScatterSpanMax2x;
737
+ var SIZE_SCATTER_RAMP_START_THIN_1X = DEFAULT_MATERIAL_PROFILE.sizeScatterRampStartThin1x;
738
+ var SIZE_SCATTER_RAMP_START_THICK_1X = DEFAULT_MATERIAL_PROFILE.sizeScatterRampStartThick1x;
739
+ var SIZE_SCATTER_RAMP_START_THIN_2X = DEFAULT_MATERIAL_PROFILE.sizeScatterRampStartThin2x;
740
+ var SIZE_SCATTER_RAMP_START_THICK_2X = DEFAULT_MATERIAL_PROFILE.sizeScatterRampStartThick2x;
741
+ var SIZE_SCATTER_RAMP_START_FAR_1X = DEFAULT_MATERIAL_PROFILE.sizeScatterRampStartFar1x;
742
+ var SIZE_SCATTER_RAMP_START_FAR_2X = DEFAULT_MATERIAL_PROFILE.sizeScatterRampStartFar2x;
743
+ var SIZE_SCATTER_RAMP_REACH_1X_PX = DEFAULT_MATERIAL_PROFILE.sizeScatterRampReach1xPx;
744
+ var SIZE_SCATTER_RAMP_REACH_2X_PX = DEFAULT_MATERIAL_PROFILE.sizeScatterRampReach2xPx;
644
745
  var SIZE_OCCLUSION_GAIN = DEFAULT_MATERIAL_PROFILE.sizeOcclusionGain;
645
746
  var SIZE_SHADOW_GAIN_MAX = DEFAULT_MATERIAL_PROFILE.sizeShadowGainMax;
646
747
  var LENS_REFRACTION_GAIN = DEFAULT_MATERIAL_PROFILE.lensRefractionGain;
748
+ var LENS_HEIGHT_PER_SPAN = DEFAULT_MATERIAL_PROFILE.lensHeightPerSpan;
749
+ var LENS_HEIGHT_MAX = DEFAULT_MATERIAL_PROFILE.lensHeightMax;
750
+ var LENS_AMOUNT_PER_SPAN = DEFAULT_MATERIAL_PROFILE.lensAmountPerSpan;
751
+ var LENS_AMOUNT_MAX = DEFAULT_MATERIAL_PROFILE.lensAmountMax;
752
+ var LENS_THICKNESS_REFERENCE = DEFAULT_MATERIAL_PROFILE.lensThicknessReference;
753
+ var LENS_EXTENT_GAIN = DEFAULT_MATERIAL_PROFILE.lensExtentGain;
754
+ var LENS_PROFILE_EXPONENT = DEFAULT_MATERIAL_PROFILE.lensProfileExponent;
755
+ var LENS_OVALIZATION = DEFAULT_MATERIAL_PROFILE.lensOvalization;
647
756
  var BACKDROP_TONE_MAX = DEFAULT_MATERIAL_PROFILE.backdropToneMax;
648
757
  var BACKDROP_TONE_LOW = DEFAULT_MATERIAL_PROFILE.backdropToneLow;
649
758
  var BACKDROP_TONE_HIGH = DEFAULT_MATERIAL_PROFILE.backdropToneHigh;
@@ -653,7 +762,20 @@ DEFAULT_MATERIAL_PROFILE.backdropToneResponseThin;
653
762
  DEFAULT_MATERIAL_PROFILE.backdropToneResponseThick;
654
763
  DEFAULT_MATERIAL_PROFILE.backdropToneResponseStrength;
655
764
  var OUTER_SHADOW = DEFAULT_MATERIAL_PROFILE.outerShadow;
765
+ var RETIRED_OUTER_SHADOW_LEAVES = {
766
+ occlusion: "the six amplitude anchors (thinOcclusionDark, thinOcclusionMid, thinOcclusionBright, thickOcclusionAt96, thickOcclusionAt128, thickOcclusionAt160) and liftAmplitude for the second term"
767
+ };
768
+ function rejectRetiredOuterShadowLeaves(patch) {
769
+ if (patch === void 0)
770
+ return;
771
+ for (const [leaf, replacement] of Object.entries(RETIRED_OUTER_SHADOW_LEAVES)) {
772
+ if (!(leaf in patch))
773
+ continue;
774
+ throw new Error(`outerShadow.${leaf} was retired by W14 G1 (claims \xA75.62) and is replaced by ${replacement}. Applying this patch would have rendered the default shadow while recording itself as configured. It is refused rather than mapped: a single span-flat amplitude is the material the measurement retired.`);
775
+ }
776
+ }
656
777
  function withMaterialOverrides(base, patch) {
778
+ rejectRetiredOuterShadowLeaves(patch.outerShadow);
657
779
  const optics = {};
658
780
  for (const variant of MATERIAL_VARIANTS) {
659
781
  optics[variant] = { ...base.optics[variant], ...patch.optics?.[variant] };
@@ -675,9 +797,31 @@ function withMaterialOverrides(base, patch) {
675
797
  sizeScatterGainMax: patch.sizeScatterGainMax ?? base.sizeScatterGainMax,
676
798
  sizeScatterFloor: patch.sizeScatterFloor ?? base.sizeScatterFloor,
677
799
  sizeScatterSpanMax: patch.sizeScatterSpanMax ?? base.sizeScatterSpanMax,
800
+ sizeScatterGainMax2x: patch.sizeScatterGainMax2x ?? base.sizeScatterGainMax2x,
801
+ sizeScatterFloor2x: patch.sizeScatterFloor2x ?? base.sizeScatterFloor2x,
802
+ sizeScatterSpanMax2x: patch.sizeScatterSpanMax2x ?? base.sizeScatterSpanMax2x,
803
+ sizeScatterGainFar2x: patch.sizeScatterGainFar2x ?? base.sizeScatterGainFar2x,
804
+ sizeScatterRampStartThin1x: patch.sizeScatterRampStartThin1x ?? base.sizeScatterRampStartThin1x,
805
+ sizeScatterRampStartThick1x: patch.sizeScatterRampStartThick1x ?? base.sizeScatterRampStartThick1x,
806
+ sizeScatterRampStartThin2x: patch.sizeScatterRampStartThin2x ?? base.sizeScatterRampStartThin2x,
807
+ sizeScatterRampStartThick2x: patch.sizeScatterRampStartThick2x ?? base.sizeScatterRampStartThick2x,
808
+ sizeScatterRampStartFar1x: patch.sizeScatterRampStartFar1x ?? base.sizeScatterRampStartFar1x,
809
+ sizeScatterRampStartFar2x: patch.sizeScatterRampStartFar2x ?? base.sizeScatterRampStartFar2x,
810
+ sizeScatterRampReach1xPx: patch.sizeScatterRampReach1xPx ?? base.sizeScatterRampReach1xPx,
811
+ sizeScatterRampReach2xPx: patch.sizeScatterRampReach2xPx ?? base.sizeScatterRampReach2xPx,
678
812
  sizeOcclusionGain: patch.sizeOcclusionGain ?? base.sizeOcclusionGain,
679
813
  sizeShadowGainMax: patch.sizeShadowGainMax ?? base.sizeShadowGainMax,
680
814
  lensRefractionGain: patch.lensRefractionGain ?? base.lensRefractionGain,
815
+ lensHeightPerSpan: patch.lensHeightPerSpan ?? base.lensHeightPerSpan,
816
+ lensHeightMax: patch.lensHeightMax ?? base.lensHeightMax,
817
+ lensAmountPerSpan: patch.lensAmountPerSpan ?? base.lensAmountPerSpan,
818
+ lensAmountMax: patch.lensAmountMax ?? base.lensAmountMax,
819
+ lensThicknessReference: patch.lensThicknessReference ?? base.lensThicknessReference,
820
+ lensExtentGain: patch.lensExtentGain ?? base.lensExtentGain,
821
+ lensProfileExponent: patch.lensProfileExponent ?? base.lensProfileExponent,
822
+ lensOvalization: patch.lensOvalization ?? base.lensOvalization,
823
+ lensOvalizationSpanMin: patch.lensOvalizationSpanMin ?? base.lensOvalizationSpanMin,
824
+ lensOvalizationSpanMax: patch.lensOvalizationSpanMax ?? base.lensOvalizationSpanMax,
681
825
  reducedTransparencyFrost: patch.reducedTransparencyFrost ?? base.reducedTransparencyFrost,
682
826
  increasedOcclusionLift: patch.increasedOcclusionLift ?? base.increasedOcclusionLift,
683
827
  strongBorderRim: { ...base.strongBorderRim, ...patch.strongBorderRim },
@@ -810,15 +954,122 @@ function lensSizeGain(spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
810
954
  function lensSizeGainFromThickness(thickness, profile = DEFAULT_MATERIAL_PROFILE) {
811
955
  return 1 + (profile.lensSizeGainMax - 1) * thickness;
812
956
  }
813
- function sizeScatterSigma(sigmaPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
814
- return sizeScatterSigmaAt(sigmaPx, scatterThickness(spanPx, 1, profile), profile);
957
+ function shadowDepthPx(thicknessPx, spanPx, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
958
+ return Math.min(Math.max(thicknessPx, 0) * lensSizeGainFromThickness(thickness, profile), spanPx * 0.5);
959
+ }
960
+ function lensHeightBasePx(spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
961
+ return Math.min(profile.lensHeightPerSpan * Math.max(spanPx, 0), profile.lensHeightMax);
962
+ }
963
+ function lensAmountBasePx(spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
964
+ return Math.min(profile.lensAmountPerSpan * Math.max(spanPx, 0), profile.lensAmountMax);
965
+ }
966
+ function lensOvalizationAt(spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
967
+ return profile.lensOvalization * smoothstep(profile.lensOvalizationSpanMin, profile.lensOvalizationSpanMax, spanPx);
968
+ }
969
+ function lensMagnitudePx(thicknessPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE, fold = 1) {
970
+ const thick = Math.max(thicknessPx, 0);
971
+ if (thick <= 0)
972
+ return 0;
973
+ const scale = thick / profile.lensThicknessReference;
974
+ const unclamped = thick + (lensHeightBasePx(spanPx, profile) * scale - thick) * fold;
975
+ const depth = lensDepthPx(thicknessPx, spanPx, profile, fold);
976
+ const amount = thick + (lensAmountBasePx(spanPx, profile) * scale - thick) * fold;
977
+ return profile.lensRefractionGain * amount * (unclamped > 0 ? depth / unclamped : 0);
978
+ }
979
+ function lensExtentPx(thicknessPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE, fold = 1) {
980
+ return profile.lensExtentGain * lensDepthPx(thicknessPx, spanPx, profile, fold);
981
+ }
982
+ function lensDirection(normal, offset, half, ovalization) {
983
+ const a = Math.max(half[0], 1e-6);
984
+ const b = Math.max(half[1], 1e-6);
985
+ const ex = offset[0] / (a * a);
986
+ const ey = offset[1] / (b * b);
987
+ const r = Math.max(Math.hypot(offset[0] / a, offset[1] / b), 1e-6);
988
+ const scale = Math.min(a, b) / r;
989
+ const gx = (1 - ovalization) * normal[0] + ovalization * ex * scale;
990
+ const gy = (1 - ovalization) * normal[1] + ovalization * ey * scale;
991
+ const len = Math.hypot(gx, gy);
992
+ return len > 1e-9 ? [gx / len, gy / len] : [normal[0], normal[1]];
993
+ }
994
+ function sizeScatterSigma(sigmaPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE, devicePixelRatio = 1, extentsCssPx) {
995
+ return sizeScatterSigmaAt(sigmaPx, scatterThickness(spanPx, 1, profile, devicePixelRatio, extentsCssPx), profile, devicePixelRatio, spanPx);
996
+ }
997
+ function scatterRampStart(devicePixelRatio, profile = DEFAULT_MATERIAL_PROFILE, spanPx = 0) {
998
+ const thin = rampAtScale(profile.sizeScatterRampStartThin1x, profile.sizeScatterRampStartThin2x, devicePixelRatio);
999
+ const thick = rampAtScale(profile.sizeScatterRampStartThick1x, profile.sizeScatterRampStartThick2x, devicePixelRatio);
1000
+ const far = rampAtScale(profile.sizeScatterRampStartFar1x, profile.sizeScatterRampStartFar2x, devicePixelRatio);
1001
+ const decline = smoothstep(profile.sizeSpanMax, scatterSpanMaxAtScale(profile, devicePixelRatio), spanPx);
1002
+ return thin + (thick - thin) * sizeThickness(spanPx, profile) + (far - thick) * decline;
1003
+ }
1004
+ function scatterRampReachDevicePx(devicePixelRatio, profile = DEFAULT_MATERIAL_PROFILE) {
1005
+ return rampAtScale(profile.sizeScatterRampReach1xPx, profile.sizeScatterRampReach2xPx, devicePixelRatio);
1006
+ }
1007
+ function rampAtScale(at1x, at2x, devicePixelRatio) {
1008
+ const t = devicePixelRatio - 1;
1009
+ if (t <= 0)
1010
+ return at1x;
1011
+ if (t >= 1)
1012
+ return at2x;
1013
+ return at1x + (at2x - at1x) * t;
1014
+ }
1015
+ function scatterGainAtScale(profile = DEFAULT_MATERIAL_PROFILE, devicePixelRatio = 1) {
1016
+ return rampAtScale(profile.sizeScatterGainMax, profile.sizeScatterGainMax2x, devicePixelRatio);
1017
+ }
1018
+ function scatterGainFarAtScale(profile = DEFAULT_MATERIAL_PROFILE, devicePixelRatio = 1) {
1019
+ return rampAtScale(profile.sizeScatterGainMax, profile.sizeScatterGainFar2x, devicePixelRatio);
1020
+ }
1021
+ function scatterGainAt(spanPx, profile = DEFAULT_MATERIAL_PROFILE, devicePixelRatio = 1) {
1022
+ const near = scatterGainAtScale(profile, devicePixelRatio);
1023
+ const far = scatterGainFarAtScale(profile, devicePixelRatio);
1024
+ if (far === near)
1025
+ return near;
1026
+ return near + (far - near) * smoothstep(profile.sizeSpanMax, scatterSpanMaxAtScale(profile, devicePixelRatio), spanPx);
815
1027
  }
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;
1028
+ function scatterFloorAtScale(profile = DEFAULT_MATERIAL_PROFILE, devicePixelRatio = 1) {
1029
+ return clampUnit(rampAtScale(profile.sizeScatterFloor, profile.sizeScatterFloor2x, devicePixelRatio));
819
1030
  }
820
- function sizeScatterSigmaAt(sigmaPx, scatter, profile = DEFAULT_MATERIAL_PROFILE) {
821
- return sigmaPx * (1 + (profile.sizeScatterGainMax - 1) * scatter);
1031
+ function scatterSpanMaxAtScale(profile = DEFAULT_MATERIAL_PROFILE, devicePixelRatio = 1) {
1032
+ return rampAtScale(profile.sizeScatterSpanMax, profile.sizeScatterSpanMax2x, devicePixelRatio);
1033
+ }
1034
+ function scatterDeepThickness(spanPx, profile = DEFAULT_MATERIAL_PROFILE, devicePixelRatio = 1) {
1035
+ const floor = scatterFloorAtScale(profile, devicePixelRatio);
1036
+ return floor + (1 - floor) * smoothstep(profile.sizeSpanMin, scatterSpanMaxAtScale(profile, devicePixelRatio), spanPx);
1037
+ }
1038
+ function scatterSharpShare(uDevicePx, devicePixelRatio, profile = DEFAULT_MATERIAL_PROFILE, spanPx = 0) {
1039
+ const deepSharp = 1 - scatterDeepThickness(spanPx, profile, devicePixelRatio);
1040
+ const start = scatterRampStart(devicePixelRatio, profile, spanPx);
1041
+ const reach = Math.max(scatterRampReachDevicePx(devicePixelRatio, profile), 1e-6);
1042
+ const excursion = Math.max(start - deepSharp, 0) * Math.max(1 - Math.max(uDevicePx, 0) / reach, 0);
1043
+ return clampUnit(deepSharp + excursion);
1044
+ }
1045
+ function scatterThickness(spanPx, fold, profile = DEFAULT_MATERIAL_PROFILE, devicePixelRatio = 1, extentsCssPx) {
1046
+ const floor = scatterFloorAtScale(profile, devicePixelRatio);
1047
+ const mean = scatterRampAreaMean(spanPx, profile, devicePixelRatio, extentsCssPx);
1048
+ return clampUnit(floor + (mean - floor) * fold);
1049
+ }
1050
+ function scatterRampAreaMean(spanPx, profile = DEFAULT_MATERIAL_PROFILE, devicePixelRatio = 1, extentsCssPx) {
1051
+ const deep = scatterDeepThickness(spanPx, profile, devicePixelRatio);
1052
+ const amplitude = Math.max(scatterRampStart(devicePixelRatio, profile, spanPx) - (1 - deep), 0);
1053
+ if (amplitude <= 0)
1054
+ return clampUnit(deep);
1055
+ const width = Math.max(extentsCssPx?.[0] ?? spanPx, 0);
1056
+ const height = Math.max(extentsCssPx?.[1] ?? spanPx, 0);
1057
+ const area = width * height;
1058
+ if (area <= 0)
1059
+ return clampUnit(deep - amplitude);
1060
+ const reach = Math.max(scatterRampReachDevicePx(devicePixelRatio, profile) / Math.max(devicePixelRatio, 1e-3), 1e-6);
1061
+ const perimeter = 2 * (width + height);
1062
+ const limit = Math.min(reach, Math.min(width, height) / 2);
1063
+ const triangleMean = (perimeter * limit - 4 * limit * limit - perimeter * limit * limit / (2 * reach) + 8 * limit * limit * limit / (3 * reach)) / area;
1064
+ return clampUnit(deep - amplitude * triangleMean);
1065
+ }
1066
+ function sizeScatterSigmaAt(sigmaPx, scatter, profile = DEFAULT_MATERIAL_PROFILE, devicePixelRatio = 1, spanPx) {
1067
+ const mix = clampUnit(scatter);
1068
+ const gain = spanPx === void 0 ? scatterGainAtScale(profile, devicePixelRatio) : scatterGainAt(spanPx, profile, devicePixelRatio);
1069
+ return sigmaPx * (1 + (gain - 1) * mix);
1070
+ }
1071
+ function clampUnit(value) {
1072
+ return Math.min(1, Math.max(0, value));
822
1073
  }
823
1074
  function sizeOcclusionAlpha(alpha, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
824
1075
  return sizeOcclusionAlphaAt(alpha, sizeThickness(spanPx, profile), profile);
@@ -838,15 +1089,79 @@ function sizeOuterShadowOcclusion(occlusion, spanPx, profile = DEFAULT_MATERIAL_
838
1089
  function sizeOuterShadowOcclusionAt(occlusion, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
839
1090
  return Math.min(1, occlusion + profile.outerShadow.sizeGain * thickness * (1 - occlusion));
840
1091
  }
1092
+ var OUTER_SHADOW_THIN_L = {
1093
+ /** At and below this backdrop luminance the black term is `thinOcclusionDark`. */
1094
+ inert: 0.02,
1095
+ /** From here the mid plateau holds. */
1096
+ midFrom: 0.06,
1097
+ /** To here — `hc-text`'s own linear luminance. */
1098
+ midTo: 0.74,
1099
+ /** `light-solid`'s linear luminance, where 0.127 was measured; held above. */
1100
+ bright: 0.891
1101
+ };
1102
+ var OUTER_SHADOW_UNMEASURED_BACKDROP_LUMINANCE = 0.3;
1103
+ function outerShadowThinOcclusion(backdropLuminance, shadow) {
1104
+ const l = backdropLuminance ?? OUTER_SHADOW_UNMEASURED_BACKDROP_LUMINANCE;
1105
+ const { inert, midFrom, midTo, bright } = OUTER_SHADOW_THIN_L;
1106
+ if (l <= inert)
1107
+ return shadow.thinOcclusionDark;
1108
+ if (l < midFrom) {
1109
+ const t2 = (l - inert) / (midFrom - inert);
1110
+ const s = t2 * t2 * (3 - 2 * t2);
1111
+ return shadow.thinOcclusionDark + (shadow.thinOcclusionMid - shadow.thinOcclusionDark) * s;
1112
+ }
1113
+ if (l <= midTo)
1114
+ return shadow.thinOcclusionMid;
1115
+ if (l >= bright)
1116
+ return shadow.thinOcclusionBright;
1117
+ const t = (l - midTo) / (bright - midTo);
1118
+ return shadow.thinOcclusionMid + (shadow.thinOcclusionBright - shadow.thinOcclusionMid) * t;
1119
+ }
1120
+ var OUTER_SHADOW_THICK_SPANS = [96, 128, 160];
1121
+ function outerShadowThickOcclusion(spanPx, shadow) {
1122
+ const [s0, s1, s2] = OUTER_SHADOW_THICK_SPANS;
1123
+ const y0 = shadow.thickOcclusionAt96;
1124
+ const y1 = shadow.thickOcclusionAt128;
1125
+ const y2 = shadow.thickOcclusionAt160;
1126
+ if (spanPx <= s0)
1127
+ return y0;
1128
+ if (spanPx >= s2)
1129
+ return y2;
1130
+ if (spanPx <= s1)
1131
+ return y0 + (y1 - y0) * (spanPx - s0) / (s1 - s0);
1132
+ return y1 + (y2 - y1) * (spanPx - s1) / (s2 - s1);
1133
+ }
1134
+ function outerShadowOcclusionAt(shadow, backdropLuminance, spanPx, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
1135
+ const thin = outerShadowThinOcclusion(backdropLuminance, shadow);
1136
+ const thick = outerShadowThickOcclusion(spanPx, shadow);
1137
+ const k = Math.min(1, Math.max(0, thickness));
1138
+ const blend = k * k * (3 - 2 * k);
1139
+ return sizeOuterShadowOcclusionAt(thin + (thick - thin) * blend, thickness, profile);
1140
+ }
1141
+ function outerShadowLiftRise(spanPx, shadow) {
1142
+ return smoothstep(shadow.liftSpanMin, shadow.liftSpanFull, spanPx);
1143
+ }
841
1144
  function outerShadowUnderPolicy(policy, profile = DEFAULT_MATERIAL_PROFILE) {
842
1145
  const shadow = profile.outerShadow;
843
1146
  if (policy.glass === "none" || policy.frost === "none")
844
- return { ...shadow, occlusion: 0 };
1147
+ return flatOuterShadow(shadow, 0);
845
1148
  if (policy.frost === "increased") {
846
- return { ...shadow, occlusion: shadow.occlusion * shadow.reducedTransparencyOcclusion };
1149
+ return flatOuterShadow(shadow, shadow.reducedTransparencyOcclusion);
847
1150
  }
848
1151
  return shadow;
849
1152
  }
1153
+ function flatOuterShadow(shadow, amplitude) {
1154
+ return {
1155
+ ...shadow,
1156
+ thinOcclusionDark: amplitude,
1157
+ thinOcclusionMid: amplitude,
1158
+ thinOcclusionBright: amplitude,
1159
+ thickOcclusionAt96: amplitude,
1160
+ thickOcclusionAt128: amplitude,
1161
+ thickOcclusionAt160: amplitude,
1162
+ liftAmplitude: 0
1163
+ };
1164
+ }
850
1165
  var SRGB_ENCODING_EXPONENT = 2.4;
851
1166
  function outerShadowFalloff(signedDistancePx, sigmaPx) {
852
1167
  const x = -signedDistancePx / Math.max(sigmaPx, 1e-4);
@@ -856,8 +1171,8 @@ function outerShadowAlpha(occlusion) {
856
1171
  const occ = Math.min(1, Math.max(0, occlusion));
857
1172
  return 1 - Math.pow(1 - occ, 1 / SRGB_ENCODING_EXPONENT);
858
1173
  }
859
- function outerShadowReachPx(shadow) {
860
- const alpha = outerShadowAlpha(shadow.occlusion);
1174
+ function outerShadowReachPx(shadow, occlusion) {
1175
+ const alpha = outerShadowAlpha(occlusion);
861
1176
  if (!(alpha > 0))
862
1177
  return 0;
863
1178
  const cutoff = 1 / 255 / alpha;
@@ -875,15 +1190,18 @@ function outerShadowReachPx(shadow) {
875
1190
  }
876
1191
  return Math.max(0, hi + Math.abs(shadow.offsetPx) + shadow.spreadPx);
877
1192
  }
878
- function lensDepthPx(thicknessPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
879
- const gain = lensSizeGain(spanPx, profile);
880
- return Math.min(Math.max(thicknessPx, 0) * gain, spanPx * 0.5);
1193
+ function lensDepthPx(thicknessPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE, fold = 1) {
1194
+ const thick = Math.max(thicknessPx, 0);
1195
+ const scale = thick / profile.lensThicknessReference;
1196
+ const depth = thick + (lensHeightBasePx(spanPx, profile) * scale - thick) * fold;
1197
+ return Math.min(Math.max(depth, 0), spanPx * 0.5);
881
1198
  }
882
- function lensDisplacementPx(depthPx, lensDepthPx2, refractionScale, profile = DEFAULT_MATERIAL_PROFILE) {
883
- if (lensDepthPx2 <= 0)
1199
+ function lensDisplacementPx(depthPx, spanPx, thicknessPx, refractionScale, profile = DEFAULT_MATERIAL_PROFILE, fold = 1) {
1200
+ const extent = lensExtentPx(thicknessPx, spanPx, profile, fold);
1201
+ if (extent <= 0)
884
1202
  return 0;
885
- const depth = Math.min(Math.max(depthPx / lensDepthPx2, 0), 1);
886
- return lensDepthPx2 * (1 - depth) * (1 - depth) * refractionScale * profile.lensRefractionGain;
1203
+ const t = Math.max(0, 1 - Math.max(depthPx, 0) / extent);
1204
+ return lensMagnitudePx(thicknessPx, spanPx, profile, fold) * Math.pow(t, profile.lensProfileExponent) * refractionScale;
887
1205
  }
888
1206
 
889
1207
  // ../renderer-webgpu/dist/analysis.js
@@ -1721,7 +2039,8 @@ var poolKey = {
1721
2039
  backdropBodyScratch: (sourceId) => `backdrop:${sourceId}:body-scratch`,
1722
2040
  backdropUpload: (sourceId) => `backdrop:${sourceId}:upload`,
1723
2041
  groupField: (groupId) => `group:${groupId}:field`,
1724
- groupAux: (groupId) => `group:${groupId}:aux`
2042
+ groupAux: (groupId) => `group:${groupId}:aux`,
2043
+ groupAux2: (groupId) => `group:${groupId}:aux2`
1725
2044
  };
1726
2045
 
1727
2046
  // ../renderer-webgpu/dist/gpu-context.js
@@ -1878,7 +2197,7 @@ function resolveSurfaces(group, family, profile = DEFAULT_MATERIAL_PROFILE, poli
1878
2197
  centre: [fieldSource.channels.center[0], fieldSource.channels.center[1]],
1879
2198
  channels,
1880
2199
  spanPx,
1881
- lensDepthPx: Math.min(Math.max(shape.channels.thickness, 0) * lensSizeGainFromThickness(thickness, profile), spanPx * 0.5),
2200
+ lensDepthPx: lensDepthPx(shape.channels.thickness, spanPx, profile, profile.refractionScale[accessibilityRefractionCap(policy)]),
1882
2201
  sizeThickness: thickness,
1883
2202
  tintStrength: Math.min(1, Math.max(0, surface.tint?.strength ?? 0))
1884
2203
  };
@@ -1943,7 +2262,7 @@ function packInstances(surfaces, origin, into) {
1943
2262
  data[o + 11] = s.shape.channels.thickness;
1944
2263
  data[o + 12] = s.channels.press;
1945
2264
  data[o + 13] = s.channels.glow;
1946
- data[o + 14] = s.lensDepthPx * Math.min(1, Math.max(0, s.channels.lensStrength));
2265
+ data[o + 14] = Math.max(s.shape.channels.thickness, 0) * Math.min(1, Math.max(0, s.channels.lensStrength));
1947
2266
  data[o + 15] = s.tintStrength;
1948
2267
  data[o + 16] = s.spanPx;
1949
2268
  data[o + 17] = 0;
@@ -2221,7 +2540,7 @@ var WGSL_INSTANCE_STRUCT = `struct Instance {
2221
2540
  thick : f32, // 44 material thickness, CSS px
2222
2541
  press : f32, // 48 interaction channel value, 0..1
2223
2542
  glow : f32, // 52 interaction channel value, 0..1
2224
- lensDepth : f32, // 56 CPU-resolved lens depth in CSS px (material.ts), already scaled by the lensStrength channel
2543
+ lensThick : f32, // 56 authored thickness in CSS px times the lensStrength channel (W12 G2): both depths are evaluated from it per pixel
2225
2544
  tintK : f32, // 60 author tint strength, 0..1 \u2014 the seed itself is a group uniform
2226
2545
  span : f32, // 64 the surface's shorter extent, CSS px \u2014 the size law's input, both curves (W11c)
2227
2546
  _pad : f32, // 68 stride padding: vec2f aligns the struct to 8, so 72 is the next legal size
@@ -2389,7 +2708,8 @@ var WGSL_FIELD_PASS = `struct FieldUniforms {
2389
2708
  // field but its members are not one size, and carrying them per pixel is what
2390
2709
  // lets a 40 px button and a 320 px platter share a field pass and still lens by
2391
2710
  // their own depth \u2014 parent acceptance #2 inside a GlassEffectContainer.
2392
- // aux = (lensDepthPx, glow, spanPx, tintStrength)
2711
+ // aux = (lensThickPx, glow, spanPx, tintStrength)
2712
+ // aux2 = (p \u2212 centre, half.x, half.y) \u2014 W12 G2, group-local CSS px
2393
2713
  //
2394
2714
  // The third and fourth slots both changed hands, and neither of the values they
2395
2715
  // used to carry was ever read by a fragment stage. 'thick' left because
@@ -2423,14 +2743,25 @@ var WGSL_FIELD_PASS = `struct FieldUniforms {
2423
2743
  // their SPANS and evaluates the curves on the blend, where it used to blend the
2424
2744
  // evaluated thicknesses. Inside either member the two are identical; only the
2425
2745
  // smooth-min neck differs, and nothing measured claims a value there.
2746
+ //
2747
+ // 'aux2' (W12 G2) carries the pixel's offset from the owning surface's centre
2748
+ // and that surface's half-extents, which
2749
+ // the optics pass needs for the lens's direction: the reference displaces along
2750
+ // the gradient of its SDF blended toward the oval inscribed in the shape's box
2751
+ // (claims \xA75.50's 'gradientOvalization'), and an oval is a property of the box,
2752
+ // not of the distance field. Blended by the same weight as 'aux' in a neck, so
2753
+ // two merged members share one oval there \u2014 a value nothing measured either.
2754
+ // For a concentric child the box is the parent's (the child is the parent's
2755
+ // level set), which is the same centre and a slightly larger oval.
2426
2756
 
2427
2757
  @group(0) @binding(0) var<uniform> fu : FieldUniforms;
2428
2758
  @group(0) @binding(1) var<storage, read> instances : array<Instance>;
2429
2759
 
2430
2760
  struct Member {
2431
- d : f32,
2432
- g : vec2f,
2433
- aux : vec4f,
2761
+ d : f32,
2762
+ g : vec2f,
2763
+ aux : vec4f,
2764
+ aux2 : vec4f,
2434
2765
  };
2435
2766
 
2436
2767
  fn eval_instance(i : u32, p : vec2f) -> Member {
@@ -2443,13 +2774,17 @@ fn eval_instance(i : u32, p : vec2f) -> Member {
2443
2774
  var m : Member;
2444
2775
  m.d = f.d + s.inset;
2445
2776
  m.g = f.g;
2446
- m.aux = vec4f(s.lensDepth, s.glow, s.span, s.tintK);
2777
+ m.aux = vec4f(s.lensThick, s.glow, s.span, s.tintK);
2778
+ // The pixel relative to the surface's centre, and the half-extents: what the
2779
+ // oval's gradient needs, written here where the group-local position is exact.
2780
+ m.aux2 = vec4f(p - s.centre, s.half);
2447
2781
  return m;
2448
2782
  }
2449
2783
 
2450
2784
  struct FieldOut {
2451
2785
  @location(0) field : vec4f,
2452
2786
  @location(1) aux : vec4f,
2787
+ @location(2) aux2 : vec4f,
2453
2788
  };
2454
2789
 
2455
2790
  @fragment
@@ -2467,6 +2802,7 @@ fn fs_field(in : FullscreenOut) -> FieldOut {
2467
2802
  // coverage downstream is not reliably zero.
2468
2803
  out.field = vec4f(65000.0, 0.0, -1.0, 0.0);
2469
2804
  out.aux = vec4f(0.0);
2805
+ out.aux2 = vec4f(0.0);
2470
2806
  return out;
2471
2807
  }
2472
2808
 
@@ -2479,6 +2815,7 @@ fn fs_field(in : FullscreenOut) -> FieldOut {
2479
2815
  acc.d = blend.x;
2480
2816
  acc.g = mix(s.g, acc.g, h);
2481
2817
  acc.aux = mix(s.aux, acc.aux, h);
2818
+ acc.aux2 = mix(s.aux2, acc.aux2, h);
2482
2819
  nearest = min(nearest, s.d);
2483
2820
  }
2484
2821
  // One clamp at the end of the fold, so |union - min| <= maxBulge holds for any
@@ -2491,6 +2828,7 @@ fn fs_field(in : FullscreenOut) -> FieldOut {
2491
2828
 
2492
2829
  out.field = vec4f(acc.d, normal.x, normal.y, coverage);
2493
2830
  out.aux = acc.aux;
2831
+ out.aux2 = acc.aux2;
2494
2832
  return out;
2495
2833
  }`;
2496
2834
  function fieldPassSource(family) {
@@ -2646,7 +2984,9 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
2646
2984
  screen : vec4f,
2647
2985
  /// backdrop uv transform on viewport-normalised coords: scale (xy), offset (zw)
2648
2986
  fit : vec4f,
2649
- /// refractionScale, lensRefractionGain (W11c G2), unused, chainMaxLod
2987
+ /// refractionScale, lensRefractionGain (W12 G2: the gain on the reference's
2988
+ /// amount law), the inner shadow's depth gain (W2's lensSizeGainMax, which the
2989
+ /// shadow keeps), chainMaxLod
2650
2990
  lens : vec4f,
2651
2991
  /// fixed tint colour, linear light (xyz), tint alpha (w)
2652
2992
  tint : vec4f,
@@ -2665,7 +3005,8 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
2665
3005
  light : vec4f,
2666
3006
  /// hasBackdrop, fieldSize.xy, fieldUpsampled
2667
3007
  flags : vec4f,
2668
- /// the size law's gains (W2): scatterGainMax, occlusionGain, shadowGainMax,
3008
+ /// the size law's gains (W2): scatterGainMax \u2014 resolved at the group's device
3009
+ /// ratio since W15 G1, claims 5.69 section 1 \u2014 occlusionGain, shadowGainMax,
2669
3010
  /// bodyChainLod \u2014 the last being the chain level whose blur already matches the
2670
3011
  /// body texture, which is what the scattering term measures its octaves from
2671
3012
  size : vec4f,
@@ -2677,17 +3018,27 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
2677
3018
  toneAdapt : vec4f,
2678
3019
  /// the backdrop source's own average colour, linear (xyz), and its luminance (w)
2679
3020
  toneColour : vec4f,
2680
- /// the outer shadow (W8): its compositing-space alpha before the size law (x),
2681
- /// the blur's sigma in group-local CSS px (y), the silhouette's outward spread
2682
- /// in the same units (z), and the downward offset expressed in field-texture UV
2683
- /// (w) \u2014 a UV so the offset silhouette is one texture read rather than a
2684
- /// gradient extrapolation, which is exact only on a straight edge and a capsule
2685
- /// is mostly not one
3021
+ /// the outer shadow (W8, amplitude re-read by W14 G1): the THIN regime's peak
3022
+ /// LINEAR occlusion, already resolved at this group's backdrop luminance and
3023
+ /// already folded under the accessibility policy (x) \u2014 a linear occlusion and
3024
+ /// no longer a compositing alpha, because the shader now blends two regimes
3025
+ /// before it converts; the blur's sigma in group-local CSS px (y), the
3026
+ /// silhouette's outward spread in the same units (z), and the downward offset
3027
+ /// expressed in field-texture UV (w) \u2014 a UV so the offset silhouette is one
3028
+ /// texture read rather than a gradient extrapolation, which is exact only on a
3029
+ /// straight edge and a capsule is mostly not one
2686
3030
  shadow : vec4f,
2687
3031
  /// the outer shadow's size-law gain (x), read against the casting surface's own
2688
3032
  /// thickness, and the field rect's height in CSS px (y) \u2014 the conversion the
2689
- /// shadow's shift needs when it lands outside the texture; zw are the padding
2690
- /// a uniform's vec4 alignment requires
3033
+ /// shadow's shift needs when it lands outside the texture; the body depth
3034
+ /// ramp's THICK start (z, W13 G1's third form \u2014 the thin end is scatter.y and
3035
+ /// this pass mixes the two by the pixel's own sizeThickness), and (w) the
3036
+ /// heavy width's gain at the TOP of the scatter span curve, resolved at this
3037
+ /// group's device ratio (W15 G1's re-form, claims \xA75.70 \xA74 and \xA77) \u2014 the
3038
+ /// padding slot this vec4's alignment already required, taken because the
3039
+ /// gain's near end (size.x) has no free neighbour and this vec4 already
3040
+ /// carries the body ramp's other span-graded end; at dpr 1 it equals size.x
3041
+ /// and the grading is flat
2691
3042
  shadowSize : vec4f,
2692
3043
  /// the backdrop tone response's anchors (W9): the three solid anchors'
2693
3044
  /// ENCODED-space means (xyz), and the backdrop's linear-space mean (w) \u2014 the
@@ -2699,10 +3050,45 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
2699
3050
  toneRowThin : vec4f,
2700
3051
  /// the thick row (sizeThickness saturated), same layout
2701
3052
  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
3053
+ /// the size law's bands: the scatter facet's floor (x, resolved at the
3054
+ /// group's device ratio since W15 G1 \u2014 the deep value is per-scale, claims
3055
+ /// 5.69 section 2 \u2014 and read here for both the deep curve and the fold) and
3056
+ /// the depth ramp's
3057
+ /// THIN start s0 at the ratio this group draws at (y, W13 G1 \u2014 the CPU has
3058
+ /// already interpolated it between the profile's 1x and 2x anchors; the thick
3059
+ /// end is shadowSize.z), and the
3060
+ /// thickness curve's sizeSpanMin (z) and sizeSpanMax (w); the fold both
3061
+ /// multiply by is tone.w. The scatter span curve's own band top is lensOval.w,
3062
+ /// and it starts at the same z.
2705
3063
  scatter : vec4f,
3064
+ /// the lens law (W12 G2): the reference's height law (lensHeightPerSpan,
3065
+ /// lensHeightMax) and amount law (lensAmountPerSpan, lensAmountMax)
3066
+ lensLaw : vec4f,
3067
+ /// the lens profile (W12 G2): lensExtentGain, lensProfileExponent,
3068
+ /// lensOvalization, lensThicknessReference
3069
+ lensShape : vec4f,
3070
+ /// the ovalization's knee (W12 G2): spanMin (x), spanMax (y); the body depth
3071
+ /// ramp's reach in group-local CSS px (z, W13 G1) \u2014 the profile names it in
3072
+ /// device px and the CPU divides by the ratio, so that this shader can read
3073
+ /// the field's own CSS-px depth without a second conversion; the scatter span
3074
+ /// curve's band top sizeScatterSpanMax (w, W11c, resolved at the group's
3075
+ /// device ratio since W15 G1), which W13 G1 keeps underneath the ramp as its
3076
+ /// deep value
3077
+ lensOval : vec4f,
3078
+ /// the outer shadow's THICK regime (W14 G1): the composite's peak linear
3079
+ /// occlusion at casting spans 96, 128 and 160 CSS px (xyz), already folded
3080
+ /// under the accessibility policy, piecewise-linear between and held outside;
3081
+ /// and (w) the body depth ramp's FAR start (W13 G1's fourth form) \u2014 the start
3082
+ /// at span >= sizeScatterSpanMax, which the ramp declines to from the thick
3083
+ /// end along the scatter span curve, in the slot W14 left free
3084
+ shadowThick : vec4f,
3085
+ /// the lift (W14 G1), the shadow's second term and GPU-tier only: its peak
3086
+ /// amplitude in LINEAR light as a fraction of the blurred backdrop's own
3087
+ /// luminance (x), the span rise's foot and top in CSS px (yz), and the chain
3088
+ /// LOD whose blur is the profile's liftBlurSigmaCss \u2014 resolved on the CPU,
3089
+ /// which is the only place the CSS-px-to-texel conversion is knowable, and
3090
+ /// already clamped to chainMaxLod (w)
3091
+ shadowLift : vec4f,
2706
3092
  };
2707
3093
 
2708
3094
  @group(0) @binding(0) var<uniform> ou : OpticsUniforms;
@@ -2712,6 +3098,9 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
2712
3098
  @group(0) @binding(4) var backdropChain : texture_2d<f32>;
2713
3099
  @group(0) @binding(5) var backdropBody : texture_2d<f32>;
2714
3100
  @group(0) @binding(6) var fieldSampler : sampler;
3101
+ /// The owning surface's box per pixel (W12 G2): the pixel's offset from its
3102
+ /// centre (xy) and its half-extents (zw), group-local CSS px \u2014 the lens's oval.
3103
+ @group(0) @binding(7) var aux2Texture : texture_2d<f32>;
2715
3104
 
2716
3105
  /// One encoded sRGB channel from a linear one \u2014 the space the backdrop tone
2717
3106
  /// response's anchors live in (W9). Mirrors material.ts's 'linearToSrgbChannel'.
@@ -2767,7 +3156,33 @@ fn outer_shadow_falloff(signedDistance : f32, sigma : f32) -> f32 {
2767
3156
  return 0.5 * (1.0 + tanh(0.7978845608028654 * (x + 0.044715 * x * x * x)));
2768
3157
  }
2769
3158
 
2770
- /// The outer shadow's alpha at this pixel (W8).
3159
+ /// What one pixel's outer shadow is made of: the compositing-space alpha the
3160
+ /// black term composites at, the falloff BOTH terms ride, and the span of the
3161
+ /// surface that cast it \u2014 which the lift's own rise is read from.
3162
+ ///
3163
+ /// One struct rather than three calls because there is one falloff (W14 G0,
3164
+ /// claims \xA75.62 \xA74): the black term's free fit and the lift's free fit return
3165
+ /// the same sigma, offset and spread, so evaluating a second geometry for the
3166
+ /// second term would be asserting a difference the measurement denies.
3167
+ struct ShadowSample {
3168
+ alpha : f32,
3169
+ falloff : f32,
3170
+ castSpanCss : f32,
3171
+ };
3172
+
3173
+ /// The composite's peak linear occlusion above the knee, at a casting span \u2014
3174
+ /// piecewise linear through the three measured anchors (96 / 128 / 160 CSS px)
3175
+ /// and held flat outside them. Mirrors material.ts's
3176
+ /// 'outerShadowThickOcclusion'.
3177
+ fn outer_shadow_thick(spanCss : f32) -> f32 {
3178
+ let y = ou.shadowThick.xyz;
3179
+ if (spanCss <= 96.0) { return y.x; }
3180
+ if (spanCss >= 160.0) { return y.z; }
3181
+ if (spanCss <= 128.0) { return y.x + (y.y - y.x) * (spanCss - 96.0) / 32.0; }
3182
+ return y.y + (y.z - y.y) * (spanCss - 128.0) / 32.0;
3183
+ }
3184
+
3185
+ /// The outer shadow's alpha at this pixel (W8, amplitude W14 G1).
2771
3186
  ///
2772
3187
  /// The shadow is the group's OWN field, translated down by 'shadow.w' in field
2773
3188
  /// UV and outset by 'shadow.z', then blurred. Reading the offset silhouette from
@@ -2778,9 +3193,13 @@ fn outer_shadow_falloff(signedDistance : f32, sigma : f32) -> f32 {
2778
3193
  /// The value is an ALPHA on pure black, so what it composites to is the backdrop
2779
3194
  /// times '1 - alpha' \u2014 multiplicative occlusion, and exactly zero over black,
2780
3195
  /// with no branch for it.
2781
- fn outer_shadow_alpha(uv : vec2f, upsampled : f32, fieldSize : vec2f) -> f32 {
2782
- if (ou.shadow.x <= 0.0) {
2783
- return 0.0;
3196
+ fn outer_shadow(uv : vec2f, upsampled : f32, fieldSize : vec2f) -> ShadowSample {
3197
+ var out : ShadowSample;
3198
+ out.alpha = 0.0;
3199
+ out.falloff = 0.0;
3200
+ out.castSpanCss = 0.0;
3201
+ if (ou.shadow.x <= 0.0 && ou.shadowThick.x <= 0.0) {
3202
+ return out;
2784
3203
  }
2785
3204
  /*
2786
3205
  * The shift can leave the field texture, and clamping alone reads a lie there.
@@ -2816,14 +3235,11 @@ fn outer_shadow_alpha(uv : vec2f, upsampled : f32, fieldSize : vec2f) -> f32 {
2816
3235
  }
2817
3236
 
2818
3237
  // The size law reaches the amplitude and nothing else \u2014 the reference's three
2819
- // lengths are span-invariant across 32\u2026160 px. The gain rides the thickness of
2820
- // the surface that CAST the shadow, which is the one read at the offset
2821
- // position rather than the one under the pixel being shaded.
3238
+ // lengths are span-invariant across 32\u2026160 px. Every span-keyed quantity here
3239
+ // rides the thickness and the span of the surface that CAST the shadow, which
3240
+ // is the one read at the offset position rather than the one under the pixel
3241
+ // being shaded.
2822
3242
  //
2823
- // Applied on '1 - alpha' rather than on the alpha: the gain is a fraction of
2824
- // the remaining transparency in LINEAR light ('sizeOuterShadowOcclusionAt'),
2825
- // and (1 - occ) is exactly what raising to 1/2.4 turns into (1 - alpha), so the
2826
- // two forms are the same law and neither needs the other's space.
2827
3243
  // The thickness curve off the casting surface's span (W11c: the span rides
2828
3244
  // the field), written as the body's own is below.
2829
3245
  let castT = clamp(
@@ -2832,10 +3248,65 @@ fn outer_shadow_alpha(uv : vec2f, upsampled : f32, fieldSize : vec2f) -> f32 {
2832
3248
  1.0,
2833
3249
  );
2834
3250
  let sizeK = clamp(castT * castT * (3.0 - 2.0 * castT) * clamp(ou.tone.w, 0.0, 1.0), 0.0, 1.0);
2835
- let sizeFold = pow(max(1.0 - ou.shadowSize.x * sizeK, 0.0), 1.0 / 2.4);
2836
- let alpha = 1.0 - (1.0 - ou.shadow.x) * sizeFold;
2837
3251
 
2838
- return alpha * outer_shadow_falloff(shadowField.x + clampedOffCss - ou.shadow.z, ou.shadow.y);
3252
+ // The two regimes and the knee between them (W14 G1). The thin amplitude is
3253
+ // resolved on the CPU because its key \u2014 the backdrop's luminance \u2014 is one
3254
+ // number for the whole group; the thick one is a span law and has to be per
3255
+ // pixel. The blend is the smoothstep of 'sizeK', which is exactly the curve
3256
+ // 'tone_response' blends its own thin and thick rows across: one knee for the
3257
+ // face and the shadow, which is the charter's third binding rule read from
3258
+ // the other side.
3259
+ let blend = sizeK * sizeK * (3.0 - 2.0 * sizeK);
3260
+ let regime = mix(ou.shadow.x, outer_shadow_thick(shadowAux.z), blend);
3261
+ // The size gain, on the relative form 'sizeOuterShadowOcclusionAt' uses, in
3262
+ // LINEAR light; then the one conversion to the canvas's compositing space,
3263
+ // which is where the black term is actually composited.
3264
+ let occ = clamp(regime + ou.shadowSize.x * sizeK * (1.0 - regime), 0.0, 1.0);
3265
+ out.alpha = 1.0 - pow(1.0 - occ, 1.0 / 2.4);
3266
+ out.falloff = outer_shadow_falloff(shadowField.x + clampedOffCss - ou.shadow.z, ou.shadow.y);
3267
+ out.castSpanCss = shadowAux.z;
3268
+ return out;
3269
+ }
3270
+
3271
+ /// The lift (W14 G1) \u2014 the shadow's second term, in LINEAR light, premultiplied
3272
+ /// by nothing yet: a blurred copy of the backdrop's own light, at an amplitude
3273
+ /// that is zero below the knee and saturating above it, on the SAME falloff the
3274
+ /// black term rides.
3275
+ ///
3276
+ /// 'V' is the backdrop chain read at the pixel's OWN position \u2014 the copy is of
3277
+ /// the backdrop beneath the shadow, not of the backdrop under the surface \u2014 at
3278
+ /// the LOD whose blur is the profile's 40 CSS px. The chain's uv transform is on
3279
+ /// viewport-normalised coordinates, so every pixel this pass draws has a valid
3280
+ /// sample: unlike the field texture, which the offset shift can walk off the top
3281
+ /// of, the chain covers the whole viewport and there is nothing to reconstruct.
3282
+ ///
3283
+ /// Zero over black by construction (V = 0), zero below the knee by the rise, and
3284
+ /// zero where the group has no backdrop to copy \u2014 so 'dark-solid', 'impulse',
3285
+ /// every thin cell and every unsampled group stay byte-for-byte what they were.
3286
+ fn outer_shadow_lift(viewport01 : vec2f, shadow : ShadowSample) -> vec3f {
3287
+ if (ou.flags.x <= 0.5 || ou.shadowLift.x <= 0.0 || shadow.falloff <= 0.0) {
3288
+ return vec3f(0.0);
3289
+ }
3290
+ // Written out rather than through WGSL's 'smoothstep', which is undefined
3291
+ // where the two edges coincide \u2014 a profile is entitled to set the rise's foot
3292
+ // and top to one number, and material.ts's own 'smoothstep' degrades that to a
3293
+ // step rather than to a NaN. Same rule as the tone band's divide above.
3294
+ let riseT = clamp(
3295
+ (shadow.castSpanCss - ou.shadowLift.y) / max(ou.shadowLift.z - ou.shadowLift.y, 1e-6),
3296
+ 0.0,
3297
+ 1.0,
3298
+ );
3299
+ let rise = riseT * riseT * (3.0 - 2.0 * riseT);
3300
+ if (rise <= 0.0) {
3301
+ return vec3f(0.0);
3302
+ }
3303
+ let uv = clamp(viewport01 * ou.fit.xy + ou.fit.zw, vec2f(0.0), vec2f(1.0));
3304
+ let chainSample = textureSampleLevel(backdropChain, backdropSampler, uv, ou.shadowLift.w);
3305
+ // Premultiplied linear in, straight colour out \u2014 the same unpremultiply the
3306
+ // body's own samples take, for the same reason: a partially transparent
3307
+ // backdrop must not darken what the material adds back.
3308
+ let v = chainSample.rgb / max(chainSample.a, 1e-6);
3309
+ return v * (ou.shadowLift.x * rise * shadow.falloff);
2839
3310
  }
2840
3311
 
2841
3312
  @fragment
@@ -2847,13 +3318,16 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
2847
3318
  // contour to the coarse grid and the rim would step along it.
2848
3319
  var field : vec4f;
2849
3320
  var aux : vec4f;
3321
+ var aux2 : vec4f;
2850
3322
  if (ou.flags.w > 0.5) {
2851
3323
  field = textureSampleLevel(fieldTexture, fieldSampler, in.uv, 0.0);
2852
3324
  aux = textureSampleLevel(auxTexture, fieldSampler, in.uv, 0.0);
3325
+ aux2 = textureSampleLevel(aux2Texture, fieldSampler, in.uv, 0.0);
2853
3326
  } else {
2854
3327
  let texel = vec2i(in.uv * ou.flags.yz);
2855
3328
  field = textureLoad(fieldTexture, texel, 0);
2856
3329
  aux = textureLoad(auxTexture, texel, 0);
3330
+ aux2 = textureLoad(aux2Texture, texel, 0);
2857
3331
  }
2858
3332
 
2859
3333
  let d = field.x;
@@ -2869,46 +3343,179 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
2869
3343
  * is clipped out of its own border box, and the two meet across the coverage
2870
3344
  * ramp with no seam because it is one expression.
2871
3345
  */
2872
- let shadowAlpha = outer_shadow_alpha(in.uv, ou.flags.w, ou.flags.yz);
3346
+ let shadowSample = outer_shadow(in.uv, ou.flags.w, ou.flags.yz);
3347
+ let shadowAlpha = shadowSample.alpha * shadowSample.falloff;
3348
+ let viewport01 = in.position.xy / ou.screen.xy;
3349
+ // Resolved once, for both sides of the coverage ramp: the lift fills exactly
3350
+ // what the surface's coverage leaves, on the same rule and in the same
3351
+ // expression as the black term's alpha at the bottom of this function, so the
3352
+ // two meet across the ramp with no seam. At full coverage it is not computed
3353
+ // at all and the chain read is not taken.
3354
+ var liftEncoded = vec3f(0.0);
3355
+ if (coverage < 1.0) {
3356
+ let lift = outer_shadow_lift(viewport01, shadowSample);
3357
+ liftEncoded = min(linear_to_srgb(max(lift, vec3f(0.0))), vec3f(shadowAlpha));
3358
+ }
2873
3359
  if (coverage <= 0.0) {
2874
- return vec4f(0.0, 0.0, 0.0, shadowAlpha);
3360
+ /*
3361
+ * The lift (W14 G1) makes this a premultiplied COMPOSITE rather than an
3362
+ * alpha on black: the compositor produces 'page\xB7(1 \u2212 shadowAlpha) + lift',
3363
+ * which is the reference's own two-term form outside the coverage.
3364
+ *
3365
+ * The added light is computed in linear light and encoded on its own before
3366
+ * it is emitted, because it IS its own light \u2014 over the checkerboard's black
3367
+ * squares, where the multiply is inert, what the reference adds is exactly
3368
+ * this term and nothing else, and encoding it alone reproduces G0's measured
3369
+ * +0.048 encoded at span 160 from its +0.0038 linear (claims \xA75.62 \xA72, \xA73).
3370
+ * Where the backdrop is bright the encoding of a sum and the sum of the
3371
+ * encodings differ, and this term is the second: the residual is bounded by
3372
+ * the term's own size and is well inside the bed's \xB14/255, on the same
3373
+ * honest-floor argument 'outerShadowAlpha' makes for the black term's space.
3374
+ *
3375
+ * A premultiplied layer may not carry a channel above its own alpha, so the
3376
+ * emitted light is capped at 'shadowAlpha'. At the profile's constants the
3377
+ * cap is inert everywhere on the falloff: both terms are proportional to the
3378
+ * same F, and the lift's encoded value stays at about a sixth of the black
3379
+ * term's alpha from the deepest ring out to where the shadow stops moving a
3380
+ * code. It is here so that a swept amplitude produces a valid layer rather
3381
+ * than an implementation-defined one.
3382
+ */
3383
+ return vec4f(liftEncoded, shadowAlpha);
2875
3384
  }
2876
3385
 
2877
- let viewport01 = in.position.xy / ou.screen.xy;
2878
3386
  let viewportCss = ou.screen.xy * ou.screen.z;
2879
3387
 
2880
- // Per-pixel, unioned through the field pass. See the module note.
2881
- let lensDepth = max(aux.x, 1e-4);
2882
- // The size law's two curves off the span of whichever surface owns this pixel
3388
+ // Per-pixel, unioned through the field pass. See the module note. 'aux.x' is
3389
+ // the authored thickness times the lensStrength channel (W12 G2); the lens's
3390
+ // depth and the inner shadow's are both evaluated from it and the span below.
3391
+ let lensThick = max(aux.x, 0.0);
3392
+ // The thickness curve off the span of whichever surface owns this pixel
2883
3393
  // (W11c). 'sizeK' is the thickness factor, 0..1: zero on anything at or below
2884
3394
  // the profile's 'sizeSpanMin', saturated at 'sizeSpanMax', folded under the
2885
3395
  // 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
3396
+ // by it, so a small control takes the pre-law path exactly. Written out as a
3397
+ // smoothstep with a guarded denominator, so a profile that collapses the band
2891
3398
  // degrades to a step rather than to NaN.
2892
3399
  let span = aux.z;
2893
3400
  let fold = clamp(ou.tone.w, 0.0, 1.0);
2894
3401
  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);
3402
+ // The unfolded curve, which the depth ramp's start grades along below, and the
3403
+ // folded factor every other facet multiplies by. One smoothstep, two readings.
3404
+ let sizeThick = thickT * thickT * (3.0 - 2.0 * thickT);
3405
+ let sizeK = clamp(sizeThick * fold, 0.0, 1.0);
3406
+ /*
3407
+ * The body's mix is the span curve W11c fitted with a ramp in DEPTH riding on
3408
+ * top of it near the contour (W13 G1, from the measurement of claims \xA75.61 \xA72
3409
+ * and the re-forming its first runtime sweep forced):
3410
+ *
3411
+ * kDeep = floor + (1 - floor) * smoothstep(sizeSpanMin, sizeScatterSpanMax, span)
3412
+ * s0 = startThin + (startThick - startThin) * sizeThickness(span)
3413
+ * + (startFar - startThick) * smoothstep(sizeSpanMax, sizeScatterSpanMax, span)
3414
+ * s(u) = (1 - kDeep) + max(0, s0 - (1 - kDeep)) * max(0, 1 - u / U)
3415
+ * k(u) = 1 - s(u)
3416
+ *
3417
+ * The first form of the ramp replaced the span curve outright and its sweep
3418
+ * measured what that cost: the ramp's own projection onto one number per
3419
+ * surface runs 0.43-0.56 where the curve it replaced runs 0.41-1.00, so the
3420
+ * family is nearly span-flat where the bed is strongly span-graded and no
3421
+ * point in 81 reached the wave's stops. So the curve stays as the deep value
3422
+ * and the ramp is what it always measured as - a near-contour excursion.
3423
+ *
3424
+ * The SECOND form gave that excursion one start per scale, and its own sweep
3425
+ * refuted that arithmetically (claims \xA75.64 \xA72): the smallest cell's span is
3426
+ * exactly sizeSpanMin, so its deep sharp share is exactly 1 - floor and no
3427
+ * start at or below it can touch that cell, while the next span up only
3428
+ * improves BELOW a start lower still. So the start grades with span too, and
3429
+ * along the material's own thin/thick curve rather than a new one - the same
3430
+ * smoothstep 'sizeK' is built from above, unfolded, because s0 is a share the
3431
+ * reference has and not a rise a preference removes. The fold below still
3432
+ * applies once, on the composed mix.
3433
+ *
3434
+ * The THIRD form stopped there and its holdout failed on one row for the
3435
+ * form's own arithmetic (claims \xA75.67 \xA74): sizeThickness saturates at
3436
+ * sizeSpanMax, so every thick span got the same start while the reference's
3437
+ * start keeps FALLING across the thick spans, and because kDeep keeps rising
3438
+ * to sizeScatterSpanMax the excursion grew with span where the reference's
3439
+ * shrinks. So the FOURTH form declines the start past the knee along the
3440
+ * scatter span curve itself \u2014 the same smoothstep 'deepT' is built from, on
3441
+ * the band above the thickness knee \u2014 from the thick anchor to startFar at
3442
+ * sizeScatterSpanMax. Same curve read twice, no new span statistic.
3443
+ *
3444
+ * '-d' is the depth, in group-local CSS px and unclamped - the same quantity
3445
+ * the lens's 'lensT' and the inner shadow's 'shadowT' are evaluated from,
3446
+ * except that those two divide it by a depth of their own while this reads it
3447
+ * absolutely, because the reach measured as a LENGTH rather than as a fraction
3448
+ * of the span. The reach reaches this shader already divided by the device
3449
+ * ratio ('lensOval.z'), so the ratio 'u / U' is the same number in CSS px as
3450
+ * it is in the device px the profile names.
3451
+ *
3452
+ * 'max(s0 - sDeep, 0)' rather than a signed difference: the excursion is the
3453
+ * band the reference has ABOVE the body, and a span whose deep sharp share
3454
+ * already exceeds s0 has nothing to add rather than something to subtract.
3455
+ *
3456
+ * The floor and the span top arrive resolved at the ratio this group draws at
3457
+ * (W15 G1, claims 5.69 section 2): the reference's 2x deep interior is fully
3458
+ * heavy on the largest spans where this curve fitted at 1x leaves a sharp
3459
+ * share of 0.24-0.36, so kDeep is a per-scale law. Nothing about the
3460
+ * arithmetic here changes - the uniforms carry a different number, not a
3461
+ * different shape - and on the landed material the two anchors are equal.
3462
+ *
3463
+ * The fold keeps its W11c semantics on the composed law: the floor is the
3464
+ * frost the material has at any size and is not folded, everything above it -
3465
+ * the span curve's rise and the ramp's excursion alike - is depth and is, so
3466
+ * fold 1 renders the law and fold 0 sits at the floor exactly.
3467
+ */
2896
3468
  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
- );
2903
- // '-d' is depth inside the surface, so the profile runs 1 at the contour to 0
2904
- // at 'lensDepth' inward, and the square makes the falloff read as curvature
2905
- // rather than as a linear ramp.
2906
- let depth = clamp(-d / lensDepth, 0.0, 1.0);
2907
- let profile = (1.0 - depth) * (1.0 - depth);
2908
-
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;
3469
+ let deepT = clamp((span - ou.scatter.z) / max(ou.lensOval.w - ou.scatter.z, 1e-6), 0.0, 1.0);
3470
+ let kDeep = scatterFloor + (1.0 - scatterFloor) * deepT * deepT * (3.0 - 2.0 * deepT);
3471
+ let sDeep = 1.0 - kDeep;
3472
+ let rampT = max(1.0 - max(-d, 0.0) / max(ou.lensOval.z, 1e-6), 0.0);
3473
+ let farT = clamp((span - ou.scatter.w) / max(ou.lensOval.w - ou.scatter.w, 1e-6), 0.0, 1.0);
3474
+ let farS = farT * farT * (3.0 - 2.0 * farT);
3475
+ let rampStart = ou.scatter.y + (ou.shadowSize.z - ou.scatter.y) * sizeThick
3476
+ + (ou.shadowThick.w - ou.shadowSize.z) * farS;
3477
+ let sharpShare = clamp(sDeep + max(rampStart - sDeep, 0.0) * rampT, 0.0, 1.0);
3478
+ let kScatter = clamp(scatterFloor + ((1.0 - sharpShare) - scatterFloor) * fold, 0.0, 1.0);
3479
+ // The inner shadow's depth and profile: W2's law, byte for byte \u2014 the
3480
+ // thickness times the size gain (folded through 'sizeK'), clamped to the
3481
+ // shorter half extent, and a square on it. The lens ran on this until W12 G2;
3482
+ // the occlusion keeps it, because nothing measured it as wrong, and every
3483
+ // solid-backdrop cell renders exactly as it did.
3484
+ let shadowLensDepth = max(min(lensThick * (1.0 + (ou.lens.z - 1.0) * sizeK), span * 0.5), 1e-4);
3485
+ let shadowT = clamp(-d / shadowLensDepth, 0.0, 1.0);
3486
+ let shadowProfile = (1.0 - shadowT) * (1.0 - shadowT);
3487
+
3488
+ // The lens (W12 G2, claims \xA75.51) \u2014 see the module note. The reference's
3489
+ // height and amount laws off the span, scaled by the thickness over the
3490
+ // reference's unit, folded like the W2 law (at fold 0 the depth is the
3491
+ // authored thickness and nothing more), and clamped to the shorter half
3492
+ // extent with the magnitude clamped by the same ratio.
3493
+ let thickScale = lensThick / max(ou.lensShape.w, 1e-4);
3494
+ let heightBase = min(ou.lensLaw.x * span, ou.lensLaw.y);
3495
+ let amountBase = min(ou.lensLaw.z * span, ou.lensLaw.w);
3496
+ let depthUnclamped = mix(lensThick, heightBase * thickScale, fold);
3497
+ let lensDepth = clamp(depthUnclamped, 0.0, span * 0.5);
3498
+ let clampRatio = select(0.0, lensDepth / depthUnclamped, depthUnclamped > 1e-6);
3499
+ let magnitude = ou.lens.y * mix(lensThick, amountBase * thickScale, fold) * clampRatio;
3500
+ let extent = max(ou.lensShape.x * lensDepth, 1e-4);
3501
+ // One steep power over the extent; '-d' is depth inside the surface.
3502
+ let lensT = max(1.0 - max(-d, 0.0) / extent, 0.0);
3503
+ let displacementCss = magnitude * pow(lensT, ou.lensShape.y) * ou.lens.x;
3504
+
3505
+ // The direction: the field's gradient blended toward the oval inscribed in
3506
+ // the surface's box (the reference's 'gradientOvalization'), on from the
3507
+ // knee, with the magnitude fixed \u2014 which tilts the displacement toward the
3508
+ // edge's midpoint and magnifies the band along the edge.
3509
+ let ovalT = clamp((span - ou.lensOval.x) / max(ou.lensOval.y - ou.lensOval.x, 1e-6), 0.0, 1.0);
3510
+ let omega = ou.lensShape.z * ovalT * ovalT * (3.0 - 2.0 * ovalT);
3511
+ let halfExt = max(aux2.zw, vec2f(1e-4));
3512
+ let rel = aux2.xy;
3513
+ let unitR = max(length(rel / halfExt), 1e-6);
3514
+ let ovalGrad = (rel / (halfExt * halfExt)) * (min(halfExt.x, halfExt.y) / unitR);
3515
+ let blended = (1.0 - omega) * normal + omega * ovalGrad;
3516
+ let blendLen = length(blended);
3517
+ let direction = select(normal, blended / blendLen, blendLen > 1e-6);
3518
+ let displaceCss = -direction * displacementCss;
2912
3519
  let refracted01 = viewport01 + displaceCss / viewportCss;
2913
3520
 
2914
3521
  let refractedUv = clamp(refracted01 * ou.fit.xy + ou.fit.zw, vec2f(0.0), vec2f(1.0));
@@ -2926,7 +3533,13 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
2926
3533
  * by a share that is \u2248 0.4 on a small control and rises with the span. The
2927
3534
  * mix, not the level, is what the span moves.
2928
3535
  */
2929
- let scatterLod = clamp(ou.size.w + log2(max(ou.size.x, 1e-4)), 0.0, ou.lens.w);
3536
+ // The gain is per PIXEL and not one number per group since W15 G1's re-form
3537
+ // (claims \xA75.70 \xA74 and \xA77): the reference's heavy kernel grows with the span,
3538
+ // so the gain rises from its near end 'size.x' to its far end 'shadowSize.w'
3539
+ // along 'farS' \u2014 the very smoothstep the ramp's far anchor already declines
3540
+ // along, computed above with the share. One curve, two quantities.
3541
+ let gainEff = ou.size.x + (ou.shadowSize.w - ou.size.x) * farS;
3542
+ let scatterLod = clamp(ou.size.w + log2(max(gainEff, 1e-4)), 0.0, ou.lens.w);
2930
3543
 
2931
3544
  var backdrop = vec3f(0.0);
2932
3545
  if (ou.flags.x > 0.5) {
@@ -3160,7 +3773,7 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
3160
3773
  // A multiplicative occlusion of the whole composite. As a layer that is the
3161
3774
  // colour scaled and the alpha raised \u2014 (k\xB7a\xB7c, 1 \u2212 k\xB7(1 \u2212 a)) composites to
3162
3775
  // 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;
3776
+ let shadowKeep = 1.0 - shadowProfile * shadowDepth * ou.light.w * present;
3164
3777
  let shadowedAlpha = 1.0 - shadowKeep * (1.0 - bodyAlpha);
3165
3778
  colour = colour * (shadowKeep * bodyAlpha / max(shadowedAlpha, 1e-6));
3166
3779
  bodyAlpha = shadowedAlpha;
@@ -3198,7 +3811,10 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
3198
3811
  // an opaque body the two are the same quantity, which is how the shadow was
3199
3812
  // first written and why the difference only surfaced with the layer form.)
3200
3813
  let body = encode_output(max(colour, vec3f(0.0)), coverage * bodyAlpha);
3201
- return vec4f(body.rgb, body.a + shadowAlpha * (1.0 - coverage));
3814
+ return vec4f(
3815
+ body.rgb + liftEncoded * (1.0 - coverage),
3816
+ body.a + shadowAlpha * (1.0 - coverage),
3817
+ );
3202
3818
  }`;
3203
3819
 
3204
3820
  // ../renderer-webgpu/dist/wgsl/prelude.js
@@ -3326,7 +3942,11 @@ function createPassRunner(context) {
3326
3942
  fragment: {
3327
3943
  module,
3328
3944
  entryPoint: "fs_field",
3329
- targets: [{ format: WORKING_TEXTURE_FORMAT }, { format: WORKING_TEXTURE_FORMAT }]
3945
+ targets: [
3946
+ { format: WORKING_TEXTURE_FORMAT },
3947
+ { format: WORKING_TEXTURE_FORMAT },
3948
+ { format: WORKING_TEXTURE_FORMAT }
3949
+ ]
3330
3950
  },
3331
3951
  primitive: { topology: "triangle-list" }
3332
3952
  };
@@ -3390,6 +4010,13 @@ function createPassRunner(context) {
3390
4010
  usage: fieldUsage(),
3391
4011
  label: `vitrea:group:${args.groupId}:aux`
3392
4012
  });
4013
+ const aux2 = pool.acquire(poolKey.groupAux2(args.groupId), {
4014
+ width,
4015
+ height,
4016
+ format: WORKING_TEXTURE_FORMAT,
4017
+ usage: fieldUsage(),
4018
+ label: `vitrea:group:${args.groupId}:aux2`
4019
+ });
3393
4020
  const slot = uniformSlot(`field:${args.groupId}`, 12);
3394
4021
  slot.data[0] = rectWidth;
3395
4022
  slot.data[1] = rectHeight;
@@ -3423,6 +4050,12 @@ function createPassRunner(context) {
3423
4050
  loadOp: "clear",
3424
4051
  storeOp: "store",
3425
4052
  clearValue: { r: 0, g: 0, b: 0, a: 0 }
4053
+ },
4054
+ {
4055
+ view: aux2.createView(),
4056
+ loadOp: "clear",
4057
+ storeOp: "store",
4058
+ clearValue: { r: 0, g: 0, b: 0, a: 0 }
3426
4059
  }
3427
4060
  ]
3428
4061
  });
@@ -3439,13 +4072,14 @@ function createPassRunner(context) {
3439
4072
  return {
3440
4073
  field,
3441
4074
  aux,
4075
+ aux2,
3442
4076
  width,
3443
4077
  height,
3444
4078
  upsampled: width !== rectWidth || height !== rectHeight
3445
4079
  };
3446
4080
  },
3447
4081
  opticsPass(encoder, args) {
3448
- const slot = uniformSlot(`optics:${args.groupId}`, 76);
4082
+ const slot = uniformSlot(`optics:${args.groupId}`, 96);
3449
4083
  const d = slot.data;
3450
4084
  d[0] = args.viewportDevice[0];
3451
4085
  d[1] = args.viewportDevice[1];
@@ -3457,7 +4091,7 @@ function createPassRunner(context) {
3457
4091
  d[7] = args.fit[3];
3458
4092
  d[8] = args.refractionScale;
3459
4093
  d[9] = args.lensRefractionGain;
3460
- d[10] = 0;
4094
+ d[10] = args.shadowDepthSizeGainMax;
3461
4095
  d[11] = args.chainMaxLod;
3462
4096
  d[12] = args.tint[0];
3463
4097
  d[13] = args.tint[1];
@@ -3505,8 +4139,8 @@ function createPassRunner(context) {
3505
4139
  d[55] = args.outerShadow[3];
3506
4140
  d[56] = args.outerShadowSizeGain;
3507
4141
  d[57] = args.outerShadowRectCssHeight;
3508
- d[58] = 0;
3509
- d[59] = 0;
4142
+ d[58] = args.sizeScatterRampStartThick;
4143
+ d[59] = args.sizeScatterGainFar;
3510
4144
  d[60] = args.backdropToneAnchorX[0];
3511
4145
  d[61] = args.backdropToneAnchorX[1];
3512
4146
  d[62] = args.backdropToneAnchorX[2];
@@ -3520,9 +4154,29 @@ function createPassRunner(context) {
3520
4154
  d[70] = args.backdropToneResponseThick[2];
3521
4155
  d[71] = 0;
3522
4156
  d[72] = args.sizeScatterFloor;
3523
- d[73] = args.sizeScatterSpanMax;
4157
+ d[73] = args.sizeScatterRampStartThin;
3524
4158
  d[74] = args.sizeSpanMin;
3525
4159
  d[75] = args.sizeSpanMax;
4160
+ d[76] = args.lensHeightPerSpan;
4161
+ d[77] = args.lensHeightMax;
4162
+ d[78] = args.lensAmountPerSpan;
4163
+ d[79] = args.lensAmountMax;
4164
+ d[80] = args.lensExtentGain;
4165
+ d[81] = args.lensProfileExponent;
4166
+ d[82] = args.lensOvalization;
4167
+ d[83] = args.lensThicknessReference;
4168
+ d[84] = args.lensOvalizationSpanMin;
4169
+ d[85] = args.lensOvalizationSpanMax;
4170
+ d[86] = args.sizeScatterRampReachCssPx;
4171
+ d[87] = args.sizeScatterSpanMax;
4172
+ d[88] = args.outerShadowThick[0];
4173
+ d[89] = args.outerShadowThick[1];
4174
+ d[90] = args.outerShadowThick[2];
4175
+ d[91] = args.sizeScatterRampStartFar;
4176
+ d[92] = args.outerShadowLift[0];
4177
+ d[93] = args.outerShadowLift[1];
4178
+ d[94] = args.outerShadowLift[2];
4179
+ d[95] = args.outerShadowLift[3];
3526
4180
  slot.write();
3527
4181
  const chain = args.backdrop?.chain ?? placeholderView;
3528
4182
  const body = args.backdrop?.body ?? placeholderView;
@@ -3545,7 +4199,8 @@ function createPassRunner(context) {
3545
4199
  { binding: 5, resource: body },
3546
4200
  // Linear, no mips: the field targets have one level, and this is only
3547
4201
  // read at all when the governor shrank them below the group's rect.
3548
- { binding: 6, resource: context.flatSampler }
4202
+ { binding: 6, resource: context.flatSampler },
4203
+ { binding: 7, resource: args.fields.aux2.createView() }
3549
4204
  ]
3550
4205
  }));
3551
4206
  pass.draw(3);
@@ -3628,6 +4283,7 @@ function createPassRunner(context) {
3628
4283
  forget(groupId) {
3629
4284
  pool.release(poolKey.groupField(groupId));
3630
4285
  pool.release(poolKey.groupAux(groupId));
4286
+ pool.release(poolKey.groupAux2(groupId));
3631
4287
  storages.get(groupId)?.destroy();
3632
4288
  storages.delete(groupId);
3633
4289
  for (const key of [`field:${groupId}`, `optics:${groupId}`, `highlight:${groupId}`]) {
@@ -3773,10 +4429,8 @@ function createRebuildLedger() {
3773
4429
  // ../renderer-webgpu/dist/pyramid.js
3774
4430
  var chainUsage = () => GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING;
3775
4431
  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
- };
4432
+ var same = (next, existing) => Math.abs(next - existing) <= 1e-6 * Math.max(1, Math.abs(existing));
4433
+ var sameBody = (existing, request) => same(densityOf({ width: existing.sourceWidth, height: existing.sourceHeight }, request), existing.texelsPerCss) && same(request.bodySigmaCss, existing.bodySigmaCss);
3780
4434
  function createPyramidStore(context) {
3781
4435
  const { device, pool, cache } = context;
3782
4436
  const resources = /* @__PURE__ */ new Map();
@@ -3852,7 +4506,7 @@ function createPyramidStore(context) {
3852
4506
  return target;
3853
4507
  };
3854
4508
  const mipView = (texture, level) => texture.createView({ baseMipLevel: level, mipLevelCount: 1, dimension: "2d" });
3855
- function allocate(sourceId, plan, bodyLevel, sizeEpoch, builtEpoch, bodySigmaTexels, density) {
4509
+ function allocate(sourceId, plan, bodyLevel, sizeEpoch, builtEpoch, bodySigmaTexels, density, bodySigmaCss) {
3856
4510
  const existing = resources.get(sourceId);
3857
4511
  const bodyWidth = (plan.levels[bodyLevel] ?? plan.levels[0]).width;
3858
4512
  const bodyHeight = (plan.levels[bodyLevel] ?? plan.levels[0]).height;
@@ -3893,7 +4547,8 @@ function createPyramidStore(context) {
3893
4547
  bodySigmaTexels,
3894
4548
  texelsPerCss: density.texelsPerCss,
3895
4549
  sourceWidth: density.sourceWidth,
3896
- sourceHeight: density.sourceHeight
4550
+ sourceHeight: density.sourceHeight,
4551
+ bodySigmaCss
3897
4552
  };
3898
4553
  resources.set(sourceId, next);
3899
4554
  return next;
@@ -4081,7 +4736,7 @@ function createPyramidStore(context) {
4081
4736
  },
4082
4737
  build(request, provider, encoder) {
4083
4738
  const existing = liveResources(request.sourceId);
4084
- if (existing !== void 0 && existing.builtEpoch >= request.epoch && !provider.isDirty() && sameDensity(existing, request)) {
4739
+ if (existing !== void 0 && existing.builtEpoch >= request.epoch && !provider.isDirty() && sameBody(existing, request)) {
4085
4740
  ledger.recordClean();
4086
4741
  return { status: "clean", resources: existing };
4087
4742
  }
@@ -4103,7 +4758,7 @@ function createPyramidStore(context) {
4103
4758
  const planScale = frame.width > 0 ? plan.width / frame.width : 1;
4104
4759
  const bodySigmaTexels = request.bodySigmaCss * texelsPerCss * planScale;
4105
4760
  const bodyPlan = bodyBlurPlan(bodySigmaTexels, plan);
4106
- const target = allocate(request.sourceId, plan, bodyPlan.level, frame.sizeEpoch, request.epoch, bodySigmaTexels, { texelsPerCss, sourceWidth: frame.width, sourceHeight: frame.height });
4761
+ const target = allocate(request.sourceId, plan, bodyPlan.level, frame.sizeEpoch, request.epoch, bodySigmaTexels, { texelsPerCss, sourceWidth: frame.width, sourceHeight: frame.height }, request.bodySigmaCss);
4107
4762
  runImport(encoder, request.sourceId, frame, target.chain);
4108
4763
  runChain(encoder, request.sourceId, plan, target.chain);
4109
4764
  runBodyBlur(encoder, request.sourceId, plan, target.chain, target.body, bodyPlan.level, bodyPlan.residualSigmaTexels);
@@ -4316,7 +4971,14 @@ function createWebGPURenderer(options = {}) {
4316
4971
  const placement = placements.get(sourceId);
4317
4972
  return isUsablePlacement(placement) ? placementFit(placement, viewport.widthCss, viewport.heightCss) : coverFit(pyramid.plan.width, pyramid.plan.height, viewport.widthCss, viewport.heightCss);
4318
4973
  };
4319
- const densityRebuilds = (explicit, pyramids) => {
4974
+ const liftChainLod = (sigmaCss, pyramid) => {
4975
+ if (pyramid === void 0 || !(sigmaCss > 0))
4976
+ return 0;
4977
+ const planScale = pyramid.sourceWidth > 0 ? pyramid.plan.width / pyramid.sourceWidth : 1;
4978
+ const sigmaTexels = sigmaCss * pyramid.texelsPerCss * planScale;
4979
+ return Math.min(chainLodForSigma(sigmaTexels), pyramid.plan.maxLod);
4980
+ };
4981
+ const staleBodyRebuilds = (explicit, pyramids) => {
4320
4982
  const requests = [];
4321
4983
  const named = new Set(explicit.map((request) => request.sourceId));
4322
4984
  for (const sourceId of providers.keys()) {
@@ -4326,7 +4988,10 @@ function createWebGPURenderer(options = {}) {
4326
4988
  if (existing === void 0)
4327
4989
  continue;
4328
4990
  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))
4991
+ const sameDensity = Math.abs(next - existing.texelsPerCss) <= 1e-6 * Math.max(1, existing.texelsPerCss);
4992
+ const sigmaCss = bodySigmaCssFor(sourceId);
4993
+ const sameSigma = Math.abs(sigmaCss - existing.bodySigmaCss) <= 1e-6 * Math.max(1, existing.bodySigmaCss);
4994
+ if (sameDensity && sameSigma)
4330
4995
  continue;
4331
4996
  requests.push({
4332
4997
  sourceId,
@@ -4339,6 +5004,14 @@ function createWebGPURenderer(options = {}) {
4339
5004
  };
4340
5005
  const unionOf = (input) => input.union ?? DEFAULT_GROUP_UNION;
4341
5006
  const variantOf = (input) => input.variant ?? "regular";
5007
+ const sourceVariant = (sourceId) => {
5008
+ const input = [...groups.values()].find((entry) => entry.input.backdropSourceId === sourceId)?.input;
5009
+ return input === void 0 ? "regular" : variantOf(input);
5010
+ };
5011
+ const bodySigmaCssFor = (sourceId) => {
5012
+ const optics = opticsUnderPolicy(material.optics[sourceVariant(sourceId)], accessibility, material);
5013
+ return optics.blurSigma / Math.max(viewport.devicePixelRatio, 1e-3);
5014
+ };
4342
5015
  const groupTintSeed = (input) => input.surfaces.find((surface) => surface.fieldReferenceOnly !== true && (surface.tint?.strength ?? 0) > 0)?.tint?.color;
4343
5016
  const stateOf = (input, resolution) => {
4344
5017
  const resolved = resolution?.groups.find((group) => group.groupId === input.groupId);
@@ -4353,8 +5026,8 @@ function createWebGPURenderer(options = {}) {
4353
5026
  function rebuildRequests(explicit) {
4354
5027
  const base = explicit ?? standaloneRequests();
4355
5028
  const { store: pyramids } = ensureContext();
4356
- const density = densityRebuilds(base, pyramids);
4357
- return density.length === 0 ? base : [...base, ...density];
5029
+ const stale = staleBodyRebuilds(base, pyramids);
5030
+ return stale.length === 0 ? base : [...base, ...stale];
4358
5031
  }
4359
5032
  function standaloneRequests() {
4360
5033
  const sampled = /* @__PURE__ */ new Set();
@@ -4390,15 +5063,21 @@ function createWebGPURenderer(options = {}) {
4390
5063
  unbuilt.push(request.sourceId);
4391
5064
  continue;
4392
5065
  }
4393
- const variant = variantOf([...groups.values()].find((entry) => entry.input.backdropSourceId === request.sourceId)?.input ?? { });
4394
- const optics = opticsUnderPolicy(material.optics[variant], accessibility, material);
4395
5066
  lastResolution.set(request.sourceId, request.resolution);
4396
5067
  const placement = placements.get(request.sourceId);
4397
5068
  const outcome = pyramids.build({
4398
5069
  sourceId: request.sourceId,
4399
5070
  epoch: request.epoch,
4400
5071
  resolution: request.resolution,
4401
- bodySigmaCss: optics.blurSigma,
5072
+ // The body's sharp width is a DEVICE-pixel quantity (W12 G3, claims
5073
+ // §5.56; restored by W15 G1): `blurSigma` device px, so
5074
+ // `blurSigma / dpr` in the CSS px the pyramid converts to source
5075
+ // texels with. The heavy component rides the same conversion — its
5076
+ // level is this one plus log2(the gain at this ratio) octaves — so it
5077
+ // is the gain's own multiple of the sharp width in device px at every
5078
+ // scale. At dpr 1 this is `optics.blurSigma` unchanged, which is what
5079
+ // leaves the 1x law exactly as landed.
5080
+ bodySigmaCss: bodySigmaCssFor(request.sourceId),
4402
5081
  viewportCss: [viewport.widthCss, viewport.heightCss],
4403
5082
  ...isUsablePlacement(placement) ? { placement } : {}
4404
5083
  }, provider, encoder);
@@ -4449,11 +5128,12 @@ function createWebGPURenderer(options = {}) {
4449
5128
  }
4450
5129
  const union = unionOf(input);
4451
5130
  const shadow = outerShadowUnderPolicy(policy, material);
4452
- let reachOcclusion = shadow.occlusion;
5131
+ const shadowBackdropLuminance = input.backdropTone === void 0 ? void 0 : input.backdropToneLevel ?? relativeLuminance(input.backdropTone);
5132
+ let reachOcclusion = outerShadowThinOcclusion(shadowBackdropLuminance, shadow);
4453
5133
  for (const surface of surfaces) {
4454
- reachOcclusion = Math.max(reachOcclusion, sizeOuterShadowOcclusionAt(shadow.occlusion, surface.sizeThickness, material));
5134
+ reachOcclusion = Math.max(reachOcclusion, outerShadowOcclusionAt(shadow, shadowBackdropLuminance, surface.spanPx, surface.sizeThickness, material));
4455
5135
  }
4456
- const shadowReachPx = outerShadowReachPx({ ...shadow, occlusion: reachOcclusion });
5136
+ const shadowReachPx = outerShadowReachPx(shadow, reachOcclusion);
4457
5137
  const snapped = snapRectToDevicePixels(groupFieldRect(surfaces, union, void 0, shadowReachPx), dpr);
4458
5138
  const rectDevice = clipFieldRectToCanvas(snapped, dpr, viewportDevice);
4459
5139
  if (rectDevice === void 0)
@@ -4493,7 +5173,7 @@ function createWebGPURenderer(options = {}) {
4493
5173
  backdropToneSizeBiasUnderPolicy(policy, material),
4494
5174
  input.backdropTone === void 0 ? 0 : backdropToneUnderPolicy(policy, material) * material.backdropToneMax
4495
5175
  ];
4496
- const backdropToneLevel = input.backdropTone === void 0 ? 0 : input.backdropToneLevel ?? relativeLuminance(input.backdropTone);
5176
+ const backdropToneLevel = shadowBackdropLuminance ?? 0;
4497
5177
  const refraction = effectiveRefraction(accessibilityRefractionCap(policy), state.refraction);
4498
5178
  const refractionScale = material.refractionScale[refraction];
4499
5179
  passes.opticsPass(encoder, {
@@ -4508,6 +5188,19 @@ function createWebGPURenderer(options = {}) {
4508
5188
  fit: pyramid === void 0 || sourceId === void 0 ? [1, 1, 0, 0] : fitFor(sourceId, pyramid),
4509
5189
  refractionScale,
4510
5190
  lensRefractionGain: material.lensRefractionGain,
5191
+ // The lens law (W12 G2) and the inner shadow's own depth gain, both
5192
+ // evaluated per pixel in the shader from the thickness and the span.
5193
+ lensHeightPerSpan: material.lensHeightPerSpan,
5194
+ lensHeightMax: material.lensHeightMax,
5195
+ lensAmountPerSpan: material.lensAmountPerSpan,
5196
+ lensAmountMax: material.lensAmountMax,
5197
+ lensThicknessReference: material.lensThicknessReference,
5198
+ lensExtentGain: material.lensExtentGain,
5199
+ lensProfileExponent: material.lensProfileExponent,
5200
+ lensOvalization: material.lensOvalization,
5201
+ lensOvalizationSpanMin: material.lensOvalizationSpanMin,
5202
+ lensOvalizationSpanMax: material.lensOvalizationSpanMax,
5203
+ shadowDepthSizeGainMax: material.lensSizeGainMax,
4511
5204
  chainMaxLod: pyramid?.plan.maxLod ?? 0,
4512
5205
  tint: optics.tint,
4513
5206
  tintAlpha: optics.tintAlpha,
@@ -4540,7 +5233,20 @@ function createWebGPURenderer(options = {}) {
4540
5233
  // group whose members are not one size. `bodyChainLod` is where the body
4541
5234
  // blur already sits on the chain — only the pyramid that built it knows
4542
5235
  // the CSS-px-to-texel conversion, so it publishes the σ and this converts.
4543
- sizeScatterGainMax: material.sizeScatterGainMax,
5236
+ // The gain at the ratio this group draws at (W15 G1, claims §5.69 §1):
5237
+ // the shader's scatter level is `bodyChainLod + log2(gain)`, so this is
5238
+ // where the heavy width's second scale acts. `sizeScatterGainMax2x`
5239
+ // defaults to the 1x gain, so this is `material.sizeScatterGainMax`
5240
+ // until the sweep fits it.
5241
+ sizeScatterGainMax: scatterGainAtScale(material, dpr),
5242
+ // The gain's far end at the same ratio (W15 G1's re-form, claims §5.70
5243
+ // §4 and §7): the shader grades the gain from the near end at
5244
+ // `sizeSpanMax` to this at `sizeScatterSpanMax`, because the reference's
5245
+ // heavy kernel grows with the span and one gain left the largest span's
5246
+ // deep interior too structured. `sizeScatterGainFar2x` defaults to the
5247
+ // 2x gain's own default and interpolates from the 1x gain, so this is
5248
+ // `sizeScatterGainMax` at dpr 1 whatever the profile says.
5249
+ sizeScatterGainFar: scatterGainFarAtScale(material, dpr),
4544
5250
  sizeOcclusionGain: material.sizeOcclusionGain,
4545
5251
  sizeShadowGainMax: material.sizeShadowGainMax,
4546
5252
  bodyChainLod: chainLodForSigma(pyramid?.bodySigmaTexels ?? 0),
@@ -4549,8 +5255,26 @@ function createWebGPURenderer(options = {}) {
4549
5255
  // same fold `sizeThicknessUnderPolicy` applies on the CPU.
4550
5256
  sizeSpanMin: material.sizeSpanMin,
4551
5257
  sizeSpanMax: material.sizeSpanMax,
4552
- sizeScatterFloor: material.sizeScatterFloor,
4553
- sizeScatterSpanMax: material.sizeScatterSpanMax,
5258
+ // The deep value's floor and span top at this ratio (W15 G1, claims
5259
+ // §5.69 §2). Same two uniform slots the shader has always read for
5260
+ // `kDeep` and for the fold; only the value is resolved from the
5261
+ // viewport now, as the ramp's anchors already were.
5262
+ sizeScatterFloor: scatterFloorAtScale(material, dpr),
5263
+ sizeScatterSpanMax: scatterSpanMaxAtScale(material, dpr),
5264
+ // The body's depth ramp (W13 G1, claims §5.61 §2, §5.64 §5), resolved
5265
+ // from the viewport's own ratio: the profile anchors the start's thin
5266
+ // and thick ends and the reach at dpr 1 and dpr 2, and the CPU
5267
+ // interpolates all three in dpr. The start's SPAN grading stays in the
5268
+ // shader, because a group's members are not one size and the span
5269
+ // arrives per pixel on the field's own channel — so the two anchors go
5270
+ // over and `sizeThickness` is evaluated there. The reach is handed over
5271
+ // in CSS px because the field's depth is in CSS px.
5272
+ sizeScatterRampStartThin: scatterRampStart(dpr, material, 0),
5273
+ sizeScatterRampStartThick: scatterRampStart(dpr, material, material.sizeSpanMax),
5274
+ // The fourth form's far end, at the scatter span curve's top; the shader
5275
+ // declines from the thick end to it along that curve.
5276
+ sizeScatterRampStartFar: scatterRampStart(dpr, material, scatterSpanMaxAtScale(material, dpr)),
5277
+ sizeScatterRampReachCssPx: scatterRampReachDevicePx(dpr, material) / dpr,
4554
5278
  sizeFold: material.refractionScale[accessibilityRefractionCap(policy)],
4555
5279
  backdropTone,
4556
5280
  backdropToneColour: [
@@ -4579,7 +5303,11 @@ function createWebGPURenderer(options = {}) {
4579
5303
  backdropToneResponseStrength: backdropToneUnderPolicy(policy, material) >= 0.999 ? material.backdropToneResponseStrength : 0,
4580
5304
  backdropToneLinearMean: input.backdropToneLinearLuminance ?? backdropToneLevel,
4581
5305
  outerShadow: [
4582
- outerShadowAlpha(shadow.occlusion),
5306
+ // The thin regime's LINEAR occlusion at this group's backdrop (W14
5307
+ // G1). The conversion to the compositing space moved into the shader
5308
+ // with the regimes: it is not linear, so converting one regime here
5309
+ // and blending in the shader would not be the same law.
5310
+ outerShadowThinOcclusion(shadowBackdropLuminance, shadow),
4583
5311
  shadow.sigmaPx,
4584
5312
  shadow.spreadPx,
4585
5313
  // The offset in field-texture UV. The field spans exactly this rect, so
@@ -4587,6 +5315,19 @@ function createWebGPURenderer(options = {}) {
4587
5315
  // scaled the rasterisation.
4588
5316
  shadow.offsetPx / Math.max(rectDevice.height * cssPerDevice, 1e-6)
4589
5317
  ],
5318
+ outerShadowThick: [
5319
+ shadow.thickOcclusionAt96,
5320
+ shadow.thickOcclusionAt128,
5321
+ shadow.thickOcclusionAt160
5322
+ ],
5323
+ outerShadowLift: [
5324
+ // The lift needs a chain to copy; without one there is nothing to add,
5325
+ // and `hasBackdrop` in the shader says the same thing a second time.
5326
+ pyramid === void 0 || policy.glass === "none" ? 0 : shadow.liftAmplitude,
5327
+ shadow.liftSpanMin,
5328
+ shadow.liftSpanFull,
5329
+ liftChainLod(shadow.liftBlurSigmaCss, pyramid)
5330
+ ],
4590
5331
  outerShadowSizeGain: shadow.sizeGain,
4591
5332
  outerShadowRectCssHeight: rectDevice.height * cssPerDevice,
4592
5333
  backdrop: pyramid === void 0 || policy.glass === "none" ? void 0 : {
@@ -4868,6 +5609,6 @@ function createWebGPURenderer(options = {}) {
4868
5609
  };
4869
5610
  }
4870
5611
 
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
5612
+ 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_AMOUNT_MAX, LENS_AMOUNT_PER_SPAN, LENS_EXTENT_GAIN, LENS_HEIGHT_MAX, LENS_HEIGHT_PER_SPAN, LENS_OVALIZATION, LENS_PROFILE_EXPONENT, LENS_REFRACTION_GAIN, LENS_SIZE_GAIN_MAX, LENS_THICKNESS_REFERENCE, LUMINANCE_WEIGHTS, MATERIAL_OPTICS, MATERIAL_VARIANTS, MAX_CHAIN_LEVELS, MIN_LEVEL_EXTENT, NOMINAL_GOVERNOR, NOMINAL_MATERIAL_POLICY, OPTICS_PASS_ID, OUTER_SHADOW, OUTER_SHADOW_THICK_SPANS, OUTER_SHADOW_THIN_L, OUTER_SHADOW_UNMEASURED_BACKDROP_LUMINANCE, OUTPUT_TEXTURE_FORMAT, PASS_LABEL, REFRACTION_LADDER, REFRACTION_SCALE, RENDERER_ERROR_CODES, RENDERER_PASS_IDS, RendererError, SIZE_OCCLUSION_GAIN, SIZE_SCATTER_FLOOR, SIZE_SCATTER_FLOOR_2X, SIZE_SCATTER_GAIN_FAR_2X, SIZE_SCATTER_GAIN_MAX, SIZE_SCATTER_GAIN_MAX_2X, SIZE_SCATTER_RAMP_REACH_1X_PX, SIZE_SCATTER_RAMP_REACH_2X_PX, SIZE_SCATTER_RAMP_START_FAR_1X, SIZE_SCATTER_RAMP_START_FAR_2X, SIZE_SCATTER_RAMP_START_THICK_1X, SIZE_SCATTER_RAMP_START_THICK_2X, SIZE_SCATTER_RAMP_START_THIN_1X, SIZE_SCATTER_RAMP_START_THIN_2X, SIZE_SCATTER_SPAN_MAX, SIZE_SCATTER_SPAN_MAX_2X, 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, lensAmountBasePx, lensDepthPx, lensDirection, lensDisplacementPx, lensExtentPx, lensHeightBasePx, lensMagnitudePx, lensOvalizationAt, lensSizeGain, lensSizeGainFromThickness, linearGradientStops, linearToSrgb, linearToSrgbChannel, occlusionAlphaUnderPolicy, opticsModule, opticsUnderPolicy, outerShadowAlpha, outerShadowFalloff, outerShadowLiftRise, outerShadowOcclusionAt, outerShadowReachPx, outerShadowThickOcclusion, outerShadowThinOcclusion, outerShadowUnderPolicy, packInstances, pipelineKey, placementFit, planPyramid, poolKey, readbackDue, refractionRank, relativeLuminance, rendererError, resolveSurfaces, samePlacement, scatterDeepThickness, scatterFloorAtScale, scatterGainAt, scatterGainAtScale, scatterGainFarAtScale, scatterRampAreaMean, scatterRampReachDevicePx, scatterRampStart, scatterSharpShare, scatterSpanMaxAtScale, scatterThickness, shadowDepthPx, sizeOcclusionAlpha, sizeOcclusionAlphaAt, sizeOuterShadowOcclusion, sizeOuterShadowOcclusionAt, sizeScatterSigma, sizeScatterSigmaAt, sizeShadowDepth, sizeShadowDepthAt, sizeThickness, sizeThicknessUnderPolicy, snapRectToDevicePixels, srgbToLinear, srgbToLinearChannel, statsFromBuffer, supportsTimestamps, texelsPerCssPx, tintShade, tintShadeLayer, tintToneAdaptation, tintedMaterialColour, validateAppTexture, withMaterialOverrides };
5613
+ //# sourceMappingURL=dist-OHNXYEZP.js.map
5614
+ //# sourceMappingURL=dist-OHNXYEZP.js.map