@vitreajs/vitrea 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -302,9 +302,11 @@ var DIAGNOSTIC_CODES = [
302
302
  "same-plane-overlap",
303
303
  /** `regular` and `clear` nodes share one GlassGroup (§Material variants). */
304
304
  "variant-mixing",
305
+ /** Two different author tint seeds share one GlassGroup, which is one optics pass (§Material tint). */
306
+ "tint-mixing",
305
307
  /** A group's `mergeDistance` is below its `samplingPadding`, so proxies can double-filter (X1). */
306
308
  "merge-distance-below-padding",
307
- /** Two groups' padded proxies cover the same pixels, so the filter applies twice (X1). */
309
+ /** A group's padded proxy samples pixels a neighbouring group paints, so the filter applies twice (X1). */
308
310
  "group-proxy-overlap",
309
311
  /** A `clear` node has no dimming policy, so it resolved to `regular` instead. */
310
312
  "clear-variant-needs-dimming",
@@ -420,10 +422,24 @@ var FRAME_PHASES = ["collect", "read", "update", "write", "render"];
420
422
 
421
423
  // src/material.ts
422
424
  var MATERIAL_VARIANTS = ["regular", "clear"];
425
+ var clamp01 = (value) => Math.min(1, Math.max(0, value));
426
+ function glassTint(color, strength = 1) {
427
+ return {
428
+ color: [clamp01(color[0]), clamp01(color[1]), clamp01(color[2])],
429
+ strength: clamp01(strength)
430
+ };
431
+ }
432
+ function normaliseTint(tint) {
433
+ if (tint === null || tint === void 0) return void 0;
434
+ const normalised = glassTint(tint.color, tint.strength);
435
+ return normalised.strength === 0 ? void 0 : normalised;
436
+ }
423
437
  var DEFAULT_CLEAR_DIMMING = { scrim: 0.28, direction: "darken" };
424
438
  function resolveMaterial(request) {
425
439
  const { variant, dimming, nodeId, diagnostics } = request;
426
- if (variant === "regular") return { variant: "regular", adaptation: "adaptive" };
440
+ const tint = normaliseTint(request.tint);
441
+ const tinted = tint === void 0 ? {} : { tint };
442
+ if (variant === "regular") return { variant: "regular", adaptation: "adaptive", ...tinted };
427
443
  if (dimming === void 0) {
428
444
  diagnostics?.report({
429
445
  code: "clear-variant-needs-dimming",
@@ -431,9 +447,31 @@ function resolveMaterial(request) {
431
447
  subjects: [nodeId ?? "*"],
432
448
  message: `The clear variant requires a dimming policy (\xA7Material variants); without one its foreground is not guaranteed legible. This surface rendered as regular instead. Supply one on the group's material profile \u2014 DEFAULT_CLEAR_DIMMING is a usable starting point.`
433
449
  });
434
- return { variant: "regular", adaptation: "adaptive" };
450
+ return { variant: "regular", adaptation: "adaptive", ...tinted };
435
451
  }
436
- return { variant: "clear", adaptation: "constrained", dimming };
452
+ return { variant: "clear", adaptation: "constrained", dimming, ...tinted };
453
+ }
454
+ function sameTint(a, b) {
455
+ if (a === void 0 || b === void 0) return a === b;
456
+ return a.strength === b.strength && a.color[0] === b.color[0] && a.color[1] === b.color[1] && a.color[2] === b.color[2];
457
+ }
458
+ function checkTintMixing(check) {
459
+ const { groupId, members, diagnostics } = check;
460
+ const tinted = members.filter(
461
+ (member) => member.tint !== void 0
462
+ );
463
+ const distinct = tinted.filter(
464
+ (member, index) => tinted.findIndex((other) => sameTint(other.tint, member.tint)) === index
465
+ );
466
+ if (distinct.length < 2) return false;
467
+ const name = (list) => list.map((member) => member.nodeId).join(", ");
468
+ diagnostics?.report({
469
+ code: "tint-mixing",
470
+ severity: "warning",
471
+ subjects: [groupId],
472
+ message: `Group "${groupId}" asks for ${distinct.length} different tint seeds (${name(tinted)}). A group is one optics pass and carries one seed, so the GPU tier paints them all with the first surface's colour while the CSS tier honours each \u2014 the two tiers will not agree. Apple's guidance is to tint one control rather than several; give a second tinted surface its own GlassGroup if it really needs a different colour.`
473
+ });
474
+ return true;
437
475
  }
438
476
  function checkVariantMixing(check) {
439
477
  const { groupId, members, diagnostics } = check;
@@ -475,6 +513,19 @@ function inflateRect(rect, by) {
475
513
  height: rect.height + by * 2
476
514
  };
477
515
  }
516
+ function clipRect(rect, clip) {
517
+ if (clip === void 0 || clip.length === 0) return rect;
518
+ let { x, y } = rect;
519
+ let right = rect.x + rect.width;
520
+ let bottom = rect.y + rect.height;
521
+ for (const window of clip) {
522
+ x = Math.max(x, window.x);
523
+ y = Math.max(y, window.y);
524
+ right = Math.min(right, window.x + window.width);
525
+ bottom = Math.min(bottom, window.y + window.height);
526
+ }
527
+ return { x, y, width: Math.max(0, right - x), height: Math.max(0, bottom - y) };
528
+ }
478
529
  function rectsOverlap(a, b) {
479
530
  if (a.width <= 0 || a.height <= 0 || b.width <= 0 || b.height <= 0) return false;
480
531
  return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
@@ -553,17 +604,51 @@ function createGlassScene(options) {
553
604
  };
554
605
  const groupsOfSource = (sourceId) => [...groups.values()].filter((group) => group.descriptor.backdropSourceId === sourceId);
555
606
  const nodesOfGroup = (groupId) => [...nodes.values()].filter((node) => node.descriptor.groupId === groupId);
607
+ const concentricChildrenOf = (nodeId) => [...nodes.values()].filter((node) => node.descriptor.concentricOf?.nodeId === nodeId);
608
+ function requireConcentricParent(descriptor) {
609
+ const link = descriptor.concentricOf;
610
+ if (link === void 0) return;
611
+ if (link.nodeId === descriptor.id) {
612
+ throw new GlassSceneError(
613
+ "in-use",
614
+ `Glass node "${descriptor.id}" is its own concentric parent. A surface cannot be a level set of its own field.`
615
+ );
616
+ }
617
+ const parent = nodes.get(link.nodeId);
618
+ if (parent === void 0) {
619
+ throw unknown("glass node", link.nodeId);
620
+ }
621
+ if (parent.descriptor.groupId !== descriptor.groupId) {
622
+ throw new GlassSceneError(
623
+ "in-use",
624
+ `Glass node "${descriptor.id}" is concentric on "${link.nodeId}", which is in group "${parent.descriptor.groupId}" rather than "${descriptor.groupId}". A concentric child is drawn as a level set of its parent's field, and fields are resolved per group (X8 rider 2).`
625
+ );
626
+ }
627
+ const seen = /* @__PURE__ */ new Set([descriptor.id]);
628
+ let ancestor = link.nodeId;
629
+ while (ancestor !== void 0) {
630
+ if (seen.has(ancestor)) {
631
+ throw new GlassSceneError(
632
+ "in-use",
633
+ `Concentric parent "${link.nodeId}" would put glass node "${descriptor.id}" in a cycle (${[...seen].join(" \u2192 ")} \u2192 ${ancestor}). A level set has to bottom out in a shape.`
634
+ );
635
+ }
636
+ seen.add(ancestor);
637
+ ancestor = nodes.get(ancestor)?.descriptor.concentricOf?.nodeId;
638
+ }
639
+ }
556
640
  const isRebuildable = (record) => record.descriptor.kind === "texture" && record.dirtyEpoch > record.builtEpoch;
557
641
  function capabilityInputs(group, hint) {
558
642
  const source = requireSource(group.descriptor.backdropSourceId);
559
643
  const pressure = group.governor ?? governor;
644
+ const probe = group.platform ?? platform;
560
645
  return source.descriptor.kind === "texture" ? {
561
646
  configuredSource: "texture",
562
- platform,
647
+ platform: probe,
563
648
  source: source.descriptor.probe,
564
649
  governor: pressure,
565
650
  hint
566
- } : { configuredSource: "dom", platform, governor: pressure, hint };
651
+ } : { configuredSource: "dom", platform: probe, governor: pressure, hint };
567
652
  }
568
653
  const paddingOf = (group) => group.descriptor.samplingPadding ?? DEFAULT_GROUP_SAMPLING.samplingPadding;
569
654
  function samplingOf(group) {
@@ -659,6 +744,7 @@ function createGlassScene(options) {
659
744
  registerGlassNode(descriptor) {
660
745
  if (nodes.has(descriptor.id)) throw duplicate("glass node", descriptor.id);
661
746
  requireGroup(descriptor.groupId);
747
+ requireConcentricParent(descriptor);
662
748
  guardFrozenScene(descriptor.id);
663
749
  nodes.set(descriptor.id, { descriptor });
664
750
  },
@@ -666,12 +752,20 @@ function createGlassScene(options) {
666
752
  const record = requireNode(id);
667
753
  const descriptor = applyPatch(record.descriptor, patch);
668
754
  requireGroup(descriptor.groupId);
755
+ requireConcentricParent(descriptor);
669
756
  guardFrozenScene(id);
670
757
  nodes.set(id, { ...record, descriptor });
671
758
  },
672
759
  removeGlassNode(id) {
673
760
  requireNode(id);
674
761
  guardFrozenScene(id);
762
+ const children = concentricChildrenOf(id);
763
+ if (children.length > 0) {
764
+ throw new GlassSceneError(
765
+ "in-use",
766
+ `Glass node "${id}" is the concentric parent of ${children.map((node) => `"${node.descriptor.id}"`).join(", ")}. A child drawn as a level set of this field has no shape of its own once it is gone \u2014 remove the children first, or clear their \`concentricOf\`.`
767
+ );
768
+ }
675
769
  nodes.delete(id);
676
770
  },
677
771
  glassNode(id) {
@@ -690,8 +784,13 @@ function createGlassScene(options) {
690
784
  }
691
785
  nodes.set(id, { ...record, bounds, ...clip === void 0 ? {} : { clip } });
692
786
  },
693
- setPlatformProbe(probe) {
694
- platform = probe;
787
+ setPlatformProbe(probe, groupId) {
788
+ if (groupId === void 0) {
789
+ platform = probe;
790
+ return;
791
+ }
792
+ const record = requireGroup(groupId);
793
+ groups.set(groupId, { ...record, platform: probe });
695
794
  },
696
795
  setSourceProbe(sourceId, probe) {
697
796
  const record = requireSource(sourceId);
@@ -790,14 +889,21 @@ function createGlassScene(options) {
790
889
  resolvedGroups.push({ groupId, state, hint, foreground, sampling: samplingOf(group) });
791
890
  const profile = group.descriptor.material ?? { variant: "regular" };
792
891
  const members = nodesOfGroup(groupId);
892
+ const tinted = [];
793
893
  for (const node of members) {
794
894
  const nodeId = node.descriptor.id;
895
+ const declaredTint = node.descriptor.tint === void 0 ? profile.tint : node.descriptor.tint;
795
896
  const material = resolveMaterial({
796
897
  variant: node.descriptor.variant ?? profile.variant,
797
898
  ...profile.dimming === void 0 ? {} : { dimming: profile.dimming },
899
+ ...declaredTint === void 0 || declaredTint === null ? {} : { tint: declaredTint },
798
900
  nodeId,
799
901
  diagnostics
800
902
  });
903
+ tinted.push({
904
+ nodeId,
905
+ ...material.tint === void 0 ? {} : { tint: material.tint }
906
+ });
801
907
  resolvedNodes.push({
802
908
  nodeId,
803
909
  groupId,
@@ -817,6 +923,7 @@ function createGlassScene(options) {
817
923
  })),
818
924
  diagnostics
819
925
  });
926
+ checkTintMixing({ groupId, members: tinted, diagnostics });
820
927
  }
821
928
  }
822
929
  for (const group of settled) groups.set(group.descriptor.id, group);
@@ -831,16 +938,18 @@ function createGlassScene(options) {
831
938
  if (!devMode) return [];
832
939
  const measured = [...nodes.values()].filter(
833
940
  (node) => node.bounds !== void 0
834
- );
941
+ ).map((node) => ({ node, visible: clipRect(node.bounds, node.clip) }));
835
942
  const overlaps = [];
836
943
  for (let i = 0; i < measured.length; i += 1) {
837
944
  for (let j = i + 1; j < measured.length; j += 1) {
838
- const a = measured[i];
839
- const b = measured[j];
945
+ const a = measured[i]?.node;
946
+ const b = measured[j]?.node;
840
947
  if (a === void 0 || b === void 0) continue;
841
948
  const plane = a.descriptor.zSlot.plane;
842
949
  if (plane !== b.descriptor.zSlot.plane) continue;
843
- if (!rectsOverlap(a.bounds, b.bounds)) continue;
950
+ if (!rectsOverlap(measured[i]?.visible ?? a.bounds, measured[j]?.visible ?? b.bounds)) {
951
+ continue;
952
+ }
844
953
  const nodeIds = [a.descriptor.id, b.descriptor.id];
845
954
  overlaps.push({ plane, nodeIds });
846
955
  diagnostics.report({
@@ -863,12 +972,14 @@ function createGlassScene(options) {
863
972
  for (const node of nodesOfGroup(groupId)) {
864
973
  const { bounds } = node;
865
974
  if (bounds === void 0) continue;
975
+ const visible = clipRect(bounds, node.clip);
976
+ if (visible.width <= 0 || visible.height <= 0) continue;
866
977
  const plane = node.descriptor.zSlot.plane;
867
978
  const grown = byPlane.get(plane);
868
- byPlane.set(plane, grown === void 0 ? bounds : unionRect(grown, bounds));
979
+ byPlane.set(plane, grown === void 0 ? visible : unionRect(grown, visible));
869
980
  }
870
981
  for (const [plane, union] of byPlane) {
871
- boxes.push({ groupId, plane, box: inflateRect(union, padding) });
982
+ boxes.push({ groupId, plane, box: inflateRect(union, padding), clipUnion: union });
872
983
  }
873
984
  }
874
985
  const overlaps = [];
@@ -878,14 +989,14 @@ function createGlassScene(options) {
878
989
  const b = boxes[j];
879
990
  if (a === void 0 || b === void 0) continue;
880
991
  if (a.plane !== b.plane || a.groupId === b.groupId) continue;
881
- if (!rectsOverlap(a.box, b.box)) continue;
992
+ if (!rectsOverlap(a.box, b.clipUnion) && !rectsOverlap(b.box, a.clipUnion)) continue;
882
993
  const groupIds = [a.groupId, b.groupId];
883
994
  overlaps.push({ plane: a.plane, groupIds });
884
995
  diagnostics.report({
885
996
  code: "group-proxy-overlap",
886
997
  severity: "warning",
887
998
  subjects: [...groupIds],
888
- message: `Groups "${groupIds[0]}" and "${groupIds[1]}" sit close enough in the "${a.plane}" plane that their padded backdrop proxies overlap, and X1 says the filter then applies twice over that region \u2014 paint-order dependent, measured drifting up to 17/255. mergeDistance cannot help: it only unions members inside one group. Either put these surfaces in one group so they share a proxy, or separate them by more than the sum of their samplingPadding.`
999
+ message: `Groups "${groupIds[0]}" and "${groupIds[1]}" sit close enough in the "${a.plane}" plane that one group's padded backdrop proxy samples the pixels the other group paints, and X1 says the filter then applies twice over them \u2014 paint-order dependent, and steeply distance-dependent: measured at most 3/255 at a 1.5\u03C3 gap, mean 0.43 / max 4 at 1\u03C3, mean 2.56 / max 15 at 0.25\u03C3, and byte-identical zero once the gap reaches the padding. mergeDistance cannot help: it only unions members inside one group. Either put these surfaces in one group so they share a proxy, or separate them by at least the larger group's samplingPadding.`
889
1000
  });
890
1001
  }
891
1002
  }
@@ -981,7 +1092,7 @@ function isHealthy(state) {
981
1092
 
982
1093
  // src/renderer-seam.ts
983
1094
  async function loadWebGPURendererModule() {
984
- return import('./dist-FGJI5LQM.js');
1095
+ return import('./dist-RQ4ZMA3D.js');
985
1096
  }
986
1097
  async function loadWebGPURenderer() {
987
1098
  const { createWebGPURenderer } = await loadWebGPURendererModule();
@@ -996,6 +1107,6 @@ var VITREA_CONTRACTS = {
996
1107
  };
997
1108
  var RENDERER_TIERS = ["webgpu", "css"];
998
1109
 
999
- export { ACCESSIBILITY_BEHAVIOR_TABLE, ACCESSIBILITY_FLAGS, ACCESSIBILITY_PRECEDENCE, DEFAULT_BACKDROP_RESOLUTION, DEFAULT_CLEAR_DIMMING, DEFAULT_GROUP_SAMPLING, DEMOTION_REASONS, DEMOTION_RECOVERY, DIAGNOSTIC_CODES, FOREGROUND_MODES, FRAME_PHASES, GLASS_PLANES, GOVERNOR_PRESSURES, GlassSceneError, HINT_AVAILABILITIES, MATERIAL_VARIANTS, NOMINAL_ACCESSIBILITY_POLICY, OVERRIDABLE_ACCESSIBILITY_FLAGS, RENDERER_TIERS, SAMPLED_ASYNC_DEFAULTS, SAMPLED_ASYNC_RATE_LIMITS, VITREA_CONTRACTS, WEBGPU_AVAILABILITIES, checkVariantMixing, classifyStateChange, compareZSlot, createDiagnosticsChannel, createFrameScheduler, createGlassScene, defaultForegroundAdaptation, inflateRect, isHealthy, loadWebGPURenderer, loadWebGPURendererModule, rectsOverlap, resolveAccessibilityPolicy, resolveBackdropHint, resolveForegroundAdaptation, resolveGlassGroupState, resolveMaterial, unionRect };
1110
+ export { ACCESSIBILITY_BEHAVIOR_TABLE, ACCESSIBILITY_FLAGS, ACCESSIBILITY_PRECEDENCE, DEFAULT_BACKDROP_RESOLUTION, DEFAULT_CLEAR_DIMMING, DEFAULT_GROUP_SAMPLING, DEMOTION_REASONS, DEMOTION_RECOVERY, DIAGNOSTIC_CODES, FOREGROUND_MODES, FRAME_PHASES, GLASS_PLANES, GOVERNOR_PRESSURES, GlassSceneError, HINT_AVAILABILITIES, MATERIAL_VARIANTS, NOMINAL_ACCESSIBILITY_POLICY, OVERRIDABLE_ACCESSIBILITY_FLAGS, RENDERER_TIERS, SAMPLED_ASYNC_DEFAULTS, SAMPLED_ASYNC_RATE_LIMITS, VITREA_CONTRACTS, WEBGPU_AVAILABILITIES, checkTintMixing, checkVariantMixing, classifyStateChange, clipRect, compareZSlot, createDiagnosticsChannel, createFrameScheduler, createGlassScene, defaultForegroundAdaptation, glassTint, inflateRect, isHealthy, loadWebGPURenderer, loadWebGPURendererModule, rectsOverlap, resolveAccessibilityPolicy, resolveBackdropHint, resolveForegroundAdaptation, resolveGlassGroupState, resolveMaterial, unionRect };
1000
1111
  //# sourceMappingURL=index.js.map
1001
1112
  //# sourceMappingURL=index.js.map