@vitreajs/vitrea 0.15.0 → 0.17.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.
@@ -1,4 +1,4 @@
1
- import { DEFAULT_GROUP_UNION, LowPassHysteresisDriver, WGSL_RSUPN, WGSL_RSUP, resolveFromChannels, resolveConcentric, governorFieldParams, fieldParams, DEFAULT_MOTION_PROFILE } from './chunk-WM2DV3ZD.js';
1
+ import { DEFAULT_GROUP_UNION, LowPassHysteresisDriver, WGSL_RSUPN, WGSL_RSUP, resolveFromChannels, resolveConcentric, governorFieldParams, fieldParams, DEFAULT_MOTION_PROFILE } from './chunk-IOTISK2R.js';
2
2
 
3
3
  // ../renderer-webgpu/dist/wgsl/analysis.js
4
4
  var ANALYSIS_WORKGROUP = 64;
@@ -220,6 +220,12 @@ function occlusionAlphaUnderPolicy(nominal, occlusion, lift = INCREASED_OCCLUSIO
220
220
  return 1;
221
221
  }
222
222
  }
223
+ function occlusionLiftForPolicy(policy, profile = DEFAULT_MATERIAL_PROFILE) {
224
+ if (policy.occlusion !== "increased")
225
+ return profile.increasedOcclusionLift;
226
+ const levels = profile.increasedOcclusionLiftByPolicy;
227
+ return policy.ambientTint === "reduced" ? levels?.increaseContrast ?? profile.increasedOcclusionLift : levels?.reduceTransparency ?? profile.increasedOcclusionLift;
228
+ }
223
229
  var DEFAULT_MATERIAL_PROFILE = {
224
230
  optics: {
225
231
  // σ = 8 for the regular variant, which keeps this package's blur and
@@ -1152,6 +1158,20 @@ function rejectRetiredOuterShadowLeaves(patch) {
1152
1158
  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.`);
1153
1159
  }
1154
1160
  }
1161
+ function rejectMixedBackdropToneArity(anchorX, thin, thick) {
1162
+ for (const [name, row] of [
1163
+ ["backdropToneAnchorX", anchorX],
1164
+ ["backdropToneResponseThin", thin],
1165
+ ["backdropToneResponseThick", thick]
1166
+ ]) {
1167
+ if (Array.isArray(row) && (row.length === 3 || row.length === 4))
1168
+ continue;
1169
+ throw new Error(`The backdrop tone response's ${name} is ${JSON.stringify(row) ?? String(row)}, which is not an array of three or four knots. The curve has exactly those two forms, and anything else is read as a different one by each tier: the CPU curve and the CSS mirror take the four-knot branch for any length but three, where the shader takes the three-knot branch for any length but four.`);
1170
+ }
1171
+ if (anchorX.length === thin.length && thin.length === thick.length)
1172
+ return;
1173
+ throw new Error(`The backdrop tone response's three rows resolved to different knot counts \u2014 backdropToneAnchorX ${anchorX.length}, backdropToneResponseThin ${thin.length}, backdropToneResponseThick ${thick.length}. They are one curve's knots and that curve's levels at its two thickness ends, and the shader reads the knot count off the anchors alone, so a mixed triplet renders as NaN on the CPU curve and as a fabricated segment on the GPU. A patch moving the response to a new knot count has to name all three rows.`);
1174
+ }
1155
1175
  function withMaterialOverrides(base, patch) {
1156
1176
  rejectRetiredOuterShadowLeaves(patch.outerShadow);
1157
1177
  const optics = {};
@@ -1162,6 +1182,10 @@ function withMaterialOverrides(base, patch) {
1162
1182
  for (const rung of REFRACTION_LADDER) {
1163
1183
  refractionScale[rung] = patch.refractionScale?.[rung] ?? base.refractionScale[rung];
1164
1184
  }
1185
+ const backdropToneAnchorX = patch.backdropToneAnchorX ?? base.backdropToneAnchorX;
1186
+ const backdropToneResponseThin = patch.backdropToneResponseThin ?? base.backdropToneResponseThin;
1187
+ const backdropToneResponseThick = patch.backdropToneResponseThick ?? base.backdropToneResponseThick;
1188
+ rejectMixedBackdropToneArity(backdropToneAnchorX, backdropToneResponseThin, backdropToneResponseThick);
1165
1189
  return {
1166
1190
  optics,
1167
1191
  adaptiveTintDark: patch.adaptiveTintDark ?? base.adaptiveTintDark,
@@ -1207,11 +1231,21 @@ function withMaterialOverrides(base, patch) {
1207
1231
  lensOvalizationSpanMax: patch.lensOvalizationSpanMax ?? base.lensOvalizationSpanMax,
1208
1232
  reducedTransparencyFrost: patch.reducedTransparencyFrost ?? base.reducedTransparencyFrost,
1209
1233
  increasedOcclusionLift: patch.increasedOcclusionLift ?? base.increasedOcclusionLift,
1234
+ ...(patch.increasedOcclusionLiftByPolicy ?? base.increasedOcclusionLiftByPolicy) === void 0 ? {} : { increasedOcclusionLiftByPolicy: {
1235
+ ...base.increasedOcclusionLiftByPolicy,
1236
+ ...patch.increasedOcclusionLiftByPolicy
1237
+ } },
1210
1238
  strongBorderRim: { ...base.strongBorderRim, ...patch.strongBorderRim },
1211
1239
  reducedTintAdaptation: patch.reducedTintAdaptation ?? base.reducedTintAdaptation,
1212
1240
  tintShadeDark: patch.tintShadeDark ?? base.tintShadeDark,
1213
1241
  tintShadeLight: patch.tintShadeLight ?? base.tintShadeLight,
1214
1242
  tintShadeStrength: patch.tintShadeStrength ?? base.tintShadeStrength,
1243
+ ...(patch.tintChromaScale ?? base.tintChromaScale ?? 1) === 1 ? {} : {
1244
+ tintChromaScale: patch.tintChromaScale ?? base.tintChromaScale
1245
+ },
1246
+ ...(patch.tintShadeCollapseRetention ?? base.tintShadeCollapseRetention ?? 0) === 0 ? {} : {
1247
+ tintShadeCollapseRetention: patch.tintShadeCollapseRetention ?? base.tintShadeCollapseRetention
1248
+ },
1215
1249
  backdropToneMax: patch.backdropToneMax ?? base.backdropToneMax,
1216
1250
  backdropToneLow: patch.backdropToneLow ?? base.backdropToneLow,
1217
1251
  backdropToneHigh: patch.backdropToneHigh ?? base.backdropToneHigh,
@@ -1221,9 +1255,9 @@ function withMaterialOverrides(base, patch) {
1221
1255
  rimCollapsed: patch.rimCollapsed ?? base.rimCollapsed,
1222
1256
  rimCollapsedTinted: patch.rimCollapsedTinted ?? base.rimCollapsedTinted,
1223
1257
  rimTintChroma: patch.rimTintChroma ?? base.rimTintChroma,
1224
- backdropToneAnchorX: patch.backdropToneAnchorX ?? base.backdropToneAnchorX,
1225
- backdropToneResponseThin: patch.backdropToneResponseThin ?? base.backdropToneResponseThin,
1226
- backdropToneResponseThick: patch.backdropToneResponseThick ?? base.backdropToneResponseThick,
1258
+ backdropToneAnchorX,
1259
+ backdropToneResponseThin,
1260
+ backdropToneResponseThick,
1227
1261
  backdropToneResponseStrength: patch.backdropToneResponseStrength ?? base.backdropToneResponseStrength,
1228
1262
  outerShadow: { ...base.outerShadow, ...patch.outerShadow },
1229
1263
  lightDirection: patch.lightDirection ?? base.lightDirection,
@@ -1243,7 +1277,7 @@ function opticsUnderPolicy(optics, policy, profile = DEFAULT_MATERIAL_PROFILE) {
1243
1277
  }
1244
1278
  next = {
1245
1279
  ...next,
1246
- tintAlpha: occlusionAlphaUnderPolicy(next.tintAlpha, policy.occlusion, profile.increasedOcclusionLift)
1280
+ tintAlpha: occlusionAlphaUnderPolicy(next.tintAlpha, policy.occlusion, occlusionLiftForPolicy(policy, profile))
1247
1281
  };
1248
1282
  if (policy.border === "strong") {
1249
1283
  next = {
@@ -1300,7 +1334,15 @@ function tintShade(materialLuminance, grip, profile = DEFAULT_MATERIAL_PROFILE)
1300
1334
  }
1301
1335
  function tintShadeLayer(seed, materialLuminance, grip, profile = DEFAULT_MATERIAL_PROFILE) {
1302
1336
  const shade = tintShade(materialLuminance, grip, profile);
1303
- return [seed[0] * shade, seed[1] * shade, seed[2] * shade];
1337
+ const chroma = Math.min(1, Math.max(0, profile.tintChromaScale ?? 1));
1338
+ if (chroma === 1)
1339
+ return [seed[0] * shade, seed[1] * shade, seed[2] * shade];
1340
+ const neutral = Math.max(...seed);
1341
+ return [
1342
+ (neutral + (seed[0] - neutral) * chroma) * shade,
1343
+ (neutral + (seed[1] - neutral) * chroma) * shade,
1344
+ (neutral + (seed[2] - neutral) * chroma) * shade
1345
+ ];
1304
1346
  }
1305
1347
  function tintedMaterialColour(material, tint, grip, profile = DEFAULT_MATERIAL_PROFILE) {
1306
1348
  if (tint === void 0 || tint.strength <= 0)
@@ -1359,19 +1401,45 @@ function backdropToneResponse(encodedInput, thickness, profile = DEFAULT_MATERIA
1359
1401
  const xs = profile.backdropToneAnchorX;
1360
1402
  const f = smoothstep(0, 1, thickness);
1361
1403
  const ys = [0, 1, 2].map((i) => (profile.backdropToneResponseThin[i] ?? 0) + ((profile.backdropToneResponseThick[i] ?? 0) - (profile.backdropToneResponseThin[i] ?? 0)) * f);
1362
- const x = Math.min(xs[2], Math.max(xs[0], encodedInput));
1363
- const h0 = xs[1] - xs[0];
1364
- const h1 = xs[2] - xs[1];
1365
- const d0 = (ys[1] - ys[0]) / h0;
1366
- const d1 = (ys[2] - ys[1]) / h1;
1367
- const m1 = d0 * d1 <= 0 ? 0 : 2 * d0 * d1 / (d0 + d1);
1368
- const seg = x <= xs[1] ? 0 : 1;
1369
- const h = seg === 0 ? h0 : h1;
1370
- const t = (x - (seg === 0 ? xs[0] : xs[1])) / h;
1371
- const y0 = seg === 0 ? ys[0] : ys[1];
1372
- const y1 = seg === 0 ? ys[1] : ys[2];
1373
- const s0 = seg === 0 ? d0 : m1;
1374
- const s1 = seg === 0 ? m1 : d1;
1404
+ let x, h, t, y0, y1, s0, s1;
1405
+ if (xs.length === 3) {
1406
+ x = Math.min(xs[2], Math.max(xs[0], encodedInput));
1407
+ const h0 = xs[1] - xs[0];
1408
+ const h1 = xs[2] - xs[1];
1409
+ const d0 = (ys[1] - ys[0]) / h0;
1410
+ const d1 = (ys[2] - ys[1]) / h1;
1411
+ const m1 = d0 * d1 <= 0 ? 0 : 2 * d0 * d1 / (d0 + d1);
1412
+ const seg = x <= xs[1] ? 0 : 1;
1413
+ h = seg === 0 ? h0 : h1;
1414
+ t = (x - (seg === 0 ? xs[0] : xs[1])) / h;
1415
+ y0 = seg === 0 ? ys[0] : ys[1];
1416
+ y1 = seg === 0 ? ys[1] : ys[2];
1417
+ s0 = seg === 0 ? d0 : m1;
1418
+ s1 = seg === 0 ? m1 : d1;
1419
+ } else {
1420
+ const ys4 = [
1421
+ ys[0],
1422
+ ys[1],
1423
+ ys[2],
1424
+ profile.backdropToneResponseThin[3] + (profile.backdropToneResponseThick[3] - profile.backdropToneResponseThin[3]) * f
1425
+ ];
1426
+ x = Math.min(xs[3], Math.max(xs[0], encodedInput));
1427
+ const h0 = xs[1] - xs[0], h1 = xs[2] - xs[1], h2 = xs[3] - xs[2];
1428
+ const d0 = (ys4[1] - ys4[0]) / h0;
1429
+ const d1 = (ys4[2] - ys4[1]) / h1;
1430
+ const d2 = (ys4[3] - ys4[2]) / h2;
1431
+ const m1 = d0 * d1 <= 0 ? 0 : 2 * d0 * d1 / (d0 + d1);
1432
+ const m2 = d1 * d2 <= 0 ? 0 : 2 * d1 * d2 / (d1 + d2);
1433
+ const seg = x <= xs[1] ? 0 : x <= xs[2] ? 1 : 2;
1434
+ const hs = [h0, h1, h2];
1435
+ const slopes = [d0, m1, m2, d2];
1436
+ h = hs[seg];
1437
+ t = (x - xs[seg]) / h;
1438
+ y0 = ys4[seg];
1439
+ y1 = ys4[seg + 1];
1440
+ s0 = slopes[seg];
1441
+ s1 = slopes[seg + 1];
1442
+ }
1375
1443
  return y0 * (1 + 2 * t) * (1 - t) * (1 - t) + s0 * h * t * (1 - t) * (1 - t) + y1 * t * t * (3 - 2 * t) + s1 * h * t * t * (t - 1) + levelFar;
1376
1444
  }
1377
1445
  function backdropToneSolveWeight(encodedInput, profile = DEFAULT_MATERIAL_PROFILE) {
@@ -2498,9 +2566,17 @@ var poolKey = {
2498
2566
  backdropHeavy: (sourceId) => `backdrop:${sourceId}:heavy`,
2499
2567
  backdropHeavyScratch: (sourceId) => `backdrop:${sourceId}:heavy-scratch`,
2500
2568
  backdropUpload: (sourceId) => `backdrop:${sourceId}:upload`,
2501
- groupField: (groupId) => `group:${groupId}:field`,
2502
- groupAux: (groupId) => `group:${groupId}:aux`,
2503
- groupAux2: (groupId) => `group:${groupId}:aux2`
2569
+ /*
2570
+ * The four field targets. Their key is a group's RESOURCE identity — the id
2571
+ * qualified by the plane it draws on (`groupResourceId`) — and not the group
2572
+ * id, because one group can draw on two planes in one frame and each plane's
2573
+ * field is its own size.
2574
+ */
2575
+ groupField: (resourceId) => `group:${resourceId}:field`,
2576
+ groupAux: (resourceId) => `group:${resourceId}:aux`,
2577
+ groupAux2: (resourceId) => `group:${resourceId}:aux2`,
2578
+ /** The surface's presence per pixel (W27d) — the field pass's fourth target. */
2579
+ groupPresence: (resourceId) => `group:${resourceId}:presence`
2504
2580
  };
2505
2581
 
2506
2582
  // ../renderer-webgpu/dist/gpu-context.js
@@ -2589,12 +2665,32 @@ var IDLE_CHANNELS = {
2589
2665
  glow: 0,
2590
2666
  sweep: 0,
2591
2667
  shimmer: 0,
2592
- lensStrength: 1
2668
+ lensStrength: 1,
2669
+ // 1 rather than 0, for `lensStrength`'s reason: a surface nobody is driving is
2670
+ // a surface that is fully there, and 0 is a distinct claim a driver can make.
2671
+ materialization: 1
2593
2672
  };
2594
2673
 
2595
2674
  // ../renderer-webgpu/dist/instances.js
2596
2675
  var INSTANCE_FLOATS = 18;
2597
2676
  var INSTANCE_BYTES = INSTANCE_FLOATS * 4;
2677
+ var LENS_STRENGTH_MAX = 4;
2678
+ function boundedLensStrength(strength) {
2679
+ if (Number.isNaN(strength))
2680
+ return IDLE_CHANNELS.lensStrength;
2681
+ return Math.min(LENS_STRENGTH_MAX, Math.max(0, strength));
2682
+ }
2683
+ function boundedMaterialization(value) {
2684
+ if (Number.isNaN(value))
2685
+ return IDLE_CHANNELS.materialization;
2686
+ return Math.min(1, Math.max(0, value));
2687
+ }
2688
+ function drawsMaterial(surface) {
2689
+ if (surface.fieldReferenceOnly === true)
2690
+ return false;
2691
+ const presence = surface.channels?.materialization ?? IDLE_CHANNELS.materialization;
2692
+ return boundedMaterialization(presence) > 0;
2693
+ }
2598
2694
  var channelsOf = (input) => ({
2599
2695
  ...IDLE_CHANNELS,
2600
2696
  ...input.channels
@@ -2643,7 +2739,7 @@ function resolveSurfaces(group, family, profile = DEFAULT_MATERIAL_PROFILE, poli
2643
2739
  return shape;
2644
2740
  };
2645
2741
  const paramsFor = (shape) => family === "rsup" ? governorFieldParams(shape) : fieldParams(shape);
2646
- return group.surfaces.filter((surface) => surface.fieldReferenceOnly !== true).map((surface) => {
2742
+ return group.surfaces.filter(drawsMaterial).map((surface) => {
2647
2743
  const channels = channelsOf(surface);
2648
2744
  const shape = resolveShapeOf(surface);
2649
2745
  const fieldSource = surface.concentricOf === void 0 ? shape : resolveShapeOf(byId.get(surface.concentricOf.nodeId));
@@ -2660,6 +2756,7 @@ function resolveSurfaces(group, family, profile = DEFAULT_MATERIAL_PROFILE, poli
2660
2756
  spanPx,
2661
2757
  lensDepthPx: lensDepthPx(shape.channels.thickness, spanPx, profile, profile.refractionScale[accessibilityRefractionCap(policy)]),
2662
2758
  sizeThickness: thickness,
2759
+ materialization: boundedMaterialization(channels.materialization),
2663
2760
  tintStrength: Math.min(1, Math.max(0, surface.tint?.strength ?? 0))
2664
2761
  };
2665
2762
  });
@@ -2723,10 +2820,10 @@ function packInstances(surfaces, origin, into) {
2723
2820
  data[o + 11] = s.shape.channels.thickness;
2724
2821
  data[o + 12] = s.channels.press;
2725
2822
  data[o + 13] = s.channels.glow;
2726
- data[o + 14] = Math.max(s.shape.channels.thickness, 0) * Math.min(1, Math.max(0, s.channels.lensStrength));
2823
+ data[o + 14] = Math.max(s.shape.channels.thickness, 0) * boundedLensStrength(s.channels.lensStrength);
2727
2824
  data[o + 15] = s.tintStrength;
2728
2825
  data[o + 16] = s.spanPx;
2729
- data[o + 17] = 0;
2826
+ data[o + 17] = s.materialization;
2730
2827
  }
2731
2828
  return { data, count: surfaces.length };
2732
2829
  }
@@ -3004,7 +3101,7 @@ var WGSL_INSTANCE_STRUCT = `struct Instance {
3004
3101
  lensThick : f32, // 56 authored thickness in CSS px times the lensStrength channel (W12 G2): both depths are evaluated from it per pixel
3005
3102
  tintK : f32, // 60 author tint strength, 0..1 \u2014 the seed itself is a group uniform
3006
3103
  span : f32, // 64 the surface's shorter extent, CSS px \u2014 the size law's input, both curves (W11c)
3007
- _pad : f32, // 68 stride padding: vec2f aligns the struct to 8, so 72 is the next legal size
3104
+ mat : f32, // 68 presence, 0..1 (W27d): the slot the 8-byte alignment already required
3008
3105
  };`;
3009
3106
  var WGSL_FIELD_SAMPLE = `struct FieldSample {
3010
3107
  d : f32,
@@ -3214,6 +3311,17 @@ var WGSL_FIELD_PASS = `struct FieldUniforms {
3214
3311
  // two merged members share one oval there \u2014 a value nothing measured either.
3215
3312
  // For a concentric child the box is the parent's (the child is the parent's
3216
3313
  // level set), which is the same centre and a slightly larger oval.
3314
+ //
3315
+ // 'presence' (W27d) is the fourth target and carries one number: the surface's
3316
+ // materialization, blended through the union on the same weight as everything
3317
+ // else. It is a target of its own rather than a component of one of the three
3318
+ // above because all three are full \u2014 the field carries the distance, the normal
3319
+ // and the coverage, 'aux' the four optical scalars, 'aux2' the surface's own box
3320
+ // \u2014 and repurposing a component would mean re-deriving a value the fragment
3321
+ // stages read, which is how a golden moves for a reason nobody named. It is
3322
+ // r16float rather than the working rgba16float for the same honesty: one scalar
3323
+ // costs one channel, and a wave that needs a second widens the format in the one
3324
+ // place it is declared ('passes.ts').
3217
3325
 
3218
3326
  @group(0) @binding(0) var<uniform> fu : FieldUniforms;
3219
3327
  @group(0) @binding(1) var<storage, read> instances : array<Instance>;
@@ -3223,6 +3331,7 @@ struct Member {
3223
3331
  g : vec2f,
3224
3332
  aux : vec4f,
3225
3333
  aux2 : vec4f,
3334
+ mat : f32,
3226
3335
  };
3227
3336
 
3228
3337
  fn eval_instance(i : u32, p : vec2f) -> Member {
@@ -3239,6 +3348,7 @@ fn eval_instance(i : u32, p : vec2f) -> Member {
3239
3348
  // The pixel relative to the surface's centre, and the half-extents: what the
3240
3349
  // oval's gradient needs, written here where the group-local position is exact.
3241
3350
  m.aux2 = vec4f(p - s.centre, s.half);
3351
+ m.mat = s.mat;
3242
3352
  return m;
3243
3353
  }
3244
3354
 
@@ -3246,6 +3356,7 @@ struct FieldOut {
3246
3356
  @location(0) field : vec4f,
3247
3357
  @location(1) aux : vec4f,
3248
3358
  @location(2) aux2 : vec4f,
3359
+ @location(3) presence : vec4f,
3249
3360
  };
3250
3361
 
3251
3362
  @fragment
@@ -3264,6 +3375,10 @@ fn fs_field(in : FullscreenOut) -> FieldOut {
3264
3375
  out.field = vec4f(65000.0, 0.0, -1.0, 0.0);
3265
3376
  out.aux = vec4f(0.0);
3266
3377
  out.aux2 = vec4f(0.0);
3378
+ // The idle, not zero: no surface here means nothing to be absent, and a
3379
+ // reader that walks off the instances (the outer shadow's offset read) must
3380
+ // not find a material it has to fade.
3381
+ out.presence = vec4f(1.0, 0.0, 0.0, 0.0);
3267
3382
  return out;
3268
3383
  }
3269
3384
 
@@ -3277,6 +3392,7 @@ fn fs_field(in : FullscreenOut) -> FieldOut {
3277
3392
  acc.g = mix(s.g, acc.g, h);
3278
3393
  acc.aux = mix(s.aux, acc.aux, h);
3279
3394
  acc.aux2 = mix(s.aux2, acc.aux2, h);
3395
+ acc.mat = mix(s.mat, acc.mat, h);
3280
3396
  nearest = min(nearest, s.d);
3281
3397
  }
3282
3398
  // One clamp at the end of the fold, so |union - min| <= maxBulge holds for any
@@ -3290,6 +3406,7 @@ fn fs_field(in : FullscreenOut) -> FieldOut {
3290
3406
  out.field = vec4f(acc.d, normal.x, normal.y, coverage);
3291
3407
  out.aux = acc.aux;
3292
3408
  out.aux2 = acc.aux2;
3409
+ out.presence = vec4f(acc.mat, 0.0, 0.0, 0.0);
3293
3410
  return out;
3294
3411
  }`;
3295
3412
  function fieldPassSource(family) {
@@ -3349,6 +3466,10 @@ var WGSL_HIGHLIGHT_PASS = `struct HighlightUniforms {
3349
3466
  @group(0) @binding(1) var fieldTexture : texture_2d<f32>;
3350
3467
  @group(0) @binding(2) var auxTexture : texture_2d<f32>;
3351
3468
  @group(0) @binding(3) var fieldSampler : sampler;
3469
+ /// The surface's presence per pixel (W27d) \u2014 the field pass's fourth target,
3470
+ /// bound here for the same reason the optics pass binds it: a highlight is the
3471
+ /// material catching light, and a material that is not there catches none.
3472
+ @group(0) @binding(4) var presenceTexture : texture_2d<f32>;
3352
3473
 
3353
3474
  const TAU = 6.283185307179586;
3354
3475
 
@@ -3367,14 +3488,18 @@ fn fs_highlight(in : FullscreenOut) -> @location(0) vec4f {
3367
3488
  let fieldUv = in.uv * hu.fieldFit.xy + hu.fieldFit.zw;
3368
3489
  var field : vec4f;
3369
3490
  var aux : vec4f;
3491
+ var presence : vec4f;
3370
3492
  if (hu.flags.z > 0.5) {
3371
3493
  field = textureSampleLevel(fieldTexture, fieldSampler, fieldUv, 0.0);
3372
3494
  aux = textureSampleLevel(auxTexture, fieldSampler, fieldUv, 0.0);
3495
+ presence = textureSampleLevel(presenceTexture, fieldSampler, fieldUv, 0.0);
3373
3496
  } else {
3374
3497
  let texel = vec2i(fieldUv * hu.flags.xy);
3375
3498
  field = textureLoad(fieldTexture, texel, 0);
3376
3499
  aux = textureLoad(auxTexture, texel, 0);
3500
+ presence = textureLoad(presenceTexture, texel, 0);
3377
3501
  }
3502
+ let mat = clamp(presence.x, 0.0, 1.0);
3378
3503
  let d = field.x;
3379
3504
  let normal = field.yz;
3380
3505
  let coverage = field.w;
@@ -3432,7 +3557,15 @@ fn fs_highlight(in : FullscreenOut) -> @location(0) vec4f {
3432
3557
  toneAdapt = clamp(hu.toneAdapt.w, 0.0, 1.0) * (1.0 - toneT * toneT * (3.0 - 2.0 * toneT));
3433
3558
  }
3434
3559
 
3435
- let intensity = clamp(sweep + press, 0.0, 1.0) * coverage * (1.0 - toneAdapt);
3560
+ /*
3561
+ * The presence (W27d) fades both lights on one factor, exactly as the collapse
3562
+ * above fades them: the sweep is the material's own rim catching a travelling
3563
+ * light and the glow is the material lit from a press, so neither survives a
3564
+ * surface that is not there. It is applied to the composed intensity rather
3565
+ * than to each term, because a highlight that fades on two statements of one
3566
+ * rule is a highlight that will one day fade on one of them.
3567
+ */
3568
+ let intensity = clamp(sweep + press, 0.0, 1.0) * coverage * (1.0 - toneAdapt) * mat;
3436
3569
  if (intensity <= 0.0) {
3437
3570
  return vec4f(0.0);
3438
3571
  }
@@ -3460,9 +3593,8 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
3460
3593
  /// accessibility fold \u2014 the refraction ladder read at the preference's cap,
3461
3594
  /// which every facet's span-dependent rise is multiplied by (W11c)
3462
3595
  tone : vec4f,
3463
- /// rimWidthPx, rimAlpha; (z) and (w) carried specularPower and specularGain
3464
- /// until W24 retired the one-sided specular from the rim (claims 5.108
3465
- /// section 1) and are written but unread
3596
+ /// rimWidthPx, rimAlpha; the slots retired by W24 now carry W27c's
3597
+ /// tintChromaScale (z) and tintShadeCollapseRetention (w).
3466
3598
  rim : vec4f,
3467
3599
  /// light direction, unit (xy), shadowDepth (z), shadowAlpha (w)
3468
3600
  light : vec4f,
@@ -3578,8 +3710,14 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
3578
3710
  /// carries a heavy texture, 0 where it does not. The WIDTH is not here: the
3579
3711
  /// pyramid blurred it into 'backdropHeavy' before any group was drawn, so all
3580
3712
  /// this pass decides is which of two textures the deep sample comes from.
3581
- /// (y) (z) (w) free.
3713
+ /// (y) is the DOM material mode (0 off, 1 unknown tone, 2 measured tone);
3714
+ /// (z) and (w) carry its reference level and minimum tint contrast.
3715
+ /// Texture draws keep those lanes zero, so their input bytes do not move.
3582
3716
  heavyTap : vec4f,
3717
+ /// Decision Log 18's optional fourth response knot: encoded x, thin y,
3718
+ /// thick z, and a length gate w. Appended so every three-knot field keeps its
3719
+ /// byte offset and its original shader branch.
3720
+ toneExtra : vec4f,
3583
3721
  };
3584
3722
 
3585
3723
  @group(0) @binding(0) var<uniform> ou : OpticsUniforms;
@@ -3598,6 +3736,11 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
3598
3736
  /// from. Bound at every draw; read only where 'heavyTap.x' says there is one.
3599
3737
  @group(0) @binding(8) var backdropHeavy : texture_2d<f32>;
3600
3738
 
3739
+ /// The surface's PRESENCE per pixel (W27d; contract X6): the 'materialization'
3740
+ /// channel, unioned in the field pass like every other per-surface scalar. One
3741
+ /// channel, in the field pass's fourth target \u2014 see 'wgsl/field.ts'.
3742
+ @group(0) @binding(9) var presenceTexture : texture_2d<f32>;
3743
+
3601
3744
  /// One encoded sRGB channel from a linear one \u2014 the space the backdrop tone
3602
3745
  /// response's anchors live in (W9). Mirrors material.ts's 'linearToSrgbChannel'.
3603
3746
  fn srgb_encode(c : f32) -> f32 {
@@ -3613,6 +3756,35 @@ fn tone_response(x : f32, sizeK : f32, levelFar : f32) -> f32 {
3613
3756
  let f = sizeK * sizeK * (3.0 - 2.0 * sizeK);
3614
3757
  let ys = mix(ou.toneRowThin.xyz, ou.toneRowThick.xyz, vec3f(f));
3615
3758
  let xs = ou.toneAnchor.xyz;
3759
+ if (ou.toneExtra.w > 0.5) {
3760
+ let ys4 = vec4f(ys, mix(ou.toneExtra.y, ou.toneExtra.z, f));
3761
+ let xs4 = vec4f(xs, ou.toneExtra.x);
3762
+ let xc4 = clamp(x, xs4.x, xs4.w);
3763
+ let h0 = max(xs4.y - xs4.x, 1e-4);
3764
+ let h1 = max(xs4.z - xs4.y, 1e-4);
3765
+ let h2 = max(xs4.w - xs4.z, 1e-4);
3766
+ let d0 = (ys4.y - ys4.x) / h0;
3767
+ let d1 = (ys4.z - ys4.y) / h1;
3768
+ let d2 = (ys4.w - ys4.z) / h2;
3769
+ var m1 = 0.0; var m2 = 0.0;
3770
+ if (d0 * d1 > 0.0) { m1 = 2.0 * d0 * d1 / (d0 + d1); }
3771
+ if (d1 * d2 > 0.0) { m2 = 2.0 * d1 * d2 / (d1 + d2); }
3772
+ var h = h0; var t = (xc4 - xs4.x) / h0;
3773
+ var y0 = ys4.x; var y1 = ys4.y; var s0 = d0; var s1 = m1;
3774
+ if (xc4 > xs4.y) {
3775
+ h = h1; t = (xc4 - xs4.y) / h1;
3776
+ y0 = ys4.y; y1 = ys4.z; s0 = m1; s1 = m2;
3777
+ }
3778
+ if (xc4 > xs4.z) {
3779
+ h = h2; t = (xc4 - xs4.z) / h2;
3780
+ y0 = ys4.z; y1 = ys4.w; s0 = m2; s1 = d2;
3781
+ }
3782
+ return y0 * (1.0 + 2.0 * t) * (1.0 - t) * (1.0 - t)
3783
+ + s0 * h * t * (1.0 - t) * (1.0 - t)
3784
+ + y1 * t * t * (3.0 - 2.0 * t)
3785
+ + s1 * h * t * t * (t - 1.0)
3786
+ + levelFar;
3787
+ }
3616
3788
  let xc = clamp(x, xs.x, xs.z);
3617
3789
  let h0 = max(xs.y - xs.x, 1e-4);
3618
3790
  let h1 = max(xs.z - xs.y, 1e-4);
@@ -3672,6 +3844,12 @@ struct ShadowSample {
3672
3844
  alpha : f32,
3673
3845
  falloff : f32,
3674
3846
  castSpanCss : f32,
3847
+ /// The presence of the surface that CAST the shadow (W27d), read at the offset
3848
+ /// position beside its span. A shadow belongs to the surface above it, so it
3849
+ /// fades with that surface's materialization and not with the one under this
3850
+ /// pixel \u2014 which is a real difference wherever a dissolving surface's shadow
3851
+ /// falls across a present one.
3852
+ castMat : f32,
3675
3853
  };
3676
3854
 
3677
3855
  /// The composite's peak linear occlusion above the knee, at a casting span \u2014
@@ -3702,6 +3880,7 @@ fn outer_shadow(uv : vec2f, upsampled : f32, fieldSize : vec2f) -> ShadowSample
3702
3880
  out.alpha = 0.0;
3703
3881
  out.falloff = 0.0;
3704
3882
  out.castSpanCss = 0.0;
3883
+ out.castMat = 1.0;
3705
3884
  if (ou.shadow.x <= 0.0 && ou.shadowThick.x <= 0.0) {
3706
3885
  return out;
3707
3886
  }
@@ -3729,14 +3908,18 @@ fn outer_shadow(uv : vec2f, upsampled : f32, fieldSize : vec2f) -> ShadowSample
3729
3908
  let shadowUv = clamp(vec2f(uv.x, shiftedY), vec2f(0.0), vec2f(1.0));
3730
3909
  var shadowField : vec4f;
3731
3910
  var shadowAux : vec4f;
3911
+ var shadowPresence : vec4f;
3732
3912
  if (upsampled > 0.5) {
3733
3913
  shadowField = textureSampleLevel(fieldTexture, fieldSampler, shadowUv, 0.0);
3734
3914
  shadowAux = textureSampleLevel(auxTexture, fieldSampler, shadowUv, 0.0);
3915
+ shadowPresence = textureSampleLevel(presenceTexture, fieldSampler, shadowUv, 0.0);
3735
3916
  } else {
3736
3917
  let texel = clamp(vec2i(shadowUv * fieldSize), vec2i(0), vec2i(fieldSize) - vec2i(1));
3737
3918
  shadowField = textureLoad(fieldTexture, texel, 0);
3738
3919
  shadowAux = textureLoad(auxTexture, texel, 0);
3920
+ shadowPresence = textureLoad(presenceTexture, texel, 0);
3739
3921
  }
3922
+ let castMat = clamp(shadowPresence.x, 0.0, 1.0);
3740
3923
 
3741
3924
  // The size law reaches the amplitude and nothing else \u2014 the reference's three
3742
3925
  // lengths are span-invariant across 32\u2026160 px. Every span-keyed quantity here
@@ -3766,9 +3949,12 @@ fn outer_shadow(uv : vec2f, upsampled : f32, fieldSize : vec2f) -> ShadowSample
3766
3949
  // LINEAR light; then the one conversion to the canvas's compositing space,
3767
3950
  // which is where the black term is actually composited.
3768
3951
  let occ = clamp(regime + ou.shadowSize.x * sizeK * (1.0 - regime), 0.0, 1.0);
3769
- out.alpha = 1.0 - pow(1.0 - occ, 1.0 / 2.4);
3952
+ // Times the casting surface's presence (W27d): a material that is not there
3953
+ // occludes nothing. At presence 1 this is the alpha the bed measures.
3954
+ out.alpha = (1.0 - pow(1.0 - occ, 1.0 / 2.4)) * castMat;
3770
3955
  out.falloff = outer_shadow_falloff(shadowField.x + clampedOffCss - ou.shadow.z, ou.shadow.y);
3771
3956
  out.castSpanCss = shadowAux.z;
3957
+ out.castMat = castMat;
3772
3958
  return out;
3773
3959
  }
3774
3960
 
@@ -3788,7 +3974,8 @@ fn outer_shadow(uv : vec2f, upsampled : f32, fieldSize : vec2f) -> ShadowSample
3788
3974
  /// zero where the group has no backdrop to copy \u2014 so 'dark-solid', 'impulse',
3789
3975
  /// every thin cell and every unsampled group stay byte-for-byte what they were.
3790
3976
  fn outer_shadow_lift(viewport01 : vec2f, shadow : ShadowSample) -> vec3f {
3791
- if (ou.flags.x <= 0.5 || ou.shadowLift.x <= 0.0 || shadow.falloff <= 0.0) {
3977
+ if ((ou.flags.x <= 0.5 && ou.heavyTap.y < 1.5) ||
3978
+ ou.shadowLift.x <= 0.0 || shadow.falloff <= 0.0) {
3792
3979
  return vec3f(0.0);
3793
3980
  }
3794
3981
  // Written out rather than through WGSL's 'smoothstep', which is undefined
@@ -3804,13 +3991,53 @@ fn outer_shadow_lift(viewport01 : vec2f, shadow : ShadowSample) -> vec3f {
3804
3991
  if (rise <= 0.0) {
3805
3992
  return vec3f(0.0);
3806
3993
  }
3994
+ if (ou.flags.x <= 0.5) {
3995
+ // A DOM page has no exterior texture: this is the law at the stated tone,
3996
+ // not a claim to reproduce the colour or structure beyond the silhouette.
3997
+ return ou.toneColour.rgb * (ou.shadowLift.x * rise * shadow.falloff);
3998
+ }
3807
3999
  let uv = clamp(viewport01 * ou.fit.xy + ou.fit.zw, vec2f(0.0), vec2f(1.0));
3808
4000
  let chainSample = textureSampleLevel(backdropChain, backdropSampler, uv, ou.shadowLift.w);
3809
4001
  // Premultiplied linear in, straight colour out \u2014 the same unpremultiply the
3810
4002
  // body's own samples take, for the same reason: a partially transparent
3811
4003
  // backdrop must not darken what the material adds back.
3812
4004
  let v = chainSample.rgb / max(chainSample.a, 1e-6);
3813
- return v * (ou.shadowLift.x * rise * shadow.falloff);
4005
+ // The black term's presence, on the shadow's second term: the two are one
4006
+ // shadow with one falloff (W14 G0), so one channel fades both.
4007
+ return v * (ou.shadowLift.x * rise * shadow.falloff * shadow.castMat);
4008
+ }
4009
+
4010
+ /// The DOM branch is the per-pixel mirror of platform-web's materialAtBackdrop.
4011
+ /// It evaluates the linear response, collapse, paint and rim at the stated tone,
4012
+ /// then solves the encoded layer the browser composites. A union's span stays
4013
+ /// per pixel, and the conversion reference never enables an unknown tone.
4014
+ fn dom_material_backdrop() -> vec3f {
4015
+ if (ou.heavyTap.y > 1.5) { return ou.toneColour.rgb; }
4016
+ return vec3f(ou.heavyTap.z);
4017
+ }
4018
+
4019
+ /// The cssTintAlpha secant, at the actual tone rather than before the response.
4020
+ fn dom_material_alpha(composite : vec3f, backdrop : vec3f, alpha : f32) -> f32 {
4021
+ if (alpha <= 1e-6) { return 0.0; }
4022
+ let weights = vec3f(0.2126, 0.7152, 0.0722);
4023
+ let neutral = max((composite - backdrop * (1.0 - alpha)) / alpha, vec3f(0.0));
4024
+ let b = srgb_encode(dot(backdrop, weights));
4025
+ let span = srgb_encode(dot(neutral, weights)) - b;
4026
+ if (abs(span) < ou.heavyTap.w) { return alpha; }
4027
+ return clamp((srgb_encode(dot(composite, weights)) - b) / span, 0.0, 1.0);
4028
+ }
4029
+
4030
+ /// The final encoded solve, including paint and contour light. The least opacity
4031
+ /// needed to keep every channel in gamut is an algebraic constraint, not a fit.
4032
+ /// At the reference B this returns E(C) exactly after browser source-over; away
4033
+ /// from B the scalar layer cannot reproduce the sampled path's local structure.
4034
+ fn dom_material_output(encodedComposite : vec3f, encodedBackdrop : vec3f, alpha : f32) -> vec4f {
4035
+ let c = clamp(encodedComposite, vec3f(0.0), vec3f(1.0));
4036
+ let b = clamp(encodedBackdrop, vec3f(0.0), vec3f(1.0));
4037
+ let need = select((b - c) / max(b, vec3f(1e-6)),
4038
+ (c - b) / max(vec3f(1.0) - b, vec3f(1e-6)), c >= b);
4039
+ let a = clamp(max(alpha, max(need.x, max(need.y, need.z))), 0.0, 1.0);
4040
+ return vec4f(clamp(c - b * (1.0 - a), vec3f(0.0), vec3f(a)), a);
3814
4041
  }
3815
4042
 
3816
4043
  @fragment
@@ -3823,20 +4050,48 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
3823
4050
  var field : vec4f;
3824
4051
  var aux : vec4f;
3825
4052
  var aux2 : vec4f;
4053
+ var presence : vec4f;
3826
4054
  if (ou.flags.w > 0.5) {
3827
4055
  field = textureSampleLevel(fieldTexture, fieldSampler, in.uv, 0.0);
3828
4056
  aux = textureSampleLevel(auxTexture, fieldSampler, in.uv, 0.0);
3829
4057
  aux2 = textureSampleLevel(aux2Texture, fieldSampler, in.uv, 0.0);
4058
+ presence = textureSampleLevel(presenceTexture, fieldSampler, in.uv, 0.0);
3830
4059
  } else {
3831
4060
  let texel = vec2i(in.uv * ou.flags.yz);
3832
4061
  field = textureLoad(fieldTexture, texel, 0);
3833
4062
  aux = textureLoad(auxTexture, texel, 0);
3834
4063
  aux2 = textureLoad(aux2Texture, texel, 0);
4064
+ presence = textureLoad(presenceTexture, texel, 0);
3835
4065
  }
3836
4066
 
3837
4067
  let d = field.x;
3838
4068
  let normal = field.yz;
3839
4069
  let coverage = field.w;
4070
+ /*
4071
+ * The surface's PRESENCE at this pixel (W27d; contract X6) \u2014 the
4072
+ * 'materialization' channel, 1 on a surface nobody is driving.
4073
+ *
4074
+ * It multiplies every term the material ADDS to what is behind it: the lens's
4075
+ * depth and magnitude, the body's mix away from the unblurred backdrop, the
4076
+ * material's own alpha, the author tint's coverage, the inner shadow, the rim,
4077
+ * the outer shadow and the highlight pass's light. It does NOT multiply the
4078
+ * coverage: a presence spent there would be a crossfade of the whole composite,
4079
+ * which is the alpha Apple's rule rules out ("prefer setting the effect
4080
+ * property over the alpha") and which the runtime cannot afford either \u2014 the
4081
+ * host that wrote it would form a Backdrop Root and lose the group's sampling.
4082
+ *
4083
+ * Not to be confused with 'present' below, which is what survives the backdrop
4084
+ * COLLAPSE. The two multiply many of the same terms and mean different things:
4085
+ * one is the author's presence, the other is the material having taken its
4086
+ * backdrop's tone. Both are one multiplication by one on the resting bed.
4087
+ *
4088
+ * The channel is strictly ABOVE zero here. Exactly 0 is 'Glass.identity' and is
4089
+ * resolved on the CPU by dropping the member before this pass ever runs
4090
+ * ('instances.ts'), so a surface that is not there has no silhouette to draw a
4091
+ * faded material into and no shape to union with its neighbours. What this
4092
+ * shader owns is the continuum between the endpoints.
4093
+ */
4094
+ let mat = clamp(presence.x, 0.0, 1.0);
3840
4095
 
3841
4096
  /*
3842
4097
  * The outer shadow (W8) sits UNDER the material, so it is resolved before the
@@ -3892,7 +4147,13 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
3892
4147
  // Per-pixel, unioned through the field pass. See the module note. 'aux.x' is
3893
4148
  // the authored thickness times the lensStrength channel (W12 G2); the lens's
3894
4149
  // depth and the inner shadow's are both evaluated from it and the span below.
3895
- let lensThick = max(aux.x, 0.0);
4150
+ //
4151
+ // Times the presence (W27d), because this is the one number Apple's own
4152
+ // description of materializing names \u2014 "by gradually modulating the light
4153
+ // bending and lensing" \u2014 and both depths are functions of it, so the lens
4154
+ // shallows and weakens together and the inner shadow's depth follows the same
4155
+ // channel rather than a second statement of it.
4156
+ let lensThick = max(aux.x, 0.0) * mat;
3896
4157
  // The thickness curve off the span of whichever surface owns this pixel
3897
4158
  // (W11c). 'sizeK' is the thickness factor, 0..1: zero on anything at or below
3898
4159
  // the profile's 'sizeSpanMin', saturated at 'sizeSpanMax', folded under the
@@ -4070,6 +4331,8 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
4070
4331
  let scatterLod = clamp(ou.size.w + log2(max(gainEff, 1e-4)), 0.0, ou.lens.w);
4071
4332
 
4072
4333
  var backdrop = vec3f(0.0);
4334
+ let domMaterial = ou.heavyTap.y > 0.5;
4335
+ if (domMaterial) { backdrop = dom_material_backdrop(); }
4073
4336
  if (ou.flags.x > 0.5) {
4074
4337
  // Both components at the refracted position (W11c G2): the band and the
4075
4338
  // interior are one body, and the displacement alone is the lens.
@@ -4105,6 +4368,26 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
4105
4368
  // the glass.
4106
4369
  let scatterColour = scatterSample.rgb / max(scatterSample.a, 1e-6);
4107
4370
  backdrop = mix(bodySample.rgb / max(bodySample.a, 1e-6), scatterColour, kScatter);
4371
+ /*
4372
+ * The body's own half of the presence (W27d): a material at half presence
4373
+ * shows half the BLUR, not half the surface. What a surface at presence 0
4374
+ * stands over is the page as it is, so the two components' composite lerps
4375
+ * back to the unblurred backdrop \u2014 the chain's own level 0, which is the
4376
+ * texture before any of the pyramid's widths were applied.
4377
+ *
4378
+ * Read at 'refractedUv' rather than at this pixel's own position: the lens is
4379
+ * scaled by the same channel, so the sample walks back to the pixel as the
4380
+ * presence falls and arrives exactly there at 0. One uv, one read, and no
4381
+ * discontinuity between the two ends.
4382
+ *
4383
+ * The branch is what keeps the resting material byte-identical \u2014 at presence
4384
+ * 1 the extra tap is not taken at all \u2014 and it is also what keeps a present
4385
+ * surface from paying for a channel it is not using.
4386
+ */
4387
+ if (mat < 1.0) {
4388
+ let sharpSample = textureSampleLevel(backdropChain, backdropSampler, refractedUv, 0.0);
4389
+ backdrop = mix(sharpSample.rgb / max(sharpSample.a, 1e-6), backdrop, mat);
4390
+ }
4108
4391
  }
4109
4392
 
4110
4393
  // Adaptive tint. 'adapt.w' is the strength the accessibility policy and the
@@ -4255,7 +4538,7 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
4255
4538
  * There the CSS tier's own 'backdrop-filter' is what carries the transmission.
4256
4539
  */
4257
4540
  var toneTarget = ou.toneColour.rgb;
4258
- if (ou.flags.x > 0.5) {
4541
+ if (ou.flags.x > 0.5 || domMaterial) {
4259
4542
  toneTarget = mix(toneTarget, backdrop, clamp(ou.toneRowThick.w, 0.0, 1.0));
4260
4543
  }
4261
4544
 
@@ -4273,7 +4556,15 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
4273
4556
  // opaque" is a statement about how much material there is), and the backdrop
4274
4557
  // adaptation moved it above. The author's colour never touches it \u2014 but not
4275
4558
  // for the reason the composition contract first gave (claims \xA75.36).
4276
- var colour = mix(backdrop, adapted, adaptedAlpha);
4559
+ /*
4560
+ * The material's alpha, times the presence (W27d) \u2014 and applied HERE rather
4561
+ * than to 'adaptedAlpha' above, which is load-bearing: that alpha is the
4562
+ * divisor the collapse's own composite is un-premultiplied by, so scaling it
4563
+ * there would move the adapted COLOUR as well as the amount of it. Presence is
4564
+ * how much of the material is there, not what the material is.
4565
+ */
4566
+ let presentAlpha = adaptedAlpha * mat;
4567
+ var colour = mix(backdrop, adapted, presentAlpha);
4277
4568
  /*
4278
4569
  * How much of this pixel the SURFACE owns, as the canvas will composite it.
4279
4570
  *
@@ -4295,10 +4586,41 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
4295
4586
  * in encoded sRGB, so the alpha is the CSS tier's, not the linear profile's.
4296
4587
  */
4297
4588
  var bodyAlpha = 1.0;
4298
- if (ou.flags.x <= 0.5) {
4589
+ var domAlpha = 1.0;
4590
+ if (domMaterial) {
4591
+ // The collapse transmits k*c of the DOM proxy as well. Solve the secant
4592
+ // after the linear material, never feed its encoded alpha back into R.
4593
+ /*
4594
+ * Times the presence (W27d), on the same factor 'presentAlpha' carries into
4595
+ * 'colour' just above \u2014 this branch's body is a SOLVED layer rather than a
4596
+ * composite, so the secant's own input is where the channel reaches it.
4597
+ *
4598
+ * It has to be here rather than on the result. The secant divides the
4599
+ * composite by this alpha to recover the material's own neutral, so scaling
4600
+ * both together leaves that neutral (and the span it spans) at exactly what
4601
+ * presence 1 solves, and only the returned coverage travels. Left unscaled,
4602
+ * the neutral would walk toward the backdrop as the surface thins, the span
4603
+ * would fall under 'heavyTap.w', and the guard would hand back the FULL
4604
+ * alpha \u2014 a flat tone painted over a page the surface has left.
4605
+ *
4606
+ * At presence 1 the factor is exactly 1.0 and the expression is bit-identical
4607
+ * to the resting one; at 0 the secant's own '<= 1e-6' guard returns 0.
4608
+ */
4609
+ let transmission = toneAdapt * clamp(ou.toneRowThick.w, 0.0, 1.0);
4610
+ domAlpha = dom_material_alpha(colour, backdrop,
4611
+ clamp(adaptedAlpha - transmission, 0.0, 1.0) * mat);
4612
+ } else if (ou.flags.x <= 0.5) {
4299
4613
  colour = adapted;
4300
- bodyAlpha = adaptedAlpha;
4614
+ bodyAlpha = presentAlpha;
4301
4615
  }
4616
+ /*
4617
+ * One gap this tier cannot close from here, named rather than left to be
4618
+ * discovered (W27d): on an unsampled group the blurred backdrop is a DOM proxy
4619
+ * beneath this canvas, so the body's mix toward the unblurred backdrop above
4620
+ * has no texture to mix with. A layer that fades to alpha 0 there reveals the
4621
+ * PROXY's blur rather than the page \u2014 presence takes the tint, the rim and the
4622
+ * shadows out, and the blur is the host's to fade with it.
4623
+ */
4302
4624
 
4303
4625
  // The author tint (W10). 'aux.w' is the per-pixel strength, unioned in the
4304
4626
  // field pass, so a toolbar can carry one tinted control among plain ones; the
@@ -4360,16 +4682,40 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
4360
4682
  * reach painted pixels only (W23 S10).
4361
4683
  */
4362
4684
  var rimTintColour = vec3f(1.0);
4363
- if (aux.w > 0.0) {
4685
+ /*
4686
+ * The author tint's coverage at this pixel, times the presence (W27d). The
4687
+ * paint is part of the material and not a thing painted onto the page, so a
4688
+ * dissolving surface takes its colour with it \u2014 at presence 0 the pixel is the
4689
+ * backdrop and not a flat orange over it. At presence 1 this is
4690
+ * 'clamp(aux.w, 0, 1)' exactly, which is what the tint's own law was fitted on.
4691
+ */
4692
+ let tintK = clamp(aux.w, 0.0, 1.0) * mat;
4693
+ if (tintK > 0.0) {
4364
4694
  // The untinted material's luminance at this pixel. Over a backdrop that is
4365
4695
  // the composite; as a layer it is the layer over the tone the host measured
4366
4696
  // for the group \u2014 zero where nothing was measured, the same reference-level
4367
4697
  // convention the CSS tier's 'materialLuminance' takes (within its 0.02).
4368
4698
  let u = bodyAlpha * dot(colour, vec3f(0.2126, 0.7152, 0.0722)) + (1.0 - bodyAlpha) * ou.toneColour.w;
4369
- let grip = clamp(ou.seed.w, 0.0, 1.0) * clamp(ou.tone.z, 0.0, 1.0) * (1.0 - toneAdapt);
4699
+ let grip = clamp(ou.seed.w, 0.0, 1.0) * clamp(ou.tone.z, 0.0, 1.0) *
4700
+ (1.0 - toneAdapt * (1.0 - clamp(ou.rim.w, 0.0, 1.0)));
4370
4701
  let shade = mix(1.0, clamp(mix(ou.tone.x, ou.tone.y, clamp(u, 0.0, 1.0)), 0.0, 1.0), grip);
4371
- let layer = ou.seed.rgb * shade;
4372
- let s = clamp(aux.w, 0.0, 1.0);
4702
+ // The identity branch preserves the active seed's arithmetic bit for bit.
4703
+ var seed = ou.seed.rgb;
4704
+ if (ou.rim.z < 1.0) {
4705
+ let neutral = max(seed.r, max(seed.g, seed.b));
4706
+ seed = mix(vec3f(neutral), seed, clamp(ou.rim.z, 0.0, 1.0));
4707
+ }
4708
+ let layer = seed * shade;
4709
+ /*
4710
+ * The recede's chroma collapse (W27c) shapes WHAT the paint is; presence
4711
+ * (W27d) is how much of it is there. They compose on this one strength and
4712
+ * each reaches it once: the collapse through 'seed', the presence through
4713
+ * 'tintK'. 's' is the coverage every consumer below reads \u2014 the rim's tint
4714
+ * mix, the encoded fold, and the DOM branch's own alpha \u2014 so folding the
4715
+ * presence in here is what keeps it from being applied twice downstream.
4716
+ */
4717
+ let s = tintK;
4718
+ if (domMaterial) { domAlpha = 1.0 - (1.0 - s) * (1.0 - domAlpha); }
4373
4719
  let layerLuma = max(dot(layer, vec3f(0.2126, 0.7152, 0.0722)), 0.05);
4374
4720
  rimTintColour = mix(vec3f(1.0), layer / layerLuma, clamp(ou.rimLaw.w, 0.0, 1.0) * s);
4375
4721
  let encodedMaterial = linear_to_srgb(clamp(colour, vec3f(0.0), vec3f(1.0)));
@@ -4412,7 +4758,11 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
4412
4758
  // A multiplicative occlusion of the whole composite. As a layer that is the
4413
4759
  // colour scaled and the alpha raised \u2014 (k\xB7a\xB7c, 1 \u2212 k\xB7(1 \u2212 a)) composites to
4414
4760
  // k times what (a\xB7c, a) would \u2014 and at bodyAlpha 1 it is the plain product.
4415
- let shadowKeep = 1.0 - shadowProfile * shadowDepth * ou.light.w * present;
4761
+ // Times the presence (W27d) on the AMPLITUDE, beside the depth the same channel
4762
+ // already shallowed through 'lensThick': the occlusion is the material's own,
4763
+ // and a material that is half there occludes half as much of what is behind it.
4764
+ let shadowKeep = 1.0 - shadowProfile * shadowDepth * ou.light.w * present * mat;
4765
+ if (domMaterial) { domAlpha = 1.0 - shadowKeep * (1.0 - domAlpha); }
4416
4766
  let shadowedAlpha = 1.0 - shadowKeep * (1.0 - bodyAlpha);
4417
4767
  colour = colour * (shadowKeep * bodyAlpha / max(shadowedAlpha, 1e-6));
4418
4768
  bodyAlpha = shadowedAlpha;
@@ -4464,7 +4814,7 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
4464
4814
  let rimLuma = materialAlpha * dot(materialColour, vec3f(0.2126, 0.7152, 0.0722))
4465
4815
  + (1.0 - materialAlpha) * ou.toneColour.w;
4466
4816
  let rimAmplitude = ou.rim.y + ou.rimLaw.x * rimLuma;
4467
- let rimCollapsed = mix(ou.rimLaw.y, ou.rimLaw.z, clamp(aux.w, 0.0, 1.0));
4817
+ let rimCollapsed = mix(ou.rimLaw.y, ou.rimLaw.z, tintK);
4468
4818
  /*
4469
4819
  * The lit edge (W24; claims \xA75.107) \u2014 the directional factor the whole rim is
4470
4820
  * multiplied by, symmetric about 'rimLit.xy'.
@@ -4524,10 +4874,15 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
4524
4874
  */
4525
4875
  let alongSide = clamp((rel.x / halfExt.x) * (rel.y / halfExt.y), -1.0, 1.0);
4526
4876
  let alongFactor = max(1.0 + ou.rimLit.w * sizeThick * alongSide, 0.0);
4527
- let rim = rw * lit * alongFactor * (rimAmplitude * present + rimCollapsed * toneAdapt);
4877
+ // The whole rim \u2014 both the appearance's own and the one the collapse keeps \u2014
4878
+ // times the presence (W27d). The rim is the mark that says a surface is HERE,
4879
+ // which is the first thing a surface that is not there stops saying; the same
4880
+ // argument the collapse's 'present' makes, from the author's side.
4881
+ let rim = rw * lit * alongFactor * (rimAmplitude * present + rimCollapsed * toneAdapt) * mat;
4528
4882
  let rimLight = rim * rimTintColour;
4529
- if (ou.flags.x > 0.5) {
4883
+ if (ou.flags.x > 0.5 || domMaterial) {
4530
4884
  colour = colour + rimLight;
4885
+ if (domMaterial) { domAlpha = clamp(domAlpha + rim, 0.0, 1.0); }
4531
4886
  } else {
4532
4887
  // Added light has no premultiplied form of its own \u2014 a canvas colour may
4533
4888
  // not exceed its alpha \u2014 so the layer carries the light in its opacity:
@@ -4555,6 +4910,17 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
4555
4910
  // translucent surface shows the page through it, not its own shadow. (With
4556
4911
  // an opaque body the two are the same quantity, which is how the shadow was
4557
4912
  // first written and why the difference only surfaced with the layer form.)
4913
+ if (domMaterial) {
4914
+ // Coverage precedes the gamut solve. The sampled rim may exceed white
4915
+ // before antialiasing; clipping it first would dim the half-covered edge.
4916
+ // Include the exterior shadow in the same target so its light and opacity
4917
+ // remain one valid premultiplied layer at that edge too.
4918
+ let b = linear_to_srgb(backdrop);
4919
+ let compositeEncoded = linear_to_srgb(max(colour, vec3f(0.0))) * coverage
4920
+ + (b * (1.0 - shadowAlpha) + liftEncoded) * (1.0 - coverage);
4921
+ let alpha = domAlpha * coverage + shadowAlpha * (1.0 - coverage);
4922
+ return dom_material_output(compositeEncoded, b, alpha);
4923
+ }
4558
4924
  let body = encode_output(max(colour, vec3f(0.0)), coverage * bodyAlpha);
4559
4925
  return vec4f(
4560
4926
  body.rgb + liftEncoded * (1.0 - coverage),
@@ -4647,10 +5013,12 @@ function allShaderSource() {
4647
5013
 
4648
5014
  // ../renderer-webgpu/dist/passes.js
4649
5015
  var fieldUsage = () => GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING;
5016
+ var PRESENCE_TEXTURE_FORMAT = "r16float";
4650
5017
  var PREMULTIPLIED_OVER = {
4651
5018
  color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
4652
5019
  alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }
4653
5020
  };
5021
+ var groupResourceId = (plane, groupId) => plane === "" ? groupId : `${plane}/${groupId}`;
4654
5022
  function createPassRunner(context) {
4655
5023
  const { device, pool, cache } = context;
4656
5024
  const uniforms = /* @__PURE__ */ new Map();
@@ -4690,7 +5058,8 @@ function createPassRunner(context) {
4690
5058
  targets: [
4691
5059
  { format: WORKING_TEXTURE_FORMAT },
4692
5060
  { format: WORKING_TEXTURE_FORMAT },
4693
- { format: WORKING_TEXTURE_FORMAT }
5061
+ { format: WORKING_TEXTURE_FORMAT },
5062
+ { format: PRESENCE_TEXTURE_FORMAT }
4694
5063
  ]
4695
5064
  },
4696
5065
  primitive: { topology: "triangle-list" }
@@ -4741,28 +5110,35 @@ function createPassRunner(context) {
4741
5110
  const rectHeight = Math.max(1, Math.round(args.rectDevice.height));
4742
5111
  const width = Math.max(1, Math.round(args.rectDevice.width * args.renderScale));
4743
5112
  const height = Math.max(1, Math.round(args.rectDevice.height * args.renderScale));
4744
- const field = pool.acquire(poolKey.groupField(args.groupId), {
5113
+ const field = pool.acquire(poolKey.groupField(args.resourceId), {
4745
5114
  width,
4746
5115
  height,
4747
5116
  format: WORKING_TEXTURE_FORMAT,
4748
5117
  usage: fieldUsage(),
4749
- label: `vitrea:group:${args.groupId}:field`
5118
+ label: `vitrea:group:${args.resourceId}:field`
4750
5119
  });
4751
- const aux = pool.acquire(poolKey.groupAux(args.groupId), {
5120
+ const aux = pool.acquire(poolKey.groupAux(args.resourceId), {
4752
5121
  width,
4753
5122
  height,
4754
5123
  format: WORKING_TEXTURE_FORMAT,
4755
5124
  usage: fieldUsage(),
4756
- label: `vitrea:group:${args.groupId}:aux`
5125
+ label: `vitrea:group:${args.resourceId}:aux`
4757
5126
  });
4758
- const aux2 = pool.acquire(poolKey.groupAux2(args.groupId), {
5127
+ const aux2 = pool.acquire(poolKey.groupAux2(args.resourceId), {
4759
5128
  width,
4760
5129
  height,
4761
5130
  format: WORKING_TEXTURE_FORMAT,
4762
5131
  usage: fieldUsage(),
4763
- label: `vitrea:group:${args.groupId}:aux2`
5132
+ label: `vitrea:group:${args.resourceId}:aux2`
5133
+ });
5134
+ const presence = pool.acquire(poolKey.groupPresence(args.resourceId), {
5135
+ width,
5136
+ height,
5137
+ format: PRESENCE_TEXTURE_FORMAT,
5138
+ usage: fieldUsage(),
5139
+ label: `vitrea:group:${args.resourceId}:presence`
4764
5140
  });
4765
- const slot = uniformSlot(`field:${args.groupId}`, 12);
5141
+ const slot = uniformSlot(`field:${args.resourceId}`, 12);
4766
5142
  slot.data[0] = rectWidth;
4767
5143
  slot.data[1] = rectHeight;
4768
5144
  slot.data[2] = args.cssPerDevice;
@@ -4776,12 +5152,12 @@ function createPassRunner(context) {
4776
5152
  slot.data[11] = 0;
4777
5153
  new Uint32Array(slot.data.buffer, slot.data.byteOffset + 32, 1)[0] = args.instanceCount;
4778
5154
  slot.write();
4779
- const instances = storageSlot(args.groupId);
5155
+ const instances = storageSlot(args.resourceId);
4780
5156
  instances.ensure(Math.max(args.instanceCount, 1) * INSTANCE_BYTES);
4781
5157
  instances.write(args.instances, Math.max(args.instanceCount, 1) * (INSTANCE_BYTES / 4));
4782
5158
  const pipeline = fieldPipeline(args.family);
4783
5159
  const pass = encoder.beginRenderPass({
4784
- label: `vitrea:pass:field:${args.groupId}`,
5160
+ label: `vitrea:pass:field:${args.resourceId}`,
4785
5161
  ...timed(PASS_LABEL.field),
4786
5162
  colorAttachments: [
4787
5163
  {
@@ -4801,6 +5177,15 @@ function createPassRunner(context) {
4801
5177
  loadOp: "clear",
4802
5178
  storeOp: "store",
4803
5179
  clearValue: { r: 0, g: 0, b: 0, a: 0 }
5180
+ },
5181
+ {
5182
+ view: presence.createView(),
5183
+ loadOp: "clear",
5184
+ // Cleared to the IDLE presence rather than to zero: a texel the
5185
+ // fullscreen triangle never wrote must not read as a material that
5186
+ // has been asked to disappear.
5187
+ storeOp: "store",
5188
+ clearValue: { r: 1, g: 0, b: 0, a: 0 }
4804
5189
  }
4805
5190
  ]
4806
5191
  });
@@ -4818,13 +5203,14 @@ function createPassRunner(context) {
4818
5203
  field,
4819
5204
  aux,
4820
5205
  aux2,
5206
+ presence,
4821
5207
  width,
4822
5208
  height,
4823
5209
  upsampled: width !== rectWidth || height !== rectHeight
4824
5210
  };
4825
5211
  },
4826
5212
  opticsPass(encoder, args) {
4827
- const slot = uniformSlot(`optics:${args.groupId}`, 112);
5213
+ const slot = uniformSlot(`optics:${args.resourceId}`, 116);
4828
5214
  const d = slot.data;
4829
5215
  d[0] = args.viewportDevice[0];
4830
5216
  d[1] = args.viewportDevice[1];
@@ -4856,8 +5242,8 @@ function createPassRunner(context) {
4856
5242
  d[27] = args.sizeFold;
4857
5243
  d[28] = args.rimWidth;
4858
5244
  d[29] = args.rimAlpha;
4859
- d[30] = args.specularPower;
4860
- d[31] = args.specularGain;
5245
+ d[30] = args.tintChromaScale ?? 1;
5246
+ d[31] = args.tintShadeCollapseRetention ?? 0;
4861
5247
  d[32] = args.lightDirection[0];
4862
5248
  d[33] = args.lightDirection[1];
4863
5249
  d[34] = args.shadowDepth;
@@ -4935,16 +5321,20 @@ function createPassRunner(context) {
4935
5321
  d[106] = 0;
4936
5322
  d[107] = 0;
4937
5323
  d[108] = args.heavyTapEnabled ? 1 : 0;
4938
- d[109] = 0;
4939
- d[110] = 0;
4940
- d[111] = 0;
5324
+ d[109] = args.domMaterial?.mode ?? 0;
5325
+ d[110] = args.domMaterial?.referenceBackdropLuminance ?? 0;
5326
+ d[111] = args.domMaterial?.minimumTintContrast ?? 0;
5327
+ d[112] = args.backdropToneAnchorX[3] ?? args.backdropToneAnchorX[2];
5328
+ d[113] = args.backdropToneResponseThin[3] ?? args.backdropToneResponseThin[2];
5329
+ d[114] = args.backdropToneResponseThick[3] ?? args.backdropToneResponseThick[2];
5330
+ d[115] = args.backdropToneAnchorX.length === 4 ? 1 : 0;
4941
5331
  slot.write();
4942
5332
  const chain = args.backdrop?.chain ?? placeholderView;
4943
5333
  const body = args.backdrop?.body ?? placeholderView;
4944
5334
  const heavy = args.backdrop?.heavy ?? placeholderView;
4945
5335
  const pipeline = opticsPipeline(args.targetFormat);
4946
5336
  const pass = encoder.beginRenderPass({
4947
- label: `vitrea:pass:optics:${args.groupId}`,
5337
+ label: `vitrea:pass:optics:${args.resourceId}`,
4948
5338
  ...timed(PASS_LABEL.optics),
4949
5339
  colorAttachments: [{ view: args.target, loadOp: "load", storeOp: "store" }]
4950
5340
  });
@@ -4963,14 +5353,15 @@ function createPassRunner(context) {
4963
5353
  // read at all when the governor shrank them below the group's rect.
4964
5354
  { binding: 6, resource: context.flatSampler },
4965
5355
  { binding: 7, resource: args.fields.aux2.createView() },
4966
- { binding: 8, resource: heavy }
5356
+ { binding: 8, resource: heavy },
5357
+ { binding: 9, resource: args.fields.presence.createView() }
4967
5358
  ]
4968
5359
  }));
4969
5360
  pass.draw(3);
4970
5361
  pass.end();
4971
5362
  },
4972
5363
  highlightPass(encoder, args) {
4973
- const slot = uniformSlot(`highlight:${args.groupId}`, 32);
5364
+ const slot = uniformSlot(`highlight:${args.resourceId}`, 32);
4974
5365
  const d = slot.data;
4975
5366
  d[0] = args.viewportDevice[0];
4976
5367
  d[1] = args.viewportDevice[1];
@@ -5008,7 +5399,7 @@ function createPassRunner(context) {
5008
5399
  slot.write();
5009
5400
  const pipeline = highlightPipeline(args.targetFormat);
5010
5401
  const pass = encoder.beginRenderPass({
5011
- label: `vitrea:pass:highlight:${args.groupId}`,
5402
+ label: `vitrea:pass:highlight:${args.resourceId}`,
5012
5403
  ...timed(PASS_LABEL.highlight),
5013
5404
  colorAttachments: [{ view: args.target, loadOp: "load", storeOp: "store" }]
5014
5405
  });
@@ -5020,7 +5411,8 @@ function createPassRunner(context) {
5020
5411
  { binding: 0, resource: { buffer: slot.buffer } },
5021
5412
  { binding: 1, resource: args.fields.field.createView() },
5022
5413
  { binding: 2, resource: args.fields.aux.createView() },
5023
- { binding: 3, resource: context.flatSampler }
5414
+ { binding: 3, resource: context.flatSampler },
5415
+ { binding: 4, resource: args.fields.presence.createView() }
5024
5416
  ]
5025
5417
  }));
5026
5418
  pass.draw(3);
@@ -5043,13 +5435,15 @@ function createPassRunner(context) {
5043
5435
  setTimeline(next) {
5044
5436
  timeline = next;
5045
5437
  },
5046
- forget(groupId) {
5047
- pool.release(poolKey.groupField(groupId));
5048
- pool.release(poolKey.groupAux(groupId));
5049
- pool.release(poolKey.groupAux2(groupId));
5050
- storages.get(groupId)?.destroy();
5051
- storages.delete(groupId);
5052
- for (const key of [`field:${groupId}`, `optics:${groupId}`, `highlight:${groupId}`]) {
5438
+ forget(resourceId) {
5439
+ pool.release(poolKey.groupField(resourceId));
5440
+ pool.release(poolKey.groupAux(resourceId));
5441
+ pool.release(poolKey.groupAux2(resourceId));
5442
+ pool.release(poolKey.groupPresence(resourceId));
5443
+ storages.get(resourceId)?.destroy();
5444
+ storages.delete(resourceId);
5445
+ for (const pass of ["field", "optics", "highlight"]) {
5446
+ const key = `${pass}:${resourceId}`;
5053
5447
  uniforms.get(key)?.buffer.destroy();
5054
5448
  uniforms.delete(key);
5055
5449
  }
@@ -5710,6 +6104,7 @@ function createWebGPURenderer(options = {}) {
5710
6104
  let generations = 0;
5711
6105
  let lastFrameTimeMs;
5712
6106
  let targets;
6107
+ const planesDrawn = /* @__PURE__ */ new Set();
5713
6108
  let pendingEncoder;
5714
6109
  let pendingRebuilds = 0;
5715
6110
  let pendingUnbuilt = [];
@@ -5831,7 +6226,7 @@ function createWebGPURenderer(options = {}) {
5831
6226
  return 0;
5832
6227
  return heavyTapSigmaAtScale(material, viewport.devicePixelRatio) / Math.max(viewport.devicePixelRatio, 1e-3);
5833
6228
  };
5834
- const groupTintSeed = (input) => input.surfaces.find((surface) => surface.fieldReferenceOnly !== true && (surface.tint?.strength ?? 0) > 0)?.tint?.color;
6229
+ const groupTintSeed = (input) => input.surfaces.find((surface) => drawsMaterial(surface) && (surface.tint?.strength ?? 0) > 0)?.tint?.color;
5835
6230
  const stateOf = (input, resolution) => {
5836
6231
  const resolved = resolution?.groups.find((group) => group.groupId === input.groupId);
5837
6232
  if (resolved === void 0) {
@@ -5933,10 +6328,15 @@ function createWebGPURenderer(options = {}) {
5933
6328
  ];
5934
6329
  const skipped = [];
5935
6330
  let drawn = 0;
6331
+ planesDrawn.add(active.plane);
6332
+ const resourceOf = (groupId) => groupResourceId(active.plane, groupId);
6333
+ const releaseIdle = (groupId) => passes.forget(resourceOf(groupId));
5936
6334
  for (const entry of groups.values()) {
5937
6335
  const input = entry.input;
5938
- if (input.surfaces.length === 0)
6336
+ if (input.surfaces.length === 0) {
6337
+ releaseIdle(input.groupId);
5939
6338
  continue;
6339
+ }
5940
6340
  const policy = resolution?.accessibility.material ?? accessibility;
5941
6341
  let surfaces;
5942
6342
  try {
@@ -5946,6 +6346,7 @@ function createWebGPURenderer(options = {}) {
5946
6346
  groupId: input.groupId,
5947
6347
  reason: error instanceof Error ? error.message : String(error)
5948
6348
  });
6349
+ releaseIdle(input.groupId);
5949
6350
  continue;
5950
6351
  }
5951
6352
  const union = unionOf(input);
@@ -5958,15 +6359,17 @@ function createWebGPURenderer(options = {}) {
5958
6359
  const shadowReachPx = outerShadowReachPx(shadow, reachOcclusion);
5959
6360
  const snapped = snapRectToDevicePixels(groupFieldRect(surfaces, union, void 0, shadowReachPx), dpr);
5960
6361
  const rectDevice = clipFieldRectToCanvas(snapped, dpr, viewportDevice);
5961
- if (rectDevice === void 0)
6362
+ if (rectDevice === void 0) {
6363
+ releaseIdle(input.groupId);
5962
6364
  continue;
6365
+ }
5963
6366
  const surfaceRectDevice = shadowReachPx === 0 ? rectDevice : clipFieldRectToCanvas(snapRectToDevicePixels(groupFieldRect(surfaces, union), dpr), dpr, viewportDevice) ?? rectDevice;
5964
6367
  const packed = packInstances(surfaces, [
5965
6368
  rectDevice.x * cssPerDevice,
5966
6369
  rectDevice.y * cssPerDevice
5967
6370
  ]);
5968
6371
  const fields = passes.fieldPass(encoder, {
5969
- groupId: input.groupId,
6372
+ resourceId: resourceOf(input.groupId),
5970
6373
  family: governor.knobs.fieldFamily,
5971
6374
  rectDevice,
5972
6375
  cssPerDevice,
@@ -5984,11 +6387,7 @@ function createWebGPURenderer(options = {}) {
5984
6387
  const pyramid = sourceId === void 0 ? void 0 : pyramids.resources(sourceId);
5985
6388
  const adapt = sourceId === void 0 ? void 0 : adaptationFor(sourceId).values;
5986
6389
  const nominalOptics = material.optics[variant];
5987
- const optics = opticsUnderPolicy(pyramid === void 0 && input.unsampledMaterial !== void 0 ? {
5988
- ...nominalOptics,
5989
- tint: input.unsampledMaterial.tint,
5990
- tintAlpha: input.unsampledMaterial.tintAlpha
5991
- } : nominalOptics, policy, material);
6390
+ const optics = opticsUnderPolicy(nominalOptics, policy, material);
5992
6391
  const backdropTone = [
5993
6392
  material.backdropToneLow,
5994
6393
  material.backdropToneHigh,
@@ -5999,7 +6398,7 @@ function createWebGPURenderer(options = {}) {
5999
6398
  const refraction = effectiveRefraction(accessibilityRefractionCap(policy), state.refraction);
6000
6399
  const refractionScale = material.refractionScale[refraction];
6001
6400
  passes.opticsPass(encoder, {
6002
- groupId: input.groupId,
6401
+ resourceId: resourceOf(input.groupId),
6003
6402
  target: active.optics,
6004
6403
  targetFormat: active.format,
6005
6404
  rectDevice,
@@ -6043,6 +6442,8 @@ function createWebGPURenderer(options = {}) {
6043
6442
  tintSeed: (policy.glass === "none" ? void 0 : groupTintSeed(input)) ?? optics.tint,
6044
6443
  tintToneAdaptation: tintToneAdaptation(policy, material),
6045
6444
  tintShade: [material.tintShadeDark, material.tintShadeLight, material.tintShadeStrength],
6445
+ tintChromaScale: material.tintChromaScale ?? 1,
6446
+ tintShadeCollapseRetention: material.tintShadeCollapseRetention ?? 0,
6046
6447
  // The band's half-width is a per-scale reading since W23: the reference's
6047
6448
  // rim narrows between the scales where vitrea's widened (claims §5.100
6048
6449
  // §5). At dpr 1, and on any variant whose two anchors are equal, this is
@@ -6091,6 +6492,10 @@ function createWebGPURenderer(options = {}) {
6091
6492
  // itself is already in the texture, so this pass only chooses which of
6092
6493
  // two textures the deep sample comes from.
6093
6494
  heavyTapEnabled: pyramid?.heavy !== void 0,
6495
+ ...pyramid === void 0 && input.unsampledMaterial !== void 0 ? { domMaterial: {
6496
+ ...input.unsampledMaterial,
6497
+ mode: input.backdropTone === void 0 ? 1 : 2
6498
+ } } : {},
6094
6499
  shadowDepth: optics.shadowDepth,
6095
6500
  shadowAlpha: optics.shadowAlpha,
6096
6501
  // The size law's gains, per group (W2); the per-pixel factor they
@@ -6196,9 +6601,9 @@ function createWebGPURenderer(options = {}) {
6196
6601
  shadow.thickOcclusionAt160
6197
6602
  ],
6198
6603
  outerShadowLift: [
6199
- // The lift needs a chain to copy; without one there is nothing to add,
6200
- // and `hasBackdrop` in the shader says the same thing a second time.
6201
- pyramid === void 0 || policy.glass === "none" ? 0 : shadow.liftAmplitude,
6604
+ // A DOM layer can evaluate the lift at its stated tone, but not at
6605
+ // unknown exterior pixels. No tone still means no light to add.
6606
+ policy.glass === "none" || pyramid === void 0 && (input.unsampledMaterial === void 0 || input.backdropTone === void 0) ? 0 : shadow.liftAmplitude,
6202
6607
  shadow.liftSpanMin,
6203
6608
  shadow.liftSpanFull,
6204
6609
  liftChainLod(shadow.liftBlurSigmaCss, pyramid)
@@ -6214,7 +6619,7 @@ function createWebGPURenderer(options = {}) {
6214
6619
  if (active.highlight !== void 0) {
6215
6620
  const lead = surfaces.reduce((best, surface) => surface.channels.glow > best.channels.glow ? surface : best, surfaces[0]);
6216
6621
  passes.highlightPass(encoder, {
6217
- groupId: input.groupId,
6622
+ resourceId: resourceOf(input.groupId),
6218
6623
  target: active.highlight,
6219
6624
  targetFormat: active.format,
6220
6625
  // The surface's own rect, not the shadow's — see `surfaceRectDevice`.
@@ -6361,7 +6766,8 @@ function createWebGPURenderer(options = {}) {
6361
6766
  },
6362
6767
  removeGroup(groupId) {
6363
6768
  groups.delete(groupId);
6364
- runner?.forget(groupId);
6769
+ for (const plane of planesDrawn)
6770
+ runner?.forget(groupResourceId(plane, groupId));
6365
6771
  },
6366
6772
  setAccessibility(policy) {
6367
6773
  accessibility = policy;
@@ -6377,7 +6783,8 @@ function createWebGPURenderer(options = {}) {
6377
6783
  targets = {
6378
6784
  optics: next.optics,
6379
6785
  ...next.highlight === void 0 ? {} : { highlight: next.highlight },
6380
- format: next.format ?? OUTPUT_TEXTURE_FORMAT
6786
+ format: next.format ?? OUTPUT_TEXTURE_FORMAT,
6787
+ plane: next.plane ?? ""
6381
6788
  };
6382
6789
  },
6383
6790
  drawFrame(args) {
@@ -6385,7 +6792,8 @@ function createWebGPURenderer(options = {}) {
6385
6792
  targets = {
6386
6793
  optics: args.optics,
6387
6794
  ...args.highlight === void 0 ? {} : { highlight: args.highlight },
6388
- format: args.format ?? OUTPUT_TEXTURE_FORMAT
6795
+ format: args.format ?? OUTPUT_TEXTURE_FORMAT,
6796
+ plane: args.plane ?? ""
6389
6797
  };
6390
6798
  pyramids.beginFrame(args.frame.id);
6391
6799
  pyramids.setTimeline(args.timing);
@@ -6502,6 +6910,6 @@ function createWebGPURenderer(options = {}) {
6502
6910
  };
6503
6911
  }
6504
6912
 
6505
- 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, backdropToneResponse, backdropToneSizeBiasUnderPolicy, backdropToneSolveWeight, backdropToneUnderPolicy, bodyBlurPlan, chainModule, clipFieldRectToCanvas, collapsedRimUnderPolicy, 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, heavyTapSigmaAtScale, 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, rimAlongSideFactor, rimAlongSideField, samePlacement, scatterDeepThickness, scatterFloorAtScale, scatterGainAt, scatterGainAtScale, scatterGainFarAtScale, scatterHeavyShareThickAtScale, scatterRampAreaMean, scatterRampReachDevicePx, scatterRampStart, scatterSharpShare, scatterSpanMaxAtScale, scatterThickness, shadowDepthPx, sizeOcclusionAlpha, sizeOcclusionAlphaAt, sizeOuterShadowOcclusion, sizeOuterShadowOcclusionAt, sizeScatterSigma, sizeScatterSigmaAt, sizeShadowDepth, sizeShadowDepthAt, sizeThickness, sizeThicknessUnderPolicy, sizeToneLevelFar, snapRectToDevicePixels, srgbToLinear, srgbToLinearChannel, statsFromBuffer, supportsTimestamps, texelsPerCssPx, tintShade, tintShadeLayer, tintToneAdaptation, tintedMaterialColour, validateAppTexture, withMaterialOverrides };
6506
- //# sourceMappingURL=dist-PT4PUS4K.js.map
6507
- //# sourceMappingURL=dist-PT4PUS4K.js.map
6913
+ 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, backdropToneResponse, backdropToneSizeBiasUnderPolicy, backdropToneSolveWeight, backdropToneUnderPolicy, bodyBlurPlan, chainModule, clipFieldRectToCanvas, collapsedRimUnderPolicy, 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, heavyTapSigmaAtScale, highlightModule, importColorMatrix, importModule, importPassSource, isUsablePlacement, lensAmountBasePx, lensDepthPx, lensDirection, lensDisplacementPx, lensExtentPx, lensHeightBasePx, lensMagnitudePx, lensOvalizationAt, lensSizeGain, lensSizeGainFromThickness, linearGradientStops, linearToSrgb, linearToSrgbChannel, occlusionAlphaUnderPolicy, occlusionLiftForPolicy, opticsModule, opticsUnderPolicy, outerShadowAlpha, outerShadowFalloff, outerShadowLiftRise, outerShadowOcclusionAt, outerShadowReachPx, outerShadowThickOcclusion, outerShadowThinOcclusion, outerShadowUnderPolicy, packInstances, pipelineKey, placementFit, planPyramid, poolKey, readbackDue, refractionRank, relativeLuminance, rendererError, resolveSurfaces, rimAlongSideFactor, rimAlongSideField, samePlacement, scatterDeepThickness, scatterFloorAtScale, scatterGainAt, scatterGainAtScale, scatterGainFarAtScale, scatterHeavyShareThickAtScale, scatterRampAreaMean, scatterRampReachDevicePx, scatterRampStart, scatterSharpShare, scatterSpanMaxAtScale, scatterThickness, shadowDepthPx, sizeOcclusionAlpha, sizeOcclusionAlphaAt, sizeOuterShadowOcclusion, sizeOuterShadowOcclusionAt, sizeScatterSigma, sizeScatterSigmaAt, sizeShadowDepth, sizeShadowDepthAt, sizeThickness, sizeThicknessUnderPolicy, sizeToneLevelFar, snapRectToDevicePixels, srgbToLinear, srgbToLinearChannel, statsFromBuffer, supportsTimestamps, texelsPerCssPx, tintShade, tintShadeLayer, tintToneAdaptation, tintedMaterialColour, validateAppTexture, withMaterialOverrides };
6914
+ //# sourceMappingURL=dist-T7PRMUM4.js.map
6915
+ //# sourceMappingURL=dist-T7PRMUM4.js.map