@vitreajs/vitrea 0.3.0 → 0.4.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,64 @@ var DEFAULT_MATERIAL_PROFILE = {
431
433
  sizeScatterGainMax: 8,
432
434
  sizeScatterFloor: 0.4,
433
435
  sizeScatterSpanMax: 256,
436
+ // The 1x three FITTED in the renderer (W13 G1's third sweep, 44 points over
437
+ // the calibration bed: `results/2026-09-03-w13-ramp/g1/sweep-3/g1-sweep-3.md`
438
+ // §3); the 2x three PROVISIONAL still, because at them the excursion is
439
+ // bit-exactly zero on the whole bed and a sweep cannot fit what does not
440
+ // move (§4). The thin anchor 0.72 is above G0's read-off of 0.637–0.642: the
441
+ // grid runs 0.60 / 0.64 / 0.68 and the refinement 0.72 / 0.76, and 0.72 is an
442
+ // interior optimum of both the interior objective and the interior gap. The
443
+ // thick anchor 0.52 is G0's `rrect-md` reading and is above that cell's deep
444
+ // sharp share of 0.481, where the joint thick fit of 0.47 would have clamped
445
+ // it to nothing. The 2x pair is G0's 2x readings, every one of which is BELOW
446
+ // its cell's deep value, so at dpr 2 the excursion is zero on this whole bed
447
+ // — the law evaluating to no excursion, and the 2x gap located in the deep
448
+ // value instead (claims §5.64 §4). See
449
+ // `MaterialProfile.sizeScatterRampStartThin1x`.
450
+ sizeScatterRampStartThin1x: 0.72,
451
+ sizeScatterRampStartThick1x: 0.52,
452
+ sizeScatterRampStartFar1x: 0.2,
453
+ sizeScatterRampStartThin2x: 0.46,
454
+ sizeScatterRampStartThick2x: 0.17,
455
+ sizeScatterRampStartFar2x: 0.15,
456
+ sizeScatterRampReach1xPx: 80,
457
+ sizeScatterRampReach2xPx: 100,
434
458
  sizeOcclusionGain: 0.05,
435
459
  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,
460
+ /*
461
+ * The lens (W12 G2, claims §5.51; Decision Log 3 of the W12 spec).
462
+ *
463
+ * MEASURED from the reference's own layer tree (§5.50): the inner refraction
464
+ * amount and height laws, verified on spans 32 / 44 / 48 … 112 / 128 / 160
465
+ * and the nested base at 130, and the ovalization's knee (0 through span 64,
466
+ * 0.5 from 72). FITTED on the pixels (§5.51, a 2-D band renderer validated on
467
+ * this renderer's own capture): the gain, the extent and the exponent, on
468
+ * `rrect-md` + `-ml` at 1x pitches 16 and 32 with `rrect-lg` held out at both
469
+ * scales; the ovalization's effective value on the box-inscribed ellipse.
470
+ * Predicted before the landing capture: checkerboard SSIM 1x md / ml / lg
471
+ * 0.954 / 0.931 / 0.929 → 0.970 / 0.946 / 0.942, 2x 0.939 / 0.902 / 0.901 →
472
+ * 0.950 / 0.918 / 0.918, the capsule 0.977 → 0.985.
473
+ *
474
+ * What W11c G2 recorded here — 1.6 lens depths on the square profile, fitted
475
+ * per depth shell — was the band's mean, not its shape (§5.48, §5.49); it is
476
+ * kept in git history and in claims §5.43.
477
+ */
478
+ lensRefractionGain: 0.745,
479
+ lensHeightPerSpan: 0.25,
480
+ lensHeightMax: 20,
481
+ lensAmountPerSpan: 0.8,
482
+ lensAmountMax: 60,
483
+ lensThicknessReference: 8,
484
+ lensExtentGain: 1.337,
485
+ lensProfileExponent: 3.69,
486
+ // 0.8 by eye (W12 Decision Log 6). The pixels preferred 0.6 by a hair — every
487
+ // texture row that can see ω scores 0.001–0.002 lower at 0.8 (claims §5.54) —
488
+ // but the field's measured tilt sits at 0.8–1.0 (§5.49 §3) and the user read
489
+ // the 0.8 sheet as much closer to macOS; the eye overrides a margin that small,
490
+ // and §5.54 §1's rows are the recorded cost.
491
+ lensOvalization: 0.8,
492
+ lensOvalizationSpanMin: 64,
493
+ lensOvalizationSpanMax: 72,
444
494
  reducedTransparencyFrost: 1.75,
445
495
  increasedOcclusionLift: INCREASED_OCCLUSION_LIFT,
446
496
  strongBorderRim: { rimWidth: 2, rimAlpha: 0.95 },
@@ -568,14 +618,18 @@ var DEFAULT_MATERIAL_PROFILE = {
568
618
  * instrument's independent shadow axis agrees, so the geometry stands on those
569
619
  * two and this objective is not asked to re-decide it.
570
620
  *
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
621
+ * `reducedTransparencyOcclusion` 0.70 was sharp where the amplitude was flat: a
622
+ * 3.83× spread over 0.45…0.95 with a clear minimum. It also reconciled two
573
623
  * routes — 0.285 × 0.70 = 0.1995 against the reference's directly measured
574
624
  * 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
625
+ * ratio of the reference's two amplitudes; that was the ratio that made
576
626
  * 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.
627
+ * differed because the base amplitude is a compromise across scenes whose
628
+ * spread is much wider in the standard profile than under reduced transparency.
629
+ * **W14 G1 re-forms the constant as the LEVEL those routes were reconciling
630
+ * to** — 0.197 absolute, replacing both regimes rather than scaling them — for
631
+ * the reason its doc comment gives: a ratio and a level stopped being the same
632
+ * thing when the single amplitude became six unequal anchors.
579
633
  *
580
634
  * Extracted from the active bed's native fixtures directly —
581
635
  * `results/2026-08-31-active-bed-stage0.json` measured the gap, these numbers
@@ -614,13 +668,33 @@ var DEFAULT_MATERIAL_PROFILE = {
614
668
  * 44 px span — the dark material's shadow is nearly invisible) and lands as a
615
669
  * profile patch, not as a branch here. Both are stated in the claims doc as the
616
670
  * open question the cascade's fit inherits.
671
+ *
672
+ * **W14 G1 answers that open question and replaces `occlusion`.** "Over the
673
+ * flat near-white `light-solid` backdrop the same fit reads 0.123 rather than
674
+ * 0.33 … it is not a function of the backdrop's luminance" was read on a bed
675
+ * that had no backdrop between `hc-text` (linear 0.74) and `light-solid`
676
+ * (0.891): it IS a function of the backdrop's luminance, and the whole factor
677
+ * of 2.6 happens inside the gap the bed cannot see (claims §5.62 §5). The
678
+ * single amplitude becomes six anchors on two regimes — three in backdrop
679
+ * luminance below the knee, three in span above it — plus the lift's four
680
+ * constants. The three lengths are untouched; G0 re-read them free on both
681
+ * terms and they came back at W8's values.
617
682
  */
618
683
  outerShadow: {
619
684
  offsetPx: 7.95,
620
685
  sigmaPx: 15.55,
621
686
  spreadPx: 3.1,
622
- occlusion: 0.285,
623
- reducedTransparencyOcclusion: 0.7,
687
+ thinOcclusionDark: 0,
688
+ thinOcclusionMid: 0.33,
689
+ thinOcclusionBright: 0.127,
690
+ thickOcclusionAt96: 0.37,
691
+ thickOcclusionAt128: 0.448,
692
+ thickOcclusionAt160: 0.479,
693
+ liftAmplitude: 0.01,
694
+ liftSpanMin: 64,
695
+ liftSpanFull: 118,
696
+ liftBlurSigmaCss: 40,
697
+ reducedTransparencyOcclusion: 0.197,
624
698
  sizeGain: 0
625
699
  },
626
700
  lightDirection: [-0.3714, -0.9285],
@@ -641,9 +715,25 @@ var LENS_SIZE_GAIN_MAX = DEFAULT_MATERIAL_PROFILE.lensSizeGainMax;
641
715
  var SIZE_SCATTER_GAIN_MAX = DEFAULT_MATERIAL_PROFILE.sizeScatterGainMax;
642
716
  var SIZE_SCATTER_FLOOR = DEFAULT_MATERIAL_PROFILE.sizeScatterFloor;
643
717
  var SIZE_SCATTER_SPAN_MAX = DEFAULT_MATERIAL_PROFILE.sizeScatterSpanMax;
718
+ var SIZE_SCATTER_RAMP_START_THIN_1X = DEFAULT_MATERIAL_PROFILE.sizeScatterRampStartThin1x;
719
+ var SIZE_SCATTER_RAMP_START_THICK_1X = DEFAULT_MATERIAL_PROFILE.sizeScatterRampStartThick1x;
720
+ var SIZE_SCATTER_RAMP_START_THIN_2X = DEFAULT_MATERIAL_PROFILE.sizeScatterRampStartThin2x;
721
+ var SIZE_SCATTER_RAMP_START_THICK_2X = DEFAULT_MATERIAL_PROFILE.sizeScatterRampStartThick2x;
722
+ var SIZE_SCATTER_RAMP_START_FAR_1X = DEFAULT_MATERIAL_PROFILE.sizeScatterRampStartFar1x;
723
+ var SIZE_SCATTER_RAMP_START_FAR_2X = DEFAULT_MATERIAL_PROFILE.sizeScatterRampStartFar2x;
724
+ var SIZE_SCATTER_RAMP_REACH_1X_PX = DEFAULT_MATERIAL_PROFILE.sizeScatterRampReach1xPx;
725
+ var SIZE_SCATTER_RAMP_REACH_2X_PX = DEFAULT_MATERIAL_PROFILE.sizeScatterRampReach2xPx;
644
726
  var SIZE_OCCLUSION_GAIN = DEFAULT_MATERIAL_PROFILE.sizeOcclusionGain;
645
727
  var SIZE_SHADOW_GAIN_MAX = DEFAULT_MATERIAL_PROFILE.sizeShadowGainMax;
646
728
  var LENS_REFRACTION_GAIN = DEFAULT_MATERIAL_PROFILE.lensRefractionGain;
729
+ var LENS_HEIGHT_PER_SPAN = DEFAULT_MATERIAL_PROFILE.lensHeightPerSpan;
730
+ var LENS_HEIGHT_MAX = DEFAULT_MATERIAL_PROFILE.lensHeightMax;
731
+ var LENS_AMOUNT_PER_SPAN = DEFAULT_MATERIAL_PROFILE.lensAmountPerSpan;
732
+ var LENS_AMOUNT_MAX = DEFAULT_MATERIAL_PROFILE.lensAmountMax;
733
+ var LENS_THICKNESS_REFERENCE = DEFAULT_MATERIAL_PROFILE.lensThicknessReference;
734
+ var LENS_EXTENT_GAIN = DEFAULT_MATERIAL_PROFILE.lensExtentGain;
735
+ var LENS_PROFILE_EXPONENT = DEFAULT_MATERIAL_PROFILE.lensProfileExponent;
736
+ var LENS_OVALIZATION = DEFAULT_MATERIAL_PROFILE.lensOvalization;
647
737
  var BACKDROP_TONE_MAX = DEFAULT_MATERIAL_PROFILE.backdropToneMax;
648
738
  var BACKDROP_TONE_LOW = DEFAULT_MATERIAL_PROFILE.backdropToneLow;
649
739
  var BACKDROP_TONE_HIGH = DEFAULT_MATERIAL_PROFILE.backdropToneHigh;
@@ -653,7 +743,20 @@ DEFAULT_MATERIAL_PROFILE.backdropToneResponseThin;
653
743
  DEFAULT_MATERIAL_PROFILE.backdropToneResponseThick;
654
744
  DEFAULT_MATERIAL_PROFILE.backdropToneResponseStrength;
655
745
  var OUTER_SHADOW = DEFAULT_MATERIAL_PROFILE.outerShadow;
746
+ var RETIRED_OUTER_SHADOW_LEAVES = {
747
+ occlusion: "the six amplitude anchors (thinOcclusionDark, thinOcclusionMid, thinOcclusionBright, thickOcclusionAt96, thickOcclusionAt128, thickOcclusionAt160) and liftAmplitude for the second term"
748
+ };
749
+ function rejectRetiredOuterShadowLeaves(patch) {
750
+ if (patch === void 0)
751
+ return;
752
+ for (const [leaf, replacement] of Object.entries(RETIRED_OUTER_SHADOW_LEAVES)) {
753
+ if (!(leaf in patch))
754
+ continue;
755
+ 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.`);
756
+ }
757
+ }
656
758
  function withMaterialOverrides(base, patch) {
759
+ rejectRetiredOuterShadowLeaves(patch.outerShadow);
657
760
  const optics = {};
658
761
  for (const variant of MATERIAL_VARIANTS) {
659
762
  optics[variant] = { ...base.optics[variant], ...patch.optics?.[variant] };
@@ -675,9 +778,27 @@ function withMaterialOverrides(base, patch) {
675
778
  sizeScatterGainMax: patch.sizeScatterGainMax ?? base.sizeScatterGainMax,
676
779
  sizeScatterFloor: patch.sizeScatterFloor ?? base.sizeScatterFloor,
677
780
  sizeScatterSpanMax: patch.sizeScatterSpanMax ?? base.sizeScatterSpanMax,
781
+ sizeScatterRampStartThin1x: patch.sizeScatterRampStartThin1x ?? base.sizeScatterRampStartThin1x,
782
+ sizeScatterRampStartThick1x: patch.sizeScatterRampStartThick1x ?? base.sizeScatterRampStartThick1x,
783
+ sizeScatterRampStartThin2x: patch.sizeScatterRampStartThin2x ?? base.sizeScatterRampStartThin2x,
784
+ sizeScatterRampStartThick2x: patch.sizeScatterRampStartThick2x ?? base.sizeScatterRampStartThick2x,
785
+ sizeScatterRampStartFar1x: patch.sizeScatterRampStartFar1x ?? base.sizeScatterRampStartFar1x,
786
+ sizeScatterRampStartFar2x: patch.sizeScatterRampStartFar2x ?? base.sizeScatterRampStartFar2x,
787
+ sizeScatterRampReach1xPx: patch.sizeScatterRampReach1xPx ?? base.sizeScatterRampReach1xPx,
788
+ sizeScatterRampReach2xPx: patch.sizeScatterRampReach2xPx ?? base.sizeScatterRampReach2xPx,
678
789
  sizeOcclusionGain: patch.sizeOcclusionGain ?? base.sizeOcclusionGain,
679
790
  sizeShadowGainMax: patch.sizeShadowGainMax ?? base.sizeShadowGainMax,
680
791
  lensRefractionGain: patch.lensRefractionGain ?? base.lensRefractionGain,
792
+ lensHeightPerSpan: patch.lensHeightPerSpan ?? base.lensHeightPerSpan,
793
+ lensHeightMax: patch.lensHeightMax ?? base.lensHeightMax,
794
+ lensAmountPerSpan: patch.lensAmountPerSpan ?? base.lensAmountPerSpan,
795
+ lensAmountMax: patch.lensAmountMax ?? base.lensAmountMax,
796
+ lensThicknessReference: patch.lensThicknessReference ?? base.lensThicknessReference,
797
+ lensExtentGain: patch.lensExtentGain ?? base.lensExtentGain,
798
+ lensProfileExponent: patch.lensProfileExponent ?? base.lensProfileExponent,
799
+ lensOvalization: patch.lensOvalization ?? base.lensOvalization,
800
+ lensOvalizationSpanMin: patch.lensOvalizationSpanMin ?? base.lensOvalizationSpanMin,
801
+ lensOvalizationSpanMax: patch.lensOvalizationSpanMax ?? base.lensOvalizationSpanMax,
681
802
  reducedTransparencyFrost: patch.reducedTransparencyFrost ?? base.reducedTransparencyFrost,
682
803
  increasedOcclusionLift: patch.increasedOcclusionLift ?? base.increasedOcclusionLift,
683
804
  strongBorderRim: { ...base.strongBorderRim, ...patch.strongBorderRim },
@@ -810,15 +931,102 @@ function lensSizeGain(spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
810
931
  function lensSizeGainFromThickness(thickness, profile = DEFAULT_MATERIAL_PROFILE) {
811
932
  return 1 + (profile.lensSizeGainMax - 1) * thickness;
812
933
  }
813
- function sizeScatterSigma(sigmaPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
814
- return sizeScatterSigmaAt(sigmaPx, scatterThickness(spanPx, 1, profile), profile);
934
+ function shadowDepthPx(thicknessPx, spanPx, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
935
+ return Math.min(Math.max(thicknessPx, 0) * lensSizeGainFromThickness(thickness, profile), spanPx * 0.5);
936
+ }
937
+ function lensHeightBasePx(spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
938
+ return Math.min(profile.lensHeightPerSpan * Math.max(spanPx, 0), profile.lensHeightMax);
939
+ }
940
+ function lensAmountBasePx(spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
941
+ return Math.min(profile.lensAmountPerSpan * Math.max(spanPx, 0), profile.lensAmountMax);
942
+ }
943
+ function lensOvalizationAt(spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
944
+ return profile.lensOvalization * smoothstep(profile.lensOvalizationSpanMin, profile.lensOvalizationSpanMax, spanPx);
945
+ }
946
+ function lensMagnitudePx(thicknessPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE, fold = 1) {
947
+ const thick = Math.max(thicknessPx, 0);
948
+ if (thick <= 0)
949
+ return 0;
950
+ const scale = thick / profile.lensThicknessReference;
951
+ const unclamped = thick + (lensHeightBasePx(spanPx, profile) * scale - thick) * fold;
952
+ const depth = lensDepthPx(thicknessPx, spanPx, profile, fold);
953
+ const amount = thick + (lensAmountBasePx(spanPx, profile) * scale - thick) * fold;
954
+ return profile.lensRefractionGain * amount * (unclamped > 0 ? depth / unclamped : 0);
955
+ }
956
+ function lensExtentPx(thicknessPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE, fold = 1) {
957
+ return profile.lensExtentGain * lensDepthPx(thicknessPx, spanPx, profile, fold);
958
+ }
959
+ function lensDirection(normal, offset, half, ovalization) {
960
+ const a = Math.max(half[0], 1e-6);
961
+ const b = Math.max(half[1], 1e-6);
962
+ const ex = offset[0] / (a * a);
963
+ const ey = offset[1] / (b * b);
964
+ const r = Math.max(Math.hypot(offset[0] / a, offset[1] / b), 1e-6);
965
+ const scale = Math.min(a, b) / r;
966
+ const gx = (1 - ovalization) * normal[0] + ovalization * ex * scale;
967
+ const gy = (1 - ovalization) * normal[1] + ovalization * ey * scale;
968
+ const len = Math.hypot(gx, gy);
969
+ return len > 1e-9 ? [gx / len, gy / len] : [normal[0], normal[1]];
970
+ }
971
+ function sizeScatterSigma(sigmaPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE, devicePixelRatio = 1, extentsCssPx) {
972
+ return sizeScatterSigmaAt(sigmaPx, scatterThickness(spanPx, 1, profile, devicePixelRatio, extentsCssPx), profile);
973
+ }
974
+ function scatterRampStart(devicePixelRatio, profile = DEFAULT_MATERIAL_PROFILE, spanPx = 0) {
975
+ const thin = rampAtScale(profile.sizeScatterRampStartThin1x, profile.sizeScatterRampStartThin2x, devicePixelRatio);
976
+ const thick = rampAtScale(profile.sizeScatterRampStartThick1x, profile.sizeScatterRampStartThick2x, devicePixelRatio);
977
+ const far = rampAtScale(profile.sizeScatterRampStartFar1x, profile.sizeScatterRampStartFar2x, devicePixelRatio);
978
+ const decline = smoothstep(profile.sizeSpanMax, profile.sizeScatterSpanMax, spanPx);
979
+ return thin + (thick - thin) * sizeThickness(spanPx, profile) + (far - thick) * decline;
980
+ }
981
+ function scatterRampReachDevicePx(devicePixelRatio, profile = DEFAULT_MATERIAL_PROFILE) {
982
+ return rampAtScale(profile.sizeScatterRampReach1xPx, profile.sizeScatterRampReach2xPx, devicePixelRatio);
983
+ }
984
+ function rampAtScale(at1x, at2x, devicePixelRatio) {
985
+ const t = devicePixelRatio - 1;
986
+ if (t <= 0)
987
+ return at1x;
988
+ if (t >= 1)
989
+ return at2x;
990
+ return at1x + (at2x - at1x) * t;
991
+ }
992
+ function scatterDeepThickness(spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
993
+ const floor = clampUnit(profile.sizeScatterFloor);
994
+ return floor + (1 - floor) * smoothstep(profile.sizeSpanMin, profile.sizeScatterSpanMax, spanPx);
815
995
  }
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;
996
+ function scatterSharpShare(uDevicePx, devicePixelRatio, profile = DEFAULT_MATERIAL_PROFILE, spanPx = 0) {
997
+ const deepSharp = 1 - scatterDeepThickness(spanPx, profile);
998
+ const start = scatterRampStart(devicePixelRatio, profile, spanPx);
999
+ const reach = Math.max(scatterRampReachDevicePx(devicePixelRatio, profile), 1e-6);
1000
+ const excursion = Math.max(start - deepSharp, 0) * Math.max(1 - Math.max(uDevicePx, 0) / reach, 0);
1001
+ return clampUnit(deepSharp + excursion);
1002
+ }
1003
+ function scatterThickness(spanPx, fold, profile = DEFAULT_MATERIAL_PROFILE, devicePixelRatio = 1, extentsCssPx) {
1004
+ const floor = clampUnit(profile.sizeScatterFloor);
1005
+ const mean = scatterRampAreaMean(spanPx, profile, devicePixelRatio, extentsCssPx);
1006
+ return clampUnit(floor + (mean - floor) * fold);
1007
+ }
1008
+ function scatterRampAreaMean(spanPx, profile = DEFAULT_MATERIAL_PROFILE, devicePixelRatio = 1, extentsCssPx) {
1009
+ const deep = scatterDeepThickness(spanPx, profile);
1010
+ const amplitude = Math.max(scatterRampStart(devicePixelRatio, profile, spanPx) - (1 - deep), 0);
1011
+ if (amplitude <= 0)
1012
+ return clampUnit(deep);
1013
+ const width = Math.max(extentsCssPx?.[0] ?? spanPx, 0);
1014
+ const height = Math.max(extentsCssPx?.[1] ?? spanPx, 0);
1015
+ const area = width * height;
1016
+ if (area <= 0)
1017
+ return clampUnit(deep - amplitude);
1018
+ const reach = Math.max(scatterRampReachDevicePx(devicePixelRatio, profile) / Math.max(devicePixelRatio, 1e-3), 1e-6);
1019
+ const perimeter = 2 * (width + height);
1020
+ const limit = Math.min(reach, Math.min(width, height) / 2);
1021
+ const triangleMean = (perimeter * limit - 4 * limit * limit - perimeter * limit * limit / (2 * reach) + 8 * limit * limit * limit / (3 * reach)) / area;
1022
+ return clampUnit(deep - amplitude * triangleMean);
819
1023
  }
820
1024
  function sizeScatterSigmaAt(sigmaPx, scatter, profile = DEFAULT_MATERIAL_PROFILE) {
821
- return sigmaPx * (1 + (profile.sizeScatterGainMax - 1) * scatter);
1025
+ const mix = clampUnit(scatter);
1026
+ return sigmaPx * (1 + (profile.sizeScatterGainMax - 1) * mix);
1027
+ }
1028
+ function clampUnit(value) {
1029
+ return Math.min(1, Math.max(0, value));
822
1030
  }
823
1031
  function sizeOcclusionAlpha(alpha, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
824
1032
  return sizeOcclusionAlphaAt(alpha, sizeThickness(spanPx, profile), profile);
@@ -838,15 +1046,79 @@ function sizeOuterShadowOcclusion(occlusion, spanPx, profile = DEFAULT_MATERIAL_
838
1046
  function sizeOuterShadowOcclusionAt(occlusion, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
839
1047
  return Math.min(1, occlusion + profile.outerShadow.sizeGain * thickness * (1 - occlusion));
840
1048
  }
1049
+ var OUTER_SHADOW_THIN_L = {
1050
+ /** At and below this backdrop luminance the black term is `thinOcclusionDark`. */
1051
+ inert: 0.02,
1052
+ /** From here the mid plateau holds. */
1053
+ midFrom: 0.06,
1054
+ /** To here — `hc-text`'s own linear luminance. */
1055
+ midTo: 0.74,
1056
+ /** `light-solid`'s linear luminance, where 0.127 was measured; held above. */
1057
+ bright: 0.891
1058
+ };
1059
+ var OUTER_SHADOW_UNMEASURED_BACKDROP_LUMINANCE = 0.3;
1060
+ function outerShadowThinOcclusion(backdropLuminance, shadow) {
1061
+ const l = backdropLuminance ?? OUTER_SHADOW_UNMEASURED_BACKDROP_LUMINANCE;
1062
+ const { inert, midFrom, midTo, bright } = OUTER_SHADOW_THIN_L;
1063
+ if (l <= inert)
1064
+ return shadow.thinOcclusionDark;
1065
+ if (l < midFrom) {
1066
+ const t2 = (l - inert) / (midFrom - inert);
1067
+ const s = t2 * t2 * (3 - 2 * t2);
1068
+ return shadow.thinOcclusionDark + (shadow.thinOcclusionMid - shadow.thinOcclusionDark) * s;
1069
+ }
1070
+ if (l <= midTo)
1071
+ return shadow.thinOcclusionMid;
1072
+ if (l >= bright)
1073
+ return shadow.thinOcclusionBright;
1074
+ const t = (l - midTo) / (bright - midTo);
1075
+ return shadow.thinOcclusionMid + (shadow.thinOcclusionBright - shadow.thinOcclusionMid) * t;
1076
+ }
1077
+ var OUTER_SHADOW_THICK_SPANS = [96, 128, 160];
1078
+ function outerShadowThickOcclusion(spanPx, shadow) {
1079
+ const [s0, s1, s2] = OUTER_SHADOW_THICK_SPANS;
1080
+ const y0 = shadow.thickOcclusionAt96;
1081
+ const y1 = shadow.thickOcclusionAt128;
1082
+ const y2 = shadow.thickOcclusionAt160;
1083
+ if (spanPx <= s0)
1084
+ return y0;
1085
+ if (spanPx >= s2)
1086
+ return y2;
1087
+ if (spanPx <= s1)
1088
+ return y0 + (y1 - y0) * (spanPx - s0) / (s1 - s0);
1089
+ return y1 + (y2 - y1) * (spanPx - s1) / (s2 - s1);
1090
+ }
1091
+ function outerShadowOcclusionAt(shadow, backdropLuminance, spanPx, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
1092
+ const thin = outerShadowThinOcclusion(backdropLuminance, shadow);
1093
+ const thick = outerShadowThickOcclusion(spanPx, shadow);
1094
+ const k = Math.min(1, Math.max(0, thickness));
1095
+ const blend = k * k * (3 - 2 * k);
1096
+ return sizeOuterShadowOcclusionAt(thin + (thick - thin) * blend, thickness, profile);
1097
+ }
1098
+ function outerShadowLiftRise(spanPx, shadow) {
1099
+ return smoothstep(shadow.liftSpanMin, shadow.liftSpanFull, spanPx);
1100
+ }
841
1101
  function outerShadowUnderPolicy(policy, profile = DEFAULT_MATERIAL_PROFILE) {
842
1102
  const shadow = profile.outerShadow;
843
1103
  if (policy.glass === "none" || policy.frost === "none")
844
- return { ...shadow, occlusion: 0 };
1104
+ return flatOuterShadow(shadow, 0);
845
1105
  if (policy.frost === "increased") {
846
- return { ...shadow, occlusion: shadow.occlusion * shadow.reducedTransparencyOcclusion };
1106
+ return flatOuterShadow(shadow, shadow.reducedTransparencyOcclusion);
847
1107
  }
848
1108
  return shadow;
849
1109
  }
1110
+ function flatOuterShadow(shadow, amplitude) {
1111
+ return {
1112
+ ...shadow,
1113
+ thinOcclusionDark: amplitude,
1114
+ thinOcclusionMid: amplitude,
1115
+ thinOcclusionBright: amplitude,
1116
+ thickOcclusionAt96: amplitude,
1117
+ thickOcclusionAt128: amplitude,
1118
+ thickOcclusionAt160: amplitude,
1119
+ liftAmplitude: 0
1120
+ };
1121
+ }
850
1122
  var SRGB_ENCODING_EXPONENT = 2.4;
851
1123
  function outerShadowFalloff(signedDistancePx, sigmaPx) {
852
1124
  const x = -signedDistancePx / Math.max(sigmaPx, 1e-4);
@@ -856,8 +1128,8 @@ function outerShadowAlpha(occlusion) {
856
1128
  const occ = Math.min(1, Math.max(0, occlusion));
857
1129
  return 1 - Math.pow(1 - occ, 1 / SRGB_ENCODING_EXPONENT);
858
1130
  }
859
- function outerShadowReachPx(shadow) {
860
- const alpha = outerShadowAlpha(shadow.occlusion);
1131
+ function outerShadowReachPx(shadow, occlusion) {
1132
+ const alpha = outerShadowAlpha(occlusion);
861
1133
  if (!(alpha > 0))
862
1134
  return 0;
863
1135
  const cutoff = 1 / 255 / alpha;
@@ -875,15 +1147,18 @@ function outerShadowReachPx(shadow) {
875
1147
  }
876
1148
  return Math.max(0, hi + Math.abs(shadow.offsetPx) + shadow.spreadPx);
877
1149
  }
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);
1150
+ function lensDepthPx(thicknessPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE, fold = 1) {
1151
+ const thick = Math.max(thicknessPx, 0);
1152
+ const scale = thick / profile.lensThicknessReference;
1153
+ const depth = thick + (lensHeightBasePx(spanPx, profile) * scale - thick) * fold;
1154
+ return Math.min(Math.max(depth, 0), spanPx * 0.5);
881
1155
  }
882
- function lensDisplacementPx(depthPx, lensDepthPx2, refractionScale, profile = DEFAULT_MATERIAL_PROFILE) {
883
- if (lensDepthPx2 <= 0)
1156
+ function lensDisplacementPx(depthPx, spanPx, thicknessPx, refractionScale, profile = DEFAULT_MATERIAL_PROFILE, fold = 1) {
1157
+ const extent = lensExtentPx(thicknessPx, spanPx, profile, fold);
1158
+ if (extent <= 0)
884
1159
  return 0;
885
- const depth = Math.min(Math.max(depthPx / lensDepthPx2, 0), 1);
886
- return lensDepthPx2 * (1 - depth) * (1 - depth) * refractionScale * profile.lensRefractionGain;
1160
+ const t = Math.max(0, 1 - Math.max(depthPx, 0) / extent);
1161
+ return lensMagnitudePx(thicknessPx, spanPx, profile, fold) * Math.pow(t, profile.lensProfileExponent) * refractionScale;
887
1162
  }
888
1163
 
889
1164
  // ../renderer-webgpu/dist/analysis.js
@@ -1721,7 +1996,8 @@ var poolKey = {
1721
1996
  backdropBodyScratch: (sourceId) => `backdrop:${sourceId}:body-scratch`,
1722
1997
  backdropUpload: (sourceId) => `backdrop:${sourceId}:upload`,
1723
1998
  groupField: (groupId) => `group:${groupId}:field`,
1724
- groupAux: (groupId) => `group:${groupId}:aux`
1999
+ groupAux: (groupId) => `group:${groupId}:aux`,
2000
+ groupAux2: (groupId) => `group:${groupId}:aux2`
1725
2001
  };
1726
2002
 
1727
2003
  // ../renderer-webgpu/dist/gpu-context.js
@@ -1878,7 +2154,7 @@ function resolveSurfaces(group, family, profile = DEFAULT_MATERIAL_PROFILE, poli
1878
2154
  centre: [fieldSource.channels.center[0], fieldSource.channels.center[1]],
1879
2155
  channels,
1880
2156
  spanPx,
1881
- lensDepthPx: Math.min(Math.max(shape.channels.thickness, 0) * lensSizeGainFromThickness(thickness, profile), spanPx * 0.5),
2157
+ lensDepthPx: lensDepthPx(shape.channels.thickness, spanPx, profile, profile.refractionScale[accessibilityRefractionCap(policy)]),
1882
2158
  sizeThickness: thickness,
1883
2159
  tintStrength: Math.min(1, Math.max(0, surface.tint?.strength ?? 0))
1884
2160
  };
@@ -1943,7 +2219,7 @@ function packInstances(surfaces, origin, into) {
1943
2219
  data[o + 11] = s.shape.channels.thickness;
1944
2220
  data[o + 12] = s.channels.press;
1945
2221
  data[o + 13] = s.channels.glow;
1946
- data[o + 14] = s.lensDepthPx * Math.min(1, Math.max(0, s.channels.lensStrength));
2222
+ data[o + 14] = Math.max(s.shape.channels.thickness, 0) * Math.min(1, Math.max(0, s.channels.lensStrength));
1947
2223
  data[o + 15] = s.tintStrength;
1948
2224
  data[o + 16] = s.spanPx;
1949
2225
  data[o + 17] = 0;
@@ -2221,7 +2497,7 @@ var WGSL_INSTANCE_STRUCT = `struct Instance {
2221
2497
  thick : f32, // 44 material thickness, CSS px
2222
2498
  press : f32, // 48 interaction channel value, 0..1
2223
2499
  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
2500
+ lensThick : f32, // 56 authored thickness in CSS px times the lensStrength channel (W12 G2): both depths are evaluated from it per pixel
2225
2501
  tintK : f32, // 60 author tint strength, 0..1 \u2014 the seed itself is a group uniform
2226
2502
  span : f32, // 64 the surface's shorter extent, CSS px \u2014 the size law's input, both curves (W11c)
2227
2503
  _pad : f32, // 68 stride padding: vec2f aligns the struct to 8, so 72 is the next legal size
@@ -2389,7 +2665,8 @@ var WGSL_FIELD_PASS = `struct FieldUniforms {
2389
2665
  // field but its members are not one size, and carrying them per pixel is what
2390
2666
  // lets a 40 px button and a 320 px platter share a field pass and still lens by
2391
2667
  // their own depth \u2014 parent acceptance #2 inside a GlassEffectContainer.
2392
- // aux = (lensDepthPx, glow, spanPx, tintStrength)
2668
+ // aux = (lensThickPx, glow, spanPx, tintStrength)
2669
+ // aux2 = (p \u2212 centre, half.x, half.y) \u2014 W12 G2, group-local CSS px
2393
2670
  //
2394
2671
  // The third and fourth slots both changed hands, and neither of the values they
2395
2672
  // used to carry was ever read by a fragment stage. 'thick' left because
@@ -2423,14 +2700,25 @@ var WGSL_FIELD_PASS = `struct FieldUniforms {
2423
2700
  // their SPANS and evaluates the curves on the blend, where it used to blend the
2424
2701
  // evaluated thicknesses. Inside either member the two are identical; only the
2425
2702
  // smooth-min neck differs, and nothing measured claims a value there.
2703
+ //
2704
+ // 'aux2' (W12 G2) carries the pixel's offset from the owning surface's centre
2705
+ // and that surface's half-extents, which
2706
+ // the optics pass needs for the lens's direction: the reference displaces along
2707
+ // the gradient of its SDF blended toward the oval inscribed in the shape's box
2708
+ // (claims \xA75.50's 'gradientOvalization'), and an oval is a property of the box,
2709
+ // not of the distance field. Blended by the same weight as 'aux' in a neck, so
2710
+ // two merged members share one oval there \u2014 a value nothing measured either.
2711
+ // For a concentric child the box is the parent's (the child is the parent's
2712
+ // level set), which is the same centre and a slightly larger oval.
2426
2713
 
2427
2714
  @group(0) @binding(0) var<uniform> fu : FieldUniforms;
2428
2715
  @group(0) @binding(1) var<storage, read> instances : array<Instance>;
2429
2716
 
2430
2717
  struct Member {
2431
- d : f32,
2432
- g : vec2f,
2433
- aux : vec4f,
2718
+ d : f32,
2719
+ g : vec2f,
2720
+ aux : vec4f,
2721
+ aux2 : vec4f,
2434
2722
  };
2435
2723
 
2436
2724
  fn eval_instance(i : u32, p : vec2f) -> Member {
@@ -2443,13 +2731,17 @@ fn eval_instance(i : u32, p : vec2f) -> Member {
2443
2731
  var m : Member;
2444
2732
  m.d = f.d + s.inset;
2445
2733
  m.g = f.g;
2446
- m.aux = vec4f(s.lensDepth, s.glow, s.span, s.tintK);
2734
+ m.aux = vec4f(s.lensThick, s.glow, s.span, s.tintK);
2735
+ // The pixel relative to the surface's centre, and the half-extents: what the
2736
+ // oval's gradient needs, written here where the group-local position is exact.
2737
+ m.aux2 = vec4f(p - s.centre, s.half);
2447
2738
  return m;
2448
2739
  }
2449
2740
 
2450
2741
  struct FieldOut {
2451
2742
  @location(0) field : vec4f,
2452
2743
  @location(1) aux : vec4f,
2744
+ @location(2) aux2 : vec4f,
2453
2745
  };
2454
2746
 
2455
2747
  @fragment
@@ -2467,6 +2759,7 @@ fn fs_field(in : FullscreenOut) -> FieldOut {
2467
2759
  // coverage downstream is not reliably zero.
2468
2760
  out.field = vec4f(65000.0, 0.0, -1.0, 0.0);
2469
2761
  out.aux = vec4f(0.0);
2762
+ out.aux2 = vec4f(0.0);
2470
2763
  return out;
2471
2764
  }
2472
2765
 
@@ -2479,6 +2772,7 @@ fn fs_field(in : FullscreenOut) -> FieldOut {
2479
2772
  acc.d = blend.x;
2480
2773
  acc.g = mix(s.g, acc.g, h);
2481
2774
  acc.aux = mix(s.aux, acc.aux, h);
2775
+ acc.aux2 = mix(s.aux2, acc.aux2, h);
2482
2776
  nearest = min(nearest, s.d);
2483
2777
  }
2484
2778
  // One clamp at the end of the fold, so |union - min| <= maxBulge holds for any
@@ -2491,6 +2785,7 @@ fn fs_field(in : FullscreenOut) -> FieldOut {
2491
2785
 
2492
2786
  out.field = vec4f(acc.d, normal.x, normal.y, coverage);
2493
2787
  out.aux = acc.aux;
2788
+ out.aux2 = acc.aux2;
2494
2789
  return out;
2495
2790
  }`;
2496
2791
  function fieldPassSource(family) {
@@ -2646,7 +2941,9 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
2646
2941
  screen : vec4f,
2647
2942
  /// backdrop uv transform on viewport-normalised coords: scale (xy), offset (zw)
2648
2943
  fit : vec4f,
2649
- /// refractionScale, lensRefractionGain (W11c G2), unused, chainMaxLod
2944
+ /// refractionScale, lensRefractionGain (W12 G2: the gain on the reference's
2945
+ /// amount law), the inner shadow's depth gain (W2's lensSizeGainMax, which the
2946
+ /// shadow keeps), chainMaxLod
2650
2947
  lens : vec4f,
2651
2948
  /// fixed tint colour, linear light (xyz), tint alpha (w)
2652
2949
  tint : vec4f,
@@ -2677,17 +2974,22 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
2677
2974
  toneAdapt : vec4f,
2678
2975
  /// the backdrop source's own average colour, linear (xyz), and its luminance (w)
2679
2976
  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
2977
+ /// the outer shadow (W8, amplitude re-read by W14 G1): the THIN regime's peak
2978
+ /// LINEAR occlusion, already resolved at this group's backdrop luminance and
2979
+ /// already folded under the accessibility policy (x) \u2014 a linear occlusion and
2980
+ /// no longer a compositing alpha, because the shader now blends two regimes
2981
+ /// before it converts; the blur's sigma in group-local CSS px (y), the
2982
+ /// silhouette's outward spread in the same units (z), and the downward offset
2983
+ /// expressed in field-texture UV (w) \u2014 a UV so the offset silhouette is one
2984
+ /// texture read rather than a gradient extrapolation, which is exact only on a
2985
+ /// straight edge and a capsule is mostly not one
2686
2986
  shadow : vec4f,
2687
2987
  /// the outer shadow's size-law gain (x), read against the casting surface's own
2688
2988
  /// 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
2989
+ /// shadow's shift needs when it lands outside the texture; the body depth
2990
+ /// ramp's THICK start (z, W13 G1's third form \u2014 the thin end is scatter.y and
2991
+ /// this pass mixes the two by the pixel's own sizeThickness), and (w) the
2992
+ /// padding a uniform's vec4 alignment requires
2691
2993
  shadowSize : vec4f,
2692
2994
  /// the backdrop tone response's anchors (W9): the three solid anchors'
2693
2995
  /// ENCODED-space means (xyz), and the backdrop's linear-space mean (w) \u2014 the
@@ -2699,10 +3001,41 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
2699
3001
  toneRowThin : vec4f,
2700
3002
  /// the thick row (sizeThickness saturated), same layout
2701
3003
  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
3004
+ /// the size law's bands: the scatter facet's floor (x) and the depth ramp's
3005
+ /// THIN start s0 at the ratio this group draws at (y, W13 G1 \u2014 the CPU has
3006
+ /// already interpolated it between the profile's 1x and 2x anchors; the thick
3007
+ /// end is shadowSize.z), and the
3008
+ /// thickness curve's sizeSpanMin (z) and sizeSpanMax (w); the fold both
3009
+ /// multiply by is tone.w. The scatter span curve's own band top is lensOval.w,
3010
+ /// and it starts at the same z.
2705
3011
  scatter : vec4f,
3012
+ /// the lens law (W12 G2): the reference's height law (lensHeightPerSpan,
3013
+ /// lensHeightMax) and amount law (lensAmountPerSpan, lensAmountMax)
3014
+ lensLaw : vec4f,
3015
+ /// the lens profile (W12 G2): lensExtentGain, lensProfileExponent,
3016
+ /// lensOvalization, lensThicknessReference
3017
+ lensShape : vec4f,
3018
+ /// the ovalization's knee (W12 G2): spanMin (x), spanMax (y); the body depth
3019
+ /// ramp's reach in group-local CSS px (z, W13 G1) \u2014 the profile names it in
3020
+ /// device px and the CPU divides by the ratio, so that this shader can read
3021
+ /// the field's own CSS-px depth without a second conversion; the scatter span
3022
+ /// curve's band top sizeScatterSpanMax (w, W11c), which W13 G1 keeps
3023
+ /// underneath the ramp as its deep value
3024
+ lensOval : vec4f,
3025
+ /// the outer shadow's THICK regime (W14 G1): the composite's peak linear
3026
+ /// occlusion at casting spans 96, 128 and 160 CSS px (xyz), already folded
3027
+ /// under the accessibility policy, piecewise-linear between and held outside;
3028
+ /// and (w) the body depth ramp's FAR start (W13 G1's fourth form) \u2014 the start
3029
+ /// at span >= sizeScatterSpanMax, which the ramp declines to from the thick
3030
+ /// end along the scatter span curve, in the slot W14 left free
3031
+ shadowThick : vec4f,
3032
+ /// the lift (W14 G1), the shadow's second term and GPU-tier only: its peak
3033
+ /// amplitude in LINEAR light as a fraction of the blurred backdrop's own
3034
+ /// luminance (x), the span rise's foot and top in CSS px (yz), and the chain
3035
+ /// LOD whose blur is the profile's liftBlurSigmaCss \u2014 resolved on the CPU,
3036
+ /// which is the only place the CSS-px-to-texel conversion is knowable, and
3037
+ /// already clamped to chainMaxLod (w)
3038
+ shadowLift : vec4f,
2706
3039
  };
2707
3040
 
2708
3041
  @group(0) @binding(0) var<uniform> ou : OpticsUniforms;
@@ -2712,6 +3045,9 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
2712
3045
  @group(0) @binding(4) var backdropChain : texture_2d<f32>;
2713
3046
  @group(0) @binding(5) var backdropBody : texture_2d<f32>;
2714
3047
  @group(0) @binding(6) var fieldSampler : sampler;
3048
+ /// The owning surface's box per pixel (W12 G2): the pixel's offset from its
3049
+ /// centre (xy) and its half-extents (zw), group-local CSS px \u2014 the lens's oval.
3050
+ @group(0) @binding(7) var aux2Texture : texture_2d<f32>;
2715
3051
 
2716
3052
  /// One encoded sRGB channel from a linear one \u2014 the space the backdrop tone
2717
3053
  /// response's anchors live in (W9). Mirrors material.ts's 'linearToSrgbChannel'.
@@ -2767,7 +3103,33 @@ fn outer_shadow_falloff(signedDistance : f32, sigma : f32) -> f32 {
2767
3103
  return 0.5 * (1.0 + tanh(0.7978845608028654 * (x + 0.044715 * x * x * x)));
2768
3104
  }
2769
3105
 
2770
- /// The outer shadow's alpha at this pixel (W8).
3106
+ /// What one pixel's outer shadow is made of: the compositing-space alpha the
3107
+ /// black term composites at, the falloff BOTH terms ride, and the span of the
3108
+ /// surface that cast it \u2014 which the lift's own rise is read from.
3109
+ ///
3110
+ /// One struct rather than three calls because there is one falloff (W14 G0,
3111
+ /// claims \xA75.62 \xA74): the black term's free fit and the lift's free fit return
3112
+ /// the same sigma, offset and spread, so evaluating a second geometry for the
3113
+ /// second term would be asserting a difference the measurement denies.
3114
+ struct ShadowSample {
3115
+ alpha : f32,
3116
+ falloff : f32,
3117
+ castSpanCss : f32,
3118
+ };
3119
+
3120
+ /// The composite's peak linear occlusion above the knee, at a casting span \u2014
3121
+ /// piecewise linear through the three measured anchors (96 / 128 / 160 CSS px)
3122
+ /// and held flat outside them. Mirrors material.ts's
3123
+ /// 'outerShadowThickOcclusion'.
3124
+ fn outer_shadow_thick(spanCss : f32) -> f32 {
3125
+ let y = ou.shadowThick.xyz;
3126
+ if (spanCss <= 96.0) { return y.x; }
3127
+ if (spanCss >= 160.0) { return y.z; }
3128
+ if (spanCss <= 128.0) { return y.x + (y.y - y.x) * (spanCss - 96.0) / 32.0; }
3129
+ return y.y + (y.z - y.y) * (spanCss - 128.0) / 32.0;
3130
+ }
3131
+
3132
+ /// The outer shadow's alpha at this pixel (W8, amplitude W14 G1).
2771
3133
  ///
2772
3134
  /// The shadow is the group's OWN field, translated down by 'shadow.w' in field
2773
3135
  /// UV and outset by 'shadow.z', then blurred. Reading the offset silhouette from
@@ -2778,9 +3140,13 @@ fn outer_shadow_falloff(signedDistance : f32, sigma : f32) -> f32 {
2778
3140
  /// The value is an ALPHA on pure black, so what it composites to is the backdrop
2779
3141
  /// times '1 - alpha' \u2014 multiplicative occlusion, and exactly zero over black,
2780
3142
  /// 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;
3143
+ fn outer_shadow(uv : vec2f, upsampled : f32, fieldSize : vec2f) -> ShadowSample {
3144
+ var out : ShadowSample;
3145
+ out.alpha = 0.0;
3146
+ out.falloff = 0.0;
3147
+ out.castSpanCss = 0.0;
3148
+ if (ou.shadow.x <= 0.0 && ou.shadowThick.x <= 0.0) {
3149
+ return out;
2784
3150
  }
2785
3151
  /*
2786
3152
  * The shift can leave the field texture, and clamping alone reads a lie there.
@@ -2816,14 +3182,11 @@ fn outer_shadow_alpha(uv : vec2f, upsampled : f32, fieldSize : vec2f) -> f32 {
2816
3182
  }
2817
3183
 
2818
3184
  // 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.
3185
+ // lengths are span-invariant across 32\u2026160 px. Every span-keyed quantity here
3186
+ // rides the thickness and the span of the surface that CAST the shadow, which
3187
+ // is the one read at the offset position rather than the one under the pixel
3188
+ // being shaded.
2822
3189
  //
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
3190
  // The thickness curve off the casting surface's span (W11c: the span rides
2828
3191
  // the field), written as the body's own is below.
2829
3192
  let castT = clamp(
@@ -2832,10 +3195,65 @@ fn outer_shadow_alpha(uv : vec2f, upsampled : f32, fieldSize : vec2f) -> f32 {
2832
3195
  1.0,
2833
3196
  );
2834
3197
  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
3198
 
2838
- return alpha * outer_shadow_falloff(shadowField.x + clampedOffCss - ou.shadow.z, ou.shadow.y);
3199
+ // The two regimes and the knee between them (W14 G1). The thin amplitude is
3200
+ // resolved on the CPU because its key \u2014 the backdrop's luminance \u2014 is one
3201
+ // number for the whole group; the thick one is a span law and has to be per
3202
+ // pixel. The blend is the smoothstep of 'sizeK', which is exactly the curve
3203
+ // 'tone_response' blends its own thin and thick rows across: one knee for the
3204
+ // face and the shadow, which is the charter's third binding rule read from
3205
+ // the other side.
3206
+ let blend = sizeK * sizeK * (3.0 - 2.0 * sizeK);
3207
+ let regime = mix(ou.shadow.x, outer_shadow_thick(shadowAux.z), blend);
3208
+ // The size gain, on the relative form 'sizeOuterShadowOcclusionAt' uses, in
3209
+ // LINEAR light; then the one conversion to the canvas's compositing space,
3210
+ // which is where the black term is actually composited.
3211
+ let occ = clamp(regime + ou.shadowSize.x * sizeK * (1.0 - regime), 0.0, 1.0);
3212
+ out.alpha = 1.0 - pow(1.0 - occ, 1.0 / 2.4);
3213
+ out.falloff = outer_shadow_falloff(shadowField.x + clampedOffCss - ou.shadow.z, ou.shadow.y);
3214
+ out.castSpanCss = shadowAux.z;
3215
+ return out;
3216
+ }
3217
+
3218
+ /// The lift (W14 G1) \u2014 the shadow's second term, in LINEAR light, premultiplied
3219
+ /// by nothing yet: a blurred copy of the backdrop's own light, at an amplitude
3220
+ /// that is zero below the knee and saturating above it, on the SAME falloff the
3221
+ /// black term rides.
3222
+ ///
3223
+ /// 'V' is the backdrop chain read at the pixel's OWN position \u2014 the copy is of
3224
+ /// the backdrop beneath the shadow, not of the backdrop under the surface \u2014 at
3225
+ /// the LOD whose blur is the profile's 40 CSS px. The chain's uv transform is on
3226
+ /// viewport-normalised coordinates, so every pixel this pass draws has a valid
3227
+ /// sample: unlike the field texture, which the offset shift can walk off the top
3228
+ /// of, the chain covers the whole viewport and there is nothing to reconstruct.
3229
+ ///
3230
+ /// Zero over black by construction (V = 0), zero below the knee by the rise, and
3231
+ /// zero where the group has no backdrop to copy \u2014 so 'dark-solid', 'impulse',
3232
+ /// every thin cell and every unsampled group stay byte-for-byte what they were.
3233
+ fn outer_shadow_lift(viewport01 : vec2f, shadow : ShadowSample) -> vec3f {
3234
+ if (ou.flags.x <= 0.5 || ou.shadowLift.x <= 0.0 || shadow.falloff <= 0.0) {
3235
+ return vec3f(0.0);
3236
+ }
3237
+ // Written out rather than through WGSL's 'smoothstep', which is undefined
3238
+ // where the two edges coincide \u2014 a profile is entitled to set the rise's foot
3239
+ // and top to one number, and material.ts's own 'smoothstep' degrades that to a
3240
+ // step rather than to a NaN. Same rule as the tone band's divide above.
3241
+ let riseT = clamp(
3242
+ (shadow.castSpanCss - ou.shadowLift.y) / max(ou.shadowLift.z - ou.shadowLift.y, 1e-6),
3243
+ 0.0,
3244
+ 1.0,
3245
+ );
3246
+ let rise = riseT * riseT * (3.0 - 2.0 * riseT);
3247
+ if (rise <= 0.0) {
3248
+ return vec3f(0.0);
3249
+ }
3250
+ let uv = clamp(viewport01 * ou.fit.xy + ou.fit.zw, vec2f(0.0), vec2f(1.0));
3251
+ let chainSample = textureSampleLevel(backdropChain, backdropSampler, uv, ou.shadowLift.w);
3252
+ // Premultiplied linear in, straight colour out \u2014 the same unpremultiply the
3253
+ // body's own samples take, for the same reason: a partially transparent
3254
+ // backdrop must not darken what the material adds back.
3255
+ let v = chainSample.rgb / max(chainSample.a, 1e-6);
3256
+ return v * (ou.shadowLift.x * rise * shadow.falloff);
2839
3257
  }
2840
3258
 
2841
3259
  @fragment
@@ -2847,13 +3265,16 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
2847
3265
  // contour to the coarse grid and the rim would step along it.
2848
3266
  var field : vec4f;
2849
3267
  var aux : vec4f;
3268
+ var aux2 : vec4f;
2850
3269
  if (ou.flags.w > 0.5) {
2851
3270
  field = textureSampleLevel(fieldTexture, fieldSampler, in.uv, 0.0);
2852
3271
  aux = textureSampleLevel(auxTexture, fieldSampler, in.uv, 0.0);
3272
+ aux2 = textureSampleLevel(aux2Texture, fieldSampler, in.uv, 0.0);
2853
3273
  } else {
2854
3274
  let texel = vec2i(in.uv * ou.flags.yz);
2855
3275
  field = textureLoad(fieldTexture, texel, 0);
2856
3276
  aux = textureLoad(auxTexture, texel, 0);
3277
+ aux2 = textureLoad(aux2Texture, texel, 0);
2857
3278
  }
2858
3279
 
2859
3280
  let d = field.x;
@@ -2869,46 +3290,172 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
2869
3290
  * is clipped out of its own border box, and the two meet across the coverage
2870
3291
  * ramp with no seam because it is one expression.
2871
3292
  */
2872
- let shadowAlpha = outer_shadow_alpha(in.uv, ou.flags.w, ou.flags.yz);
3293
+ let shadowSample = outer_shadow(in.uv, ou.flags.w, ou.flags.yz);
3294
+ let shadowAlpha = shadowSample.alpha * shadowSample.falloff;
3295
+ let viewport01 = in.position.xy / ou.screen.xy;
3296
+ // Resolved once, for both sides of the coverage ramp: the lift fills exactly
3297
+ // what the surface's coverage leaves, on the same rule and in the same
3298
+ // expression as the black term's alpha at the bottom of this function, so the
3299
+ // two meet across the ramp with no seam. At full coverage it is not computed
3300
+ // at all and the chain read is not taken.
3301
+ var liftEncoded = vec3f(0.0);
3302
+ if (coverage < 1.0) {
3303
+ let lift = outer_shadow_lift(viewport01, shadowSample);
3304
+ liftEncoded = min(linear_to_srgb(max(lift, vec3f(0.0))), vec3f(shadowAlpha));
3305
+ }
2873
3306
  if (coverage <= 0.0) {
2874
- return vec4f(0.0, 0.0, 0.0, shadowAlpha);
3307
+ /*
3308
+ * The lift (W14 G1) makes this a premultiplied COMPOSITE rather than an
3309
+ * alpha on black: the compositor produces 'page\xB7(1 \u2212 shadowAlpha) + lift',
3310
+ * which is the reference's own two-term form outside the coverage.
3311
+ *
3312
+ * The added light is computed in linear light and encoded on its own before
3313
+ * it is emitted, because it IS its own light \u2014 over the checkerboard's black
3314
+ * squares, where the multiply is inert, what the reference adds is exactly
3315
+ * this term and nothing else, and encoding it alone reproduces G0's measured
3316
+ * +0.048 encoded at span 160 from its +0.0038 linear (claims \xA75.62 \xA72, \xA73).
3317
+ * Where the backdrop is bright the encoding of a sum and the sum of the
3318
+ * encodings differ, and this term is the second: the residual is bounded by
3319
+ * the term's own size and is well inside the bed's \xB14/255, on the same
3320
+ * honest-floor argument 'outerShadowAlpha' makes for the black term's space.
3321
+ *
3322
+ * A premultiplied layer may not carry a channel above its own alpha, so the
3323
+ * emitted light is capped at 'shadowAlpha'. At the profile's constants the
3324
+ * cap is inert everywhere on the falloff: both terms are proportional to the
3325
+ * same F, and the lift's encoded value stays at about a sixth of the black
3326
+ * term's alpha from the deepest ring out to where the shadow stops moving a
3327
+ * code. It is here so that a swept amplitude produces a valid layer rather
3328
+ * than an implementation-defined one.
3329
+ */
3330
+ return vec4f(liftEncoded, shadowAlpha);
2875
3331
  }
2876
3332
 
2877
- let viewport01 = in.position.xy / ou.screen.xy;
2878
3333
  let viewportCss = ou.screen.xy * ou.screen.z;
2879
3334
 
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
3335
+ // Per-pixel, unioned through the field pass. See the module note. 'aux.x' is
3336
+ // the authored thickness times the lensStrength channel (W12 G2); the lens's
3337
+ // depth and the inner shadow's are both evaluated from it and the span below.
3338
+ let lensThick = max(aux.x, 0.0);
3339
+ // The thickness curve off the span of whichever surface owns this pixel
2883
3340
  // (W11c). 'sizeK' is the thickness factor, 0..1: zero on anything at or below
2884
3341
  // the profile's 'sizeSpanMin', saturated at 'sizeSpanMax', folded under the
2885
3342
  // 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
3343
+ // by it, so a small control takes the pre-law path exactly. Written out as a
3344
+ // smoothstep with a guarded denominator, so a profile that collapses the band
2891
3345
  // degrades to a step rather than to NaN.
2892
3346
  let span = aux.z;
2893
3347
  let fold = clamp(ou.tone.w, 0.0, 1.0);
2894
3348
  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);
3349
+ // The unfolded curve, which the depth ramp's start grades along below, and the
3350
+ // folded factor every other facet multiplies by. One smoothstep, two readings.
3351
+ let sizeThick = thickT * thickT * (3.0 - 2.0 * thickT);
3352
+ let sizeK = clamp(sizeThick * fold, 0.0, 1.0);
3353
+ /*
3354
+ * The body's mix is the span curve W11c fitted with a ramp in DEPTH riding on
3355
+ * top of it near the contour (W13 G1, from the measurement of claims \xA75.61 \xA72
3356
+ * and the re-forming its first runtime sweep forced):
3357
+ *
3358
+ * kDeep = floor + (1 - floor) * smoothstep(sizeSpanMin, sizeScatterSpanMax, span)
3359
+ * s0 = startThin + (startThick - startThin) * sizeThickness(span)
3360
+ * + (startFar - startThick) * smoothstep(sizeSpanMax, sizeScatterSpanMax, span)
3361
+ * s(u) = (1 - kDeep) + max(0, s0 - (1 - kDeep)) * max(0, 1 - u / U)
3362
+ * k(u) = 1 - s(u)
3363
+ *
3364
+ * The first form of the ramp replaced the span curve outright and its sweep
3365
+ * measured what that cost: the ramp's own projection onto one number per
3366
+ * surface runs 0.43-0.56 where the curve it replaced runs 0.41-1.00, so the
3367
+ * family is nearly span-flat where the bed is strongly span-graded and no
3368
+ * point in 81 reached the wave's stops. So the curve stays as the deep value
3369
+ * and the ramp is what it always measured as - a near-contour excursion.
3370
+ *
3371
+ * The SECOND form gave that excursion one start per scale, and its own sweep
3372
+ * refuted that arithmetically (claims \xA75.64 \xA72): the smallest cell's span is
3373
+ * exactly sizeSpanMin, so its deep sharp share is exactly 1 - floor and no
3374
+ * start at or below it can touch that cell, while the next span up only
3375
+ * improves BELOW a start lower still. So the start grades with span too, and
3376
+ * along the material's own thin/thick curve rather than a new one - the same
3377
+ * smoothstep 'sizeK' is built from above, unfolded, because s0 is a share the
3378
+ * reference has and not a rise a preference removes. The fold below still
3379
+ * applies once, on the composed mix.
3380
+ *
3381
+ * The THIRD form stopped there and its holdout failed on one row for the
3382
+ * form's own arithmetic (claims \xA75.67 \xA74): sizeThickness saturates at
3383
+ * sizeSpanMax, so every thick span got the same start while the reference's
3384
+ * start keeps FALLING across the thick spans, and because kDeep keeps rising
3385
+ * to sizeScatterSpanMax the excursion grew with span where the reference's
3386
+ * shrinks. So the FOURTH form declines the start past the knee along the
3387
+ * scatter span curve itself \u2014 the same smoothstep 'deepT' is built from, on
3388
+ * the band above the thickness knee \u2014 from the thick anchor to startFar at
3389
+ * sizeScatterSpanMax. Same curve read twice, no new span statistic.
3390
+ *
3391
+ * '-d' is the depth, in group-local CSS px and unclamped - the same quantity
3392
+ * the lens's 'lensT' and the inner shadow's 'shadowT' are evaluated from,
3393
+ * except that those two divide it by a depth of their own while this reads it
3394
+ * absolutely, because the reach measured as a LENGTH rather than as a fraction
3395
+ * of the span. The reach reaches this shader already divided by the device
3396
+ * ratio ('lensOval.z'), so the ratio 'u / U' is the same number in CSS px as
3397
+ * it is in the device px the profile names.
3398
+ *
3399
+ * 'max(s0 - sDeep, 0)' rather than a signed difference: the excursion is the
3400
+ * band the reference has ABOVE the body, and a span whose deep sharp share
3401
+ * already exceeds s0 has nothing to add rather than something to subtract.
3402
+ *
3403
+ * The fold keeps its W11c semantics on the composed law: the floor is the
3404
+ * frost the material has at any size and is not folded, everything above it -
3405
+ * the span curve's rise and the ramp's excursion alike - is depth and is, so
3406
+ * fold 1 renders the law and fold 0 sits at the floor exactly.
3407
+ */
2896
3408
  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;
3409
+ let deepT = clamp((span - ou.scatter.z) / max(ou.lensOval.w - ou.scatter.z, 1e-6), 0.0, 1.0);
3410
+ let kDeep = scatterFloor + (1.0 - scatterFloor) * deepT * deepT * (3.0 - 2.0 * deepT);
3411
+ let sDeep = 1.0 - kDeep;
3412
+ let rampT = max(1.0 - max(-d, 0.0) / max(ou.lensOval.z, 1e-6), 0.0);
3413
+ let farT = clamp((span - ou.scatter.w) / max(ou.lensOval.w - ou.scatter.w, 1e-6), 0.0, 1.0);
3414
+ let farS = farT * farT * (3.0 - 2.0 * farT);
3415
+ let rampStart = ou.scatter.y + (ou.shadowSize.z - ou.scatter.y) * sizeThick
3416
+ + (ou.shadowThick.w - ou.shadowSize.z) * farS;
3417
+ let sharpShare = clamp(sDeep + max(rampStart - sDeep, 0.0) * rampT, 0.0, 1.0);
3418
+ let kScatter = clamp(scatterFloor + ((1.0 - sharpShare) - scatterFloor) * fold, 0.0, 1.0);
3419
+ // The inner shadow's depth and profile: W2's law, byte for byte \u2014 the
3420
+ // thickness times the size gain (folded through 'sizeK'), clamped to the
3421
+ // shorter half extent, and a square on it. The lens ran on this until W12 G2;
3422
+ // the occlusion keeps it, because nothing measured it as wrong, and every
3423
+ // solid-backdrop cell renders exactly as it did.
3424
+ let shadowLensDepth = max(min(lensThick * (1.0 + (ou.lens.z - 1.0) * sizeK), span * 0.5), 1e-4);
3425
+ let shadowT = clamp(-d / shadowLensDepth, 0.0, 1.0);
3426
+ let shadowProfile = (1.0 - shadowT) * (1.0 - shadowT);
3427
+
3428
+ // The lens (W12 G2, claims \xA75.51) \u2014 see the module note. The reference's
3429
+ // height and amount laws off the span, scaled by the thickness over the
3430
+ // reference's unit, folded like the W2 law (at fold 0 the depth is the
3431
+ // authored thickness and nothing more), and clamped to the shorter half
3432
+ // extent with the magnitude clamped by the same ratio.
3433
+ let thickScale = lensThick / max(ou.lensShape.w, 1e-4);
3434
+ let heightBase = min(ou.lensLaw.x * span, ou.lensLaw.y);
3435
+ let amountBase = min(ou.lensLaw.z * span, ou.lensLaw.w);
3436
+ let depthUnclamped = mix(lensThick, heightBase * thickScale, fold);
3437
+ let lensDepth = clamp(depthUnclamped, 0.0, span * 0.5);
3438
+ let clampRatio = select(0.0, lensDepth / depthUnclamped, depthUnclamped > 1e-6);
3439
+ let magnitude = ou.lens.y * mix(lensThick, amountBase * thickScale, fold) * clampRatio;
3440
+ let extent = max(ou.lensShape.x * lensDepth, 1e-4);
3441
+ // One steep power over the extent; '-d' is depth inside the surface.
3442
+ let lensT = max(1.0 - max(-d, 0.0) / extent, 0.0);
3443
+ let displacementCss = magnitude * pow(lensT, ou.lensShape.y) * ou.lens.x;
3444
+
3445
+ // The direction: the field's gradient blended toward the oval inscribed in
3446
+ // the surface's box (the reference's 'gradientOvalization'), on from the
3447
+ // knee, with the magnitude fixed \u2014 which tilts the displacement toward the
3448
+ // edge's midpoint and magnifies the band along the edge.
3449
+ let ovalT = clamp((span - ou.lensOval.x) / max(ou.lensOval.y - ou.lensOval.x, 1e-6), 0.0, 1.0);
3450
+ let omega = ou.lensShape.z * ovalT * ovalT * (3.0 - 2.0 * ovalT);
3451
+ let halfExt = max(aux2.zw, vec2f(1e-4));
3452
+ let rel = aux2.xy;
3453
+ let unitR = max(length(rel / halfExt), 1e-6);
3454
+ let ovalGrad = (rel / (halfExt * halfExt)) * (min(halfExt.x, halfExt.y) / unitR);
3455
+ let blended = (1.0 - omega) * normal + omega * ovalGrad;
3456
+ let blendLen = length(blended);
3457
+ let direction = select(normal, blended / blendLen, blendLen > 1e-6);
3458
+ let displaceCss = -direction * displacementCss;
2912
3459
  let refracted01 = viewport01 + displaceCss / viewportCss;
2913
3460
 
2914
3461
  let refractedUv = clamp(refracted01 * ou.fit.xy + ou.fit.zw, vec2f(0.0), vec2f(1.0));
@@ -3160,7 +3707,7 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
3160
3707
  // A multiplicative occlusion of the whole composite. As a layer that is the
3161
3708
  // colour scaled and the alpha raised \u2014 (k\xB7a\xB7c, 1 \u2212 k\xB7(1 \u2212 a)) composites to
3162
3709
  // 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;
3710
+ let shadowKeep = 1.0 - shadowProfile * shadowDepth * ou.light.w * present;
3164
3711
  let shadowedAlpha = 1.0 - shadowKeep * (1.0 - bodyAlpha);
3165
3712
  colour = colour * (shadowKeep * bodyAlpha / max(shadowedAlpha, 1e-6));
3166
3713
  bodyAlpha = shadowedAlpha;
@@ -3198,7 +3745,10 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
3198
3745
  // an opaque body the two are the same quantity, which is how the shadow was
3199
3746
  // first written and why the difference only surfaced with the layer form.)
3200
3747
  let body = encode_output(max(colour, vec3f(0.0)), coverage * bodyAlpha);
3201
- return vec4f(body.rgb, body.a + shadowAlpha * (1.0 - coverage));
3748
+ return vec4f(
3749
+ body.rgb + liftEncoded * (1.0 - coverage),
3750
+ body.a + shadowAlpha * (1.0 - coverage),
3751
+ );
3202
3752
  }`;
3203
3753
 
3204
3754
  // ../renderer-webgpu/dist/wgsl/prelude.js
@@ -3326,7 +3876,11 @@ function createPassRunner(context) {
3326
3876
  fragment: {
3327
3877
  module,
3328
3878
  entryPoint: "fs_field",
3329
- targets: [{ format: WORKING_TEXTURE_FORMAT }, { format: WORKING_TEXTURE_FORMAT }]
3879
+ targets: [
3880
+ { format: WORKING_TEXTURE_FORMAT },
3881
+ { format: WORKING_TEXTURE_FORMAT },
3882
+ { format: WORKING_TEXTURE_FORMAT }
3883
+ ]
3330
3884
  },
3331
3885
  primitive: { topology: "triangle-list" }
3332
3886
  };
@@ -3390,6 +3944,13 @@ function createPassRunner(context) {
3390
3944
  usage: fieldUsage(),
3391
3945
  label: `vitrea:group:${args.groupId}:aux`
3392
3946
  });
3947
+ const aux2 = pool.acquire(poolKey.groupAux2(args.groupId), {
3948
+ width,
3949
+ height,
3950
+ format: WORKING_TEXTURE_FORMAT,
3951
+ usage: fieldUsage(),
3952
+ label: `vitrea:group:${args.groupId}:aux2`
3953
+ });
3393
3954
  const slot = uniformSlot(`field:${args.groupId}`, 12);
3394
3955
  slot.data[0] = rectWidth;
3395
3956
  slot.data[1] = rectHeight;
@@ -3423,6 +3984,12 @@ function createPassRunner(context) {
3423
3984
  loadOp: "clear",
3424
3985
  storeOp: "store",
3425
3986
  clearValue: { r: 0, g: 0, b: 0, a: 0 }
3987
+ },
3988
+ {
3989
+ view: aux2.createView(),
3990
+ loadOp: "clear",
3991
+ storeOp: "store",
3992
+ clearValue: { r: 0, g: 0, b: 0, a: 0 }
3426
3993
  }
3427
3994
  ]
3428
3995
  });
@@ -3439,13 +4006,14 @@ function createPassRunner(context) {
3439
4006
  return {
3440
4007
  field,
3441
4008
  aux,
4009
+ aux2,
3442
4010
  width,
3443
4011
  height,
3444
4012
  upsampled: width !== rectWidth || height !== rectHeight
3445
4013
  };
3446
4014
  },
3447
4015
  opticsPass(encoder, args) {
3448
- const slot = uniformSlot(`optics:${args.groupId}`, 76);
4016
+ const slot = uniformSlot(`optics:${args.groupId}`, 96);
3449
4017
  const d = slot.data;
3450
4018
  d[0] = args.viewportDevice[0];
3451
4019
  d[1] = args.viewportDevice[1];
@@ -3457,7 +4025,7 @@ function createPassRunner(context) {
3457
4025
  d[7] = args.fit[3];
3458
4026
  d[8] = args.refractionScale;
3459
4027
  d[9] = args.lensRefractionGain;
3460
- d[10] = 0;
4028
+ d[10] = args.shadowDepthSizeGainMax;
3461
4029
  d[11] = args.chainMaxLod;
3462
4030
  d[12] = args.tint[0];
3463
4031
  d[13] = args.tint[1];
@@ -3505,7 +4073,7 @@ function createPassRunner(context) {
3505
4073
  d[55] = args.outerShadow[3];
3506
4074
  d[56] = args.outerShadowSizeGain;
3507
4075
  d[57] = args.outerShadowRectCssHeight;
3508
- d[58] = 0;
4076
+ d[58] = args.sizeScatterRampStartThick;
3509
4077
  d[59] = 0;
3510
4078
  d[60] = args.backdropToneAnchorX[0];
3511
4079
  d[61] = args.backdropToneAnchorX[1];
@@ -3520,9 +4088,29 @@ function createPassRunner(context) {
3520
4088
  d[70] = args.backdropToneResponseThick[2];
3521
4089
  d[71] = 0;
3522
4090
  d[72] = args.sizeScatterFloor;
3523
- d[73] = args.sizeScatterSpanMax;
4091
+ d[73] = args.sizeScatterRampStartThin;
3524
4092
  d[74] = args.sizeSpanMin;
3525
4093
  d[75] = args.sizeSpanMax;
4094
+ d[76] = args.lensHeightPerSpan;
4095
+ d[77] = args.lensHeightMax;
4096
+ d[78] = args.lensAmountPerSpan;
4097
+ d[79] = args.lensAmountMax;
4098
+ d[80] = args.lensExtentGain;
4099
+ d[81] = args.lensProfileExponent;
4100
+ d[82] = args.lensOvalization;
4101
+ d[83] = args.lensThicknessReference;
4102
+ d[84] = args.lensOvalizationSpanMin;
4103
+ d[85] = args.lensOvalizationSpanMax;
4104
+ d[86] = args.sizeScatterRampReachCssPx;
4105
+ d[87] = args.sizeScatterSpanMax;
4106
+ d[88] = args.outerShadowThick[0];
4107
+ d[89] = args.outerShadowThick[1];
4108
+ d[90] = args.outerShadowThick[2];
4109
+ d[91] = args.sizeScatterRampStartFar;
4110
+ d[92] = args.outerShadowLift[0];
4111
+ d[93] = args.outerShadowLift[1];
4112
+ d[94] = args.outerShadowLift[2];
4113
+ d[95] = args.outerShadowLift[3];
3526
4114
  slot.write();
3527
4115
  const chain = args.backdrop?.chain ?? placeholderView;
3528
4116
  const body = args.backdrop?.body ?? placeholderView;
@@ -3545,7 +4133,8 @@ function createPassRunner(context) {
3545
4133
  { binding: 5, resource: body },
3546
4134
  // Linear, no mips: the field targets have one level, and this is only
3547
4135
  // read at all when the governor shrank them below the group's rect.
3548
- { binding: 6, resource: context.flatSampler }
4136
+ { binding: 6, resource: context.flatSampler },
4137
+ { binding: 7, resource: args.fields.aux2.createView() }
3549
4138
  ]
3550
4139
  }));
3551
4140
  pass.draw(3);
@@ -3628,6 +4217,7 @@ function createPassRunner(context) {
3628
4217
  forget(groupId) {
3629
4218
  pool.release(poolKey.groupField(groupId));
3630
4219
  pool.release(poolKey.groupAux(groupId));
4220
+ pool.release(poolKey.groupAux2(groupId));
3631
4221
  storages.get(groupId)?.destroy();
3632
4222
  storages.delete(groupId);
3633
4223
  for (const key of [`field:${groupId}`, `optics:${groupId}`, `highlight:${groupId}`]) {
@@ -3773,10 +4363,8 @@ function createRebuildLedger() {
3773
4363
  // ../renderer-webgpu/dist/pyramid.js
3774
4364
  var chainUsage = () => GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING;
3775
4365
  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
- };
4366
+ var same = (next, existing) => Math.abs(next - existing) <= 1e-6 * Math.max(1, Math.abs(existing));
4367
+ var sameBody = (existing, request) => same(densityOf({ width: existing.sourceWidth, height: existing.sourceHeight }, request), existing.texelsPerCss) && same(request.bodySigmaCss, existing.bodySigmaCss);
3780
4368
  function createPyramidStore(context) {
3781
4369
  const { device, pool, cache } = context;
3782
4370
  const resources = /* @__PURE__ */ new Map();
@@ -3852,7 +4440,7 @@ function createPyramidStore(context) {
3852
4440
  return target;
3853
4441
  };
3854
4442
  const mipView = (texture, level) => texture.createView({ baseMipLevel: level, mipLevelCount: 1, dimension: "2d" });
3855
- function allocate(sourceId, plan, bodyLevel, sizeEpoch, builtEpoch, bodySigmaTexels, density) {
4443
+ function allocate(sourceId, plan, bodyLevel, sizeEpoch, builtEpoch, bodySigmaTexels, density, bodySigmaCss) {
3856
4444
  const existing = resources.get(sourceId);
3857
4445
  const bodyWidth = (plan.levels[bodyLevel] ?? plan.levels[0]).width;
3858
4446
  const bodyHeight = (plan.levels[bodyLevel] ?? plan.levels[0]).height;
@@ -3893,7 +4481,8 @@ function createPyramidStore(context) {
3893
4481
  bodySigmaTexels,
3894
4482
  texelsPerCss: density.texelsPerCss,
3895
4483
  sourceWidth: density.sourceWidth,
3896
- sourceHeight: density.sourceHeight
4484
+ sourceHeight: density.sourceHeight,
4485
+ bodySigmaCss
3897
4486
  };
3898
4487
  resources.set(sourceId, next);
3899
4488
  return next;
@@ -4081,7 +4670,7 @@ function createPyramidStore(context) {
4081
4670
  },
4082
4671
  build(request, provider, encoder) {
4083
4672
  const existing = liveResources(request.sourceId);
4084
- if (existing !== void 0 && existing.builtEpoch >= request.epoch && !provider.isDirty() && sameDensity(existing, request)) {
4673
+ if (existing !== void 0 && existing.builtEpoch >= request.epoch && !provider.isDirty() && sameBody(existing, request)) {
4085
4674
  ledger.recordClean();
4086
4675
  return { status: "clean", resources: existing };
4087
4676
  }
@@ -4103,7 +4692,7 @@ function createPyramidStore(context) {
4103
4692
  const planScale = frame.width > 0 ? plan.width / frame.width : 1;
4104
4693
  const bodySigmaTexels = request.bodySigmaCss * texelsPerCss * planScale;
4105
4694
  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 });
4695
+ const target = allocate(request.sourceId, plan, bodyPlan.level, frame.sizeEpoch, request.epoch, bodySigmaTexels, { texelsPerCss, sourceWidth: frame.width, sourceHeight: frame.height }, request.bodySigmaCss);
4107
4696
  runImport(encoder, request.sourceId, frame, target.chain);
4108
4697
  runChain(encoder, request.sourceId, plan, target.chain);
4109
4698
  runBodyBlur(encoder, request.sourceId, plan, target.chain, target.body, bodyPlan.level, bodyPlan.residualSigmaTexels);
@@ -4316,7 +4905,14 @@ function createWebGPURenderer(options = {}) {
4316
4905
  const placement = placements.get(sourceId);
4317
4906
  return isUsablePlacement(placement) ? placementFit(placement, viewport.widthCss, viewport.heightCss) : coverFit(pyramid.plan.width, pyramid.plan.height, viewport.widthCss, viewport.heightCss);
4318
4907
  };
4319
- const densityRebuilds = (explicit, pyramids) => {
4908
+ const liftChainLod = (sigmaCss, pyramid) => {
4909
+ if (pyramid === void 0 || !(sigmaCss > 0))
4910
+ return 0;
4911
+ const planScale = pyramid.sourceWidth > 0 ? pyramid.plan.width / pyramid.sourceWidth : 1;
4912
+ const sigmaTexels = sigmaCss * pyramid.texelsPerCss * planScale;
4913
+ return Math.min(chainLodForSigma(sigmaTexels), pyramid.plan.maxLod);
4914
+ };
4915
+ const staleBodyRebuilds = (explicit, pyramids) => {
4320
4916
  const requests = [];
4321
4917
  const named = new Set(explicit.map((request) => request.sourceId));
4322
4918
  for (const sourceId of providers.keys()) {
@@ -4326,7 +4922,10 @@ function createWebGPURenderer(options = {}) {
4326
4922
  if (existing === void 0)
4327
4923
  continue;
4328
4924
  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))
4925
+ const sameDensity = Math.abs(next - existing.texelsPerCss) <= 1e-6 * Math.max(1, existing.texelsPerCss);
4926
+ const sigmaCss = bodySigmaCssFor(sourceId);
4927
+ const sameSigma = Math.abs(sigmaCss - existing.bodySigmaCss) <= 1e-6 * Math.max(1, existing.bodySigmaCss);
4928
+ if (sameDensity && sameSigma)
4330
4929
  continue;
4331
4930
  requests.push({
4332
4931
  sourceId,
@@ -4339,6 +4938,11 @@ function createWebGPURenderer(options = {}) {
4339
4938
  };
4340
4939
  const unionOf = (input) => input.union ?? DEFAULT_GROUP_UNION;
4341
4940
  const variantOf = (input) => input.variant ?? "regular";
4941
+ const sourceVariant = (sourceId) => {
4942
+ const input = [...groups.values()].find((entry) => entry.input.backdropSourceId === sourceId)?.input;
4943
+ return input === void 0 ? "regular" : variantOf(input);
4944
+ };
4945
+ const bodySigmaCssFor = (sourceId) => opticsUnderPolicy(material.optics[sourceVariant(sourceId)], accessibility, material).blurSigma;
4342
4946
  const groupTintSeed = (input) => input.surfaces.find((surface) => surface.fieldReferenceOnly !== true && (surface.tint?.strength ?? 0) > 0)?.tint?.color;
4343
4947
  const stateOf = (input, resolution) => {
4344
4948
  const resolved = resolution?.groups.find((group) => group.groupId === input.groupId);
@@ -4353,8 +4957,8 @@ function createWebGPURenderer(options = {}) {
4353
4957
  function rebuildRequests(explicit) {
4354
4958
  const base = explicit ?? standaloneRequests();
4355
4959
  const { store: pyramids } = ensureContext();
4356
- const density = densityRebuilds(base, pyramids);
4357
- return density.length === 0 ? base : [...base, ...density];
4960
+ const stale = staleBodyRebuilds(base, pyramids);
4961
+ return stale.length === 0 ? base : [...base, ...stale];
4358
4962
  }
4359
4963
  function standaloneRequests() {
4360
4964
  const sampled = /* @__PURE__ */ new Set();
@@ -4390,15 +4994,16 @@ function createWebGPURenderer(options = {}) {
4390
4994
  unbuilt.push(request.sourceId);
4391
4995
  continue;
4392
4996
  }
4393
- const variant = variantOf([...groups.values()].find((entry) => entry.input.backdropSourceId === request.sourceId)?.input ?? { });
4394
- const optics = opticsUnderPolicy(material.optics[variant], accessibility, material);
4395
4997
  lastResolution.set(request.sourceId, request.resolution);
4396
4998
  const placement = placements.get(request.sourceId);
4397
4999
  const outcome = pyramids.build({
4398
5000
  sourceId: request.sourceId,
4399
5001
  epoch: request.epoch,
4400
5002
  resolution: request.resolution,
4401
- bodySigmaCss: optics.blurSigma,
5003
+ // The body's sharp width in CSS px at every device scale (W13 Decision
5004
+ // Log 8); the heavy component rides the same conversion — its level
5005
+ // is this one plus log2(sizeScatterGainMax) octaves.
5006
+ bodySigmaCss: bodySigmaCssFor(request.sourceId),
4402
5007
  viewportCss: [viewport.widthCss, viewport.heightCss],
4403
5008
  ...isUsablePlacement(placement) ? { placement } : {}
4404
5009
  }, provider, encoder);
@@ -4449,11 +5054,12 @@ function createWebGPURenderer(options = {}) {
4449
5054
  }
4450
5055
  const union = unionOf(input);
4451
5056
  const shadow = outerShadowUnderPolicy(policy, material);
4452
- let reachOcclusion = shadow.occlusion;
5057
+ const shadowBackdropLuminance = input.backdropTone === void 0 ? void 0 : input.backdropToneLevel ?? relativeLuminance(input.backdropTone);
5058
+ let reachOcclusion = outerShadowThinOcclusion(shadowBackdropLuminance, shadow);
4453
5059
  for (const surface of surfaces) {
4454
- reachOcclusion = Math.max(reachOcclusion, sizeOuterShadowOcclusionAt(shadow.occlusion, surface.sizeThickness, material));
5060
+ reachOcclusion = Math.max(reachOcclusion, outerShadowOcclusionAt(shadow, shadowBackdropLuminance, surface.spanPx, surface.sizeThickness, material));
4455
5061
  }
4456
- const shadowReachPx = outerShadowReachPx({ ...shadow, occlusion: reachOcclusion });
5062
+ const shadowReachPx = outerShadowReachPx(shadow, reachOcclusion);
4457
5063
  const snapped = snapRectToDevicePixels(groupFieldRect(surfaces, union, void 0, shadowReachPx), dpr);
4458
5064
  const rectDevice = clipFieldRectToCanvas(snapped, dpr, viewportDevice);
4459
5065
  if (rectDevice === void 0)
@@ -4493,7 +5099,7 @@ function createWebGPURenderer(options = {}) {
4493
5099
  backdropToneSizeBiasUnderPolicy(policy, material),
4494
5100
  input.backdropTone === void 0 ? 0 : backdropToneUnderPolicy(policy, material) * material.backdropToneMax
4495
5101
  ];
4496
- const backdropToneLevel = input.backdropTone === void 0 ? 0 : input.backdropToneLevel ?? relativeLuminance(input.backdropTone);
5102
+ const backdropToneLevel = shadowBackdropLuminance ?? 0;
4497
5103
  const refraction = effectiveRefraction(accessibilityRefractionCap(policy), state.refraction);
4498
5104
  const refractionScale = material.refractionScale[refraction];
4499
5105
  passes.opticsPass(encoder, {
@@ -4508,6 +5114,19 @@ function createWebGPURenderer(options = {}) {
4508
5114
  fit: pyramid === void 0 || sourceId === void 0 ? [1, 1, 0, 0] : fitFor(sourceId, pyramid),
4509
5115
  refractionScale,
4510
5116
  lensRefractionGain: material.lensRefractionGain,
5117
+ // The lens law (W12 G2) and the inner shadow's own depth gain, both
5118
+ // evaluated per pixel in the shader from the thickness and the span.
5119
+ lensHeightPerSpan: material.lensHeightPerSpan,
5120
+ lensHeightMax: material.lensHeightMax,
5121
+ lensAmountPerSpan: material.lensAmountPerSpan,
5122
+ lensAmountMax: material.lensAmountMax,
5123
+ lensThicknessReference: material.lensThicknessReference,
5124
+ lensExtentGain: material.lensExtentGain,
5125
+ lensProfileExponent: material.lensProfileExponent,
5126
+ lensOvalization: material.lensOvalization,
5127
+ lensOvalizationSpanMin: material.lensOvalizationSpanMin,
5128
+ lensOvalizationSpanMax: material.lensOvalizationSpanMax,
5129
+ shadowDepthSizeGainMax: material.lensSizeGainMax,
4511
5130
  chainMaxLod: pyramid?.plan.maxLod ?? 0,
4512
5131
  tint: optics.tint,
4513
5132
  tintAlpha: optics.tintAlpha,
@@ -4551,6 +5170,20 @@ function createWebGPURenderer(options = {}) {
4551
5170
  sizeSpanMax: material.sizeSpanMax,
4552
5171
  sizeScatterFloor: material.sizeScatterFloor,
4553
5172
  sizeScatterSpanMax: material.sizeScatterSpanMax,
5173
+ // The body's depth ramp (W13 G1, claims §5.61 §2, §5.64 §5), resolved
5174
+ // from the viewport's own ratio: the profile anchors the start's thin
5175
+ // and thick ends and the reach at dpr 1 and dpr 2, and the CPU
5176
+ // interpolates all three in dpr. The start's SPAN grading stays in the
5177
+ // shader, because a group's members are not one size and the span
5178
+ // arrives per pixel on the field's own channel — so the two anchors go
5179
+ // over and `sizeThickness` is evaluated there. The reach is handed over
5180
+ // in CSS px because the field's depth is in CSS px.
5181
+ sizeScatterRampStartThin: scatterRampStart(dpr, material, 0),
5182
+ sizeScatterRampStartThick: scatterRampStart(dpr, material, material.sizeSpanMax),
5183
+ // The fourth form's far end, at the scatter span curve's top; the shader
5184
+ // declines from the thick end to it along that curve.
5185
+ sizeScatterRampStartFar: scatterRampStart(dpr, material, material.sizeScatterSpanMax),
5186
+ sizeScatterRampReachCssPx: scatterRampReachDevicePx(dpr, material) / dpr,
4554
5187
  sizeFold: material.refractionScale[accessibilityRefractionCap(policy)],
4555
5188
  backdropTone,
4556
5189
  backdropToneColour: [
@@ -4579,7 +5212,11 @@ function createWebGPURenderer(options = {}) {
4579
5212
  backdropToneResponseStrength: backdropToneUnderPolicy(policy, material) >= 0.999 ? material.backdropToneResponseStrength : 0,
4580
5213
  backdropToneLinearMean: input.backdropToneLinearLuminance ?? backdropToneLevel,
4581
5214
  outerShadow: [
4582
- outerShadowAlpha(shadow.occlusion),
5215
+ // The thin regime's LINEAR occlusion at this group's backdrop (W14
5216
+ // G1). The conversion to the compositing space moved into the shader
5217
+ // with the regimes: it is not linear, so converting one regime here
5218
+ // and blending in the shader would not be the same law.
5219
+ outerShadowThinOcclusion(shadowBackdropLuminance, shadow),
4583
5220
  shadow.sigmaPx,
4584
5221
  shadow.spreadPx,
4585
5222
  // The offset in field-texture UV. The field spans exactly this rect, so
@@ -4587,6 +5224,19 @@ function createWebGPURenderer(options = {}) {
4587
5224
  // scaled the rasterisation.
4588
5225
  shadow.offsetPx / Math.max(rectDevice.height * cssPerDevice, 1e-6)
4589
5226
  ],
5227
+ outerShadowThick: [
5228
+ shadow.thickOcclusionAt96,
5229
+ shadow.thickOcclusionAt128,
5230
+ shadow.thickOcclusionAt160
5231
+ ],
5232
+ outerShadowLift: [
5233
+ // The lift needs a chain to copy; without one there is nothing to add,
5234
+ // and `hasBackdrop` in the shader says the same thing a second time.
5235
+ pyramid === void 0 || policy.glass === "none" ? 0 : shadow.liftAmplitude,
5236
+ shadow.liftSpanMin,
5237
+ shadow.liftSpanFull,
5238
+ liftChainLod(shadow.liftBlurSigmaCss, pyramid)
5239
+ ],
4590
5240
  outerShadowSizeGain: shadow.sizeGain,
4591
5241
  outerShadowRectCssHeight: rectDevice.height * cssPerDevice,
4592
5242
  backdrop: pyramid === void 0 || policy.glass === "none" ? void 0 : {
@@ -4868,6 +5518,6 @@ function createWebGPURenderer(options = {}) {
4868
5518
  };
4869
5519
  }
4870
5520
 
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
5521
+ 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_GAIN_MAX, 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_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, scatterRampAreaMean, scatterRampReachDevicePx, scatterRampStart, scatterSharpShare, scatterThickness, shadowDepthPx, sizeOcclusionAlpha, sizeOcclusionAlphaAt, sizeOuterShadowOcclusion, sizeOuterShadowOcclusionAt, sizeScatterSigma, sizeScatterSigmaAt, sizeShadowDepth, sizeShadowDepthAt, sizeThickness, sizeThicknessUnderPolicy, snapRectToDevicePixels, srgbToLinear, srgbToLinearChannel, statsFromBuffer, supportsTimestamps, texelsPerCssPx, tintShade, tintShadeLayer, tintToneAdaptation, tintedMaterialColour, validateAppTexture, withMaterialOverrides };
5522
+ //# sourceMappingURL=dist-RGWZAZ6J.js.map
5523
+ //# sourceMappingURL=dist-RGWZAZ6J.js.map