@boyernick/standard-ui-react 0.4.2-canary.65 → 0.4.2-canary.67

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyernick/standard-ui-react",
3
- "version": "0.4.2-canary.65",
3
+ "version": "0.4.2-canary.67",
4
4
  "description": "React components for StandardUI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,13 +18,24 @@ export interface GlassConfig {
18
18
  rim?: number;
19
19
  /** Rim light width. Default 3.5, less on small panes. */
20
20
  rimWidth?: number;
21
+ /**
22
+ * A hairline shade just inside the outline, deepest away from the light.
23
+ * On a light page the rim light has nothing to brighten, so without it a
24
+ * pane's only edge is its soft shadow. Default 0.16 over a light ground,
25
+ * 0 over a dark one.
26
+ */
27
+ edge?: number;
21
28
  /** Specular sheen on the lit side of the bezel. Default 0.35. */
22
29
  specular?: number;
23
30
  /** Frost blur, as a Gaussian sigma. Default 3. */
24
31
  frost?: number;
25
32
  /** Film grain. Default 0.07. */
26
33
  grain?: number;
27
- /** Multiplier on the ground seen through the glass (1 = undimmed). Default 0.8. */
34
+ /**
35
+ * Multiplier on the ground seen through the glass (1 = undimmed). Default
36
+ * 0.8 over a dark ground, 1 over a light one: dimming a white page only
37
+ * turned the glass a flat mid-grey.
38
+ */
28
39
  dim?: number;
29
40
  /** Fraction of the height where the glass starts fading out. Default 1 (no fade). */
30
41
  fadeStart?: number;
@@ -90,6 +101,7 @@ export interface ResolvedGlass {
90
101
  dispersionBlue: number;
91
102
  rim: number;
92
103
  rimWidth: number;
104
+ edge: number;
93
105
  specular: number;
94
106
  frost: number;
95
107
  grain: number;
@@ -121,6 +133,7 @@ export function resolveGlass(
121
133
  width: number,
122
134
  height: number,
123
135
  cssRadius: number,
136
+ options?: { lightGround?: boolean },
124
137
  ): ResolvedGlass {
125
138
  const short = Math.max(1, Math.min(width, height));
126
139
  const radius = pick(config.cornerRadius, cssRadius, 0, short / 2);
@@ -139,10 +152,11 @@ export function resolveGlass(
139
152
  dispersionBlue: pick(config.dispersionBlue, 0.8, 0, 1),
140
153
  rim: pick(config.rim, 0.5, 0, 2),
141
154
  rimWidth: pick(config.rimWidth, Math.min(3.5, Math.max(1.25, short * 0.04)), 0.5, 12),
155
+ edge: pick(config.edge, options?.lightGround ? 0.16 : 0, 0, 1),
142
156
  specular: pick(config.specular, 0.35, 0, 2),
143
157
  frost: pick(config.frost, 3, 0, 16),
144
158
  grain: pick(config.grain, 0.07, 0, 0.5),
145
- dim: pick(config.dim, 0.8, 0, 1.5),
159
+ dim: pick(config.dim, options?.lightGround ? 1 : 0.8, 0, 1.5),
146
160
  fadeStart: pick(config.fadeStart, 1, 0, 1),
147
161
  fadeEnd: pick(config.fadeEnd, 1, 0, 1),
148
162
  shadow: pick(config.shadow, 0.18, 0, 1),
@@ -240,6 +240,33 @@ function imageCopy(image: HTMLImageElement, ready: () => void): CanvasImageSourc
240
240
  return null;
241
241
  }
242
242
 
243
+ /** Whether a CSS colour is light (relative luminance over one half), cached per colour. */
244
+ const groundLightness = new Map<string, boolean>();
245
+ let lightnessProbe: CanvasRenderingContext2D | null = null;
246
+
247
+ function isLightGround(color: string) {
248
+ const known = groundLightness.get(color);
249
+ if (known !== undefined) return known;
250
+ lightnessProbe ??= Object.assign(document.createElement("canvas"), { width: 1, height: 1 })
251
+ .getContext("2d", { willReadFrequently: true });
252
+ let light = false;
253
+ if (lightnessProbe) {
254
+ // The canvas parses any CSS colour the page can use, oklab and all.
255
+ lightnessProbe.clearRect(0, 0, 1, 1);
256
+ lightnessProbe.fillStyle = "#000";
257
+ lightnessProbe.fillStyle = color;
258
+ lightnessProbe.fillRect(0, 0, 1, 1);
259
+ const [r, g, b] = lightnessProbe.getImageData(0, 0, 1, 1).data;
260
+ const linear = (value: number) => {
261
+ const c = value / 255;
262
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
263
+ };
264
+ light = 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b) > 0.5;
265
+ }
266
+ groundLightness.set(color, light);
267
+ return light;
268
+ }
269
+
243
270
  /** Whether drawing this source would taint a canvas (its pixels are cross-origin). */
244
271
  function unreadable(source: CanvasImageSource) {
245
272
  const probe = document.createElement("canvas");
@@ -580,7 +607,9 @@ export function createLensGlass({ root, glassElements, defaults = {} }: LensGlas
580
607
  const style = getComputedStyle(element);
581
608
  const config = { ...defaults, ...parseConfig(element.dataset.config) };
582
609
  const cssRadius = parseFloat(style.borderTopLeftRadius) || 0;
583
- const glass = resolveGlass(config, rect.width, rect.height, cssRadius);
610
+ const glass = resolveGlass(config, rect.width, rect.height, cssRadius, {
611
+ lightGround: isLightGround(ground),
612
+ });
584
613
 
585
614
  const pad = samplePadding(glass) + OVERFLOW;
586
615
  const region: Box = {
@@ -736,7 +765,7 @@ export function createLensGlass({ root, glassElements, defaults = {} }: LensGlas
736
765
  ["u_radius", glass.radius], ["u_power", glass.power], ["u_bezel", glass.bezel],
737
766
  ["u_bend", glass.bend], ["u_dispersionRed", glass.dispersionRed],
738
767
  ["u_dispersionBlue", glass.dispersionBlue], ["u_rim", glass.rim],
739
- ["u_rimWidth", glass.rimWidth], ["u_specular", glass.specular], ["u_frost", glass.frost],
768
+ ["u_rimWidth", glass.rimWidth], ["u_edge", glass.edge], ["u_specular", glass.specular], ["u_frost", glass.frost],
740
769
  ["u_grain", glass.grain], ["u_dim", glass.dim], ["u_fadeStart", glass.fadeStart],
741
770
  ["u_fadeEnd", glass.fadeEnd], ["u_shadow", glass.shadow],
742
771
  ["u_hover", glass.button ? pane.hover : 0], ["u_press", glass.button ? pane.press : 0],
@@ -31,6 +31,7 @@ uniform float u_dispersionRed;
31
31
  uniform float u_dispersionBlue;
32
32
  uniform float u_rim;
33
33
  uniform float u_rimWidth;
34
+ uniform float u_edge;
34
35
  uniform float u_specular;
35
36
  uniform float u_frost;
36
37
  uniform float u_grain;
@@ -125,6 +126,11 @@ void main() {
125
126
  color += u_rim * (1.0 + 0.4 * u_hover) * rim * (0.55 + 0.45 * facing);
126
127
  color += u_specular * 0.35 * pow(t, 3.0) * facing;
127
128
 
129
+ // Hairline edge: a light ground clips the rim to white, so shade one CSS
130
+ // pixel inside the outline instead, more on the side away from the light.
131
+ float hairline = 1.0 - smoothstep(0.0, 1.0, inside);
132
+ color = min(color, vec3(1.0)) * (1.0 - u_edge * hairline * (1.0 - 0.45 * facing));
133
+
128
134
  // Grain, fixed to the device pixel grid so a still pane never needs redrawing.
129
135
  color += (hash(floor(scenePx * u_dpr)) - 0.5) * u_grain;
130
136