vireglass 2.3.1 → 3.0.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/README.md +37 -6
- package/dist/{adapters-BOPIRMnL.d.cts → adapters--bXcGvhW.d.cts} +2 -2
- package/dist/{adapters-B_amRBqc.d.ts → adapters-BlMhc6pI.d.ts} +2 -2
- package/dist/{chunk-MMPK6DCK.js → chunk-CPSIY2DZ.js} +58 -8
- package/dist/chunk-CPSIY2DZ.js.map +1 -0
- package/dist/{chunk-UDPJLNFW.js → chunk-RWUR3OV6.js} +4 -4
- package/dist/{chunk-UDPJLNFW.js.map → chunk-RWUR3OV6.js.map} +1 -1
- package/dist/{chunk-CTTZNZFV.js → chunk-VLDD655U.js} +3 -3
- package/dist/{chunk-CTTZNZFV.js.map → chunk-VLDD655U.js.map} +1 -1
- package/dist/{chunk-V47YU75P.js → chunk-YAN2OK4H.js} +31 -38
- package/dist/chunk-YAN2OK4H.js.map +1 -0
- package/dist/{chunk-SLRPHDV7.js → chunk-Z4FKYY47.js} +3 -3
- package/dist/{chunk-SLRPHDV7.js.map → chunk-Z4FKYY47.js.map} +1 -1
- package/dist/dom.cjs +180 -55
- package/dist/dom.cjs.map +1 -1
- package/dist/dom.d.cts +72 -25
- package/dist/dom.d.ts +72 -25
- package/dist/dom.js +116 -54
- package/dist/dom.js.map +1 -1
- package/dist/{geometry-RhKd7CM5.d.cts → geometry-ip9WSlZv.d.cts} +1 -1
- package/dist/{geometry-GvXtt-_S.d.ts → geometry-pabkYV7w.d.ts} +1 -1
- package/dist/{group-model-z4N27qsC.d.ts → group-model-CQiI5cCe.d.ts} +1 -1
- package/dist/{group-model-DwtixGhG.d.cts → group-model-DX2_W9J_.d.cts} +1 -1
- package/dist/index.cjs +92 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +5 -5
- package/dist/law.cjs +56 -6
- package/dist/law.cjs.map +1 -1
- package/dist/law.d.cts +54 -4
- package/dist/law.d.ts +54 -4
- package/dist/law.js +1 -1
- package/dist/{material-CnUmdKg1.d.ts → material-D9m1EN47.d.cts} +9 -6
- package/dist/{material-CnUmdKg1.d.cts → material-D9m1EN47.d.ts} +9 -6
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/reference.d.cts +2 -2
- package/dist/reference.d.ts +2 -2
- package/dist/reference.js +2 -2
- package/dist/web/index.cjs +91 -39
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.d.cts +3 -3
- package/dist/web/index.d.ts +3 -3
- package/dist/web/index.js +4 -4
- package/docs/dom.md +49 -3
- package/docs/law.md +1 -1
- package/package.json +1 -1
- package/src/dom/body.ts +93 -39
- package/src/dom/index.ts +49 -20
- package/src/dom/probe.ts +1 -1
- package/src/dom/rim.ts +63 -28
- package/src/dom/style-hold.ts +8 -0
- package/src/law.ts +54 -4
- package/src/lens-shader.ts +26 -33
- package/src/material.ts +10 -7
- package/dist/chunk-MMPK6DCK.js.map +0 -1
- package/dist/chunk-V47YU75P.js.map +0 -1
package/dist/web/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { b as VireGlassMorph, c as VireGlassTouch, V as VireGlassAccent } from '../adapters
|
|
2
|
-
import { V as VireGlassGeometry } from '../geometry-
|
|
3
|
-
import { V as VireGlassOptics, o as VireGlassDebugMode } from '../material-
|
|
1
|
+
import { b as VireGlassMorph, c as VireGlassTouch, V as VireGlassAccent } from '../adapters--bXcGvhW.cjs';
|
|
2
|
+
import { V as VireGlassGeometry } from '../geometry-ip9WSlZv.cjs';
|
|
3
|
+
import { V as VireGlassOptics, o as VireGlassDebugMode } from '../material-D9m1EN47.cjs';
|
|
4
4
|
|
|
5
5
|
declare const PROBE_GRID_WIDTH = 48;
|
|
6
6
|
declare const PROBE_GRID_HEIGHT = 96;
|
package/dist/web/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { b as VireGlassMorph, c as VireGlassTouch, V as VireGlassAccent } from '../adapters-
|
|
2
|
-
import { V as VireGlassGeometry } from '../geometry-
|
|
3
|
-
import { V as VireGlassOptics, o as VireGlassDebugMode } from '../material-
|
|
1
|
+
import { b as VireGlassMorph, c as VireGlassTouch, V as VireGlassAccent } from '../adapters-BlMhc6pI.js';
|
|
2
|
+
import { V as VireGlassGeometry } from '../geometry-pabkYV7w.js';
|
|
3
|
+
import { V as VireGlassOptics, o as VireGlassDebugMode } from '../material-D9m1EN47.js';
|
|
4
4
|
|
|
5
5
|
declare const PROBE_GRID_WIDTH = 48;
|
|
6
6
|
declare const PROBE_GRID_HEIGHT = 96;
|
package/dist/web/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { toGLSL, SURFACE_SHADER } from '../chunk-
|
|
2
|
-
import { LENS_SHADER, toLensProps, toSurfaceUniforms, toDeviceSurfaceUniforms, DYNAMIC_UNIFORMS, ICON_UNIFORMS, OVERLAY_UNIFORMS } from '../chunk-
|
|
3
|
-
import { lensPadDp, surfacePadDp, shadowOpacity } from '../chunk-
|
|
4
|
-
import '../chunk-
|
|
1
|
+
import { toGLSL, SURFACE_SHADER } from '../chunk-RWUR3OV6.js';
|
|
2
|
+
import { LENS_SHADER, toLensProps, toSurfaceUniforms, toDeviceSurfaceUniforms, DYNAMIC_UNIFORMS, ICON_UNIFORMS, OVERLAY_UNIFORMS } from '../chunk-YAN2OK4H.js';
|
|
3
|
+
import { lensPadDp, surfacePadDp, shadowOpacity } from '../chunk-Z4FKYY47.js';
|
|
4
|
+
import '../chunk-CPSIY2DZ.js';
|
|
5
5
|
|
|
6
6
|
// src/web/gl.ts
|
|
7
7
|
var FULLSCREEN_TRIANGLE_VERTEX_SOURCE = `#version 300 es
|
package/docs/dom.md
CHANGED
|
@@ -41,8 +41,8 @@ checks the centre is neutral.
|
|
|
41
41
|
| §1 refraction, magnification | `feDisplacementMap` along a baked displacement map |
|
|
42
42
|
| §1 dispersion | three displacement passes, one per channel, at the shader's own index shifts |
|
|
43
43
|
| §1 diffraction, §3 interference | one baked hue map, multiplied in by an arithmetic composite |
|
|
44
|
-
| §2 rim light, two arcs, dark edge | a masked conic gradient on `::after
|
|
45
|
-
| §3 body, legibility, presence | `resolveBody` — the lens's own law, one
|
|
44
|
+
| §2 rim light, two arcs, dark edge | a masked conic gradient on `::after`: a light line round the whole contour with the lens's key-light lobe on it, over a dark edge; strength from the ambient lightness |
|
|
45
|
+
| §3 body, legibility, presence | `resolveBody` and `bodyLayer` — the lens's own law, composed into one translucent layer per element, painted as `background-color` |
|
|
46
46
|
| §3 ink polarity | `shouldInkBeLight`, the same function every platform uses |
|
|
47
47
|
| §4 adaptive shadow | `box-shadow`, density from `shadowOpacityFrom` |
|
|
48
48
|
| §5 touch, press, drag, wave, rise | `createDeform` driving the same `touchWarp` the shader uses |
|
|
@@ -57,6 +57,51 @@ lobe, its exponent, the 0.45 opposing arc and the 0.22 dark edge are the lens's
|
|
|
57
57
|
spectral hue is baked rather than evaluated, because a filter graph has no way to run a function
|
|
58
58
|
per pixel but can multiply by an image.
|
|
59
59
|
|
|
60
|
+
## Measured against the reference
|
|
61
|
+
|
|
62
|
+
The controls of A 2:17 (over dark video), M 2:52 (over sky), A 2:18 (grass), M 18:26 (a flower),
|
|
63
|
+
M 18:30 (the same under reduced transparency) and M 11:58 (text) were cut from the reference frames,
|
|
64
|
+
the control's own footprint filled smoothly from its surround, and a 44 dp circle or capsule of
|
|
65
|
+
`VIREGLASS_CONTROL_MATERIAL` laid over each at device pixel ratio 3, with the footprint's mean colour
|
|
66
|
+
as `sample`. Body and surround are OKLab medians of the rings 0.15 to 0.45 R inside the rim and 0.15
|
|
67
|
+
to 0.55 R outside it; the rim is the brightest of 24 angular bins (90th percentile) over the surround.
|
|
68
|
+
The frames are local and are not in the repository.
|
|
69
|
+
|
|
70
|
+
Body lightness over the surround (ΔL) and rim over the surround, reference, then 2.3.1, then this
|
|
71
|
+
release:
|
|
72
|
+
|
|
73
|
+
| control | ref ΔL | 2.3.1 | now | ref rim | 2.3.1 | now |
|
|
74
|
+
|---|---|---|---|---|---|---|
|
|
75
|
+
| A 2:17 skip back | +0.086 | +0.023 | +0.090 | 0.240 | 0.707 | 0.253 |
|
|
76
|
+
| A 2:17 pause | +0.071 | +0.038 | +0.117 | 0.257 | 0.762 | 0.310 |
|
|
77
|
+
| A 2:17 skip forward | +0.045 | +0.027 | +0.114 | 0.268 | 0.796 | 0.320 |
|
|
78
|
+
| A 2:17 Continue | +0.080 | +0.076 | +0.143 | 0.256 | 0.759 | 0.317 |
|
|
79
|
+
| A 2:17 Info | +0.126 | +0.018 | +0.088 | 0.268 | 0.704 | 0.236 |
|
|
80
|
+
| M 2:52 next, over sky | +0.076 | +0.002 | +0.025 | 0.246 | 0.329 | 0.240 |
|
|
81
|
+
| A 2:18 more, over grass (a) | +0.232 | +0.016 | +0.061 | 0.372 | 0.523 | 0.352 |
|
|
82
|
+
| M 18:26 flower (a) | +0.040 | -0.019 | +0.004 | 0.291 | 0.293 | 0.203 |
|
|
83
|
+
| M 18:30 reduced transparency (a) | +0.169 | -0.127 | -0.367 | 0.187 | 0.301 | 0.192 |
|
|
84
|
+
| M 11:58 bar over text (a) | +0.049 | -0.044 | -0.040 | 0.114 | 0.114 | 0.063 |
|
|
85
|
+
|
|
86
|
+
(a) the backdrop under the control is not in the frame, so the surround is only a guess for it.
|
|
87
|
+
|
|
88
|
+
The body is lighter than the surround on every smooth row, as in the reference, and within 0.05 of
|
|
89
|
+
it on skip back, pause and Info; skip forward and Continue are 0.07 and 0.06 above, and sky is 0.051
|
|
90
|
+
below. Chroma is within 0.04 of the reference on every row. The rim is within 0.08 on every smooth
|
|
91
|
+
row. The medium's own lift (matte, pull, concentration of light) sets the body now; the control
|
|
92
|
+
material no longer asks for a presence floor on top of it. A presence that brings the sky inside 0.05
|
|
93
|
+
puts two dark rows further out, and the reference's own spread (+0.045 to +0.126 over similar
|
|
94
|
+
backdrops) is wider than the tolerance, so one constant cannot sit inside all of them.
|
|
95
|
+
|
|
96
|
+
Reduced transparency (M 18:30) is not held to the reference: the backdrop's lightness sits at the
|
|
97
|
+
ink polarity's switch, the ink stays light, and a body at the density that mode asks for goes to its
|
|
98
|
+
pole, a dark plate where the reference is a light one.
|
|
99
|
+
|
|
100
|
+
The WebGL2 lens and this path were measured on the same flat backdrops (the footprint means above)
|
|
101
|
+
with the same material: body within 0.019 of each other everywhere, rim within 0.021 over dark,
|
|
102
|
+
0.053 over sky and grass and 0.024 over near-white, the lens fainter over light. Both are inside the
|
|
103
|
+
rim tolerance.
|
|
104
|
+
|
|
60
105
|
## The probe reads styles, not pixels
|
|
61
106
|
|
|
62
107
|
Adaptation needs to know what is behind the element, and the same door that stops the lens reading
|
|
@@ -68,7 +113,8 @@ Compositing is not optional. Taking the first layer with any alpha at all reads
|
|
|
68
113
|
exist — over a dark page the ink goes dark and the label disappears.
|
|
69
114
|
|
|
70
115
|
It is exact for colour-defined surfaces and blind to images, video and iframes. Hosts pass their
|
|
71
|
-
own sample there; an app already knows its cover art's accent
|
|
116
|
+
own sample there; an app already knows its cover art's accent, and `setSample` changes it later
|
|
117
|
+
without re-attaching.
|
|
72
118
|
|
|
73
119
|
## What it costs
|
|
74
120
|
|
package/docs/law.md
CHANGED
|
@@ -42,7 +42,7 @@ It checks three things, and reports a fourth:
|
|
|
42
42
|
- and it names every unmeasured value, every run — and fails if this page or the README states a
|
|
43
43
|
different number, because a count written out in prose goes stale the moment a value is added.
|
|
44
44
|
|
|
45
|
-
Today that last line reads **
|
|
45
|
+
Today that last line reads **51 of 130**. That is the real state of the model, and it is meant to
|
|
46
46
|
be uncomfortable. It was 9 until the constants hiding inside `optics.ts`, `geometry.ts` and
|
|
47
47
|
`touch-response.ts` were brought in — the number did not get worse, the instrument got honest.
|
|
48
48
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vireglass",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "A physically-derived liquid glass material for interfaces. One material model, three render targets: WebGL2, live DOM, and Android AGSL.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Danil Kotlaev",
|
package/src/dom/body.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// The glass BODY (docs/reference.md §3): how dense the medium has to be over this particular
|
|
2
2
|
// backdrop, and which way it tints.
|
|
3
3
|
//
|
|
4
|
-
// The law is the lens shader's,
|
|
4
|
+
// The law is the lens shader's, ported to one value per element instead of one per
|
|
5
5
|
// pixel — the DOM path has a single backdrop sample, not a plane. Constants keep their names from
|
|
6
6
|
// the shader so the two stay comparable.
|
|
7
|
-
import { BODY } from '../law';
|
|
7
|
+
import { BODY, MEDIUM } from '../law';
|
|
8
8
|
import type { VireGlassOptics } from '../material';
|
|
9
9
|
|
|
10
10
|
const {
|
|
@@ -14,8 +14,10 @@ const {
|
|
|
14
14
|
tintLight: TINT_LIGHT,
|
|
15
15
|
groundSpan: GROUND_SPAN,
|
|
16
16
|
busyEdge: BUSY_EDGE,
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
inkBusyFrom: INK_BUSY_FROM,
|
|
18
|
+
inkBusyTo: INK_BUSY_TO,
|
|
19
|
+
inkPoleTo: INK_POLE_TO,
|
|
20
|
+
presenceDemand: PRESENCE_DEMAND,
|
|
19
21
|
maxDemand: MAX_DEMAND,
|
|
20
22
|
} = BODY;
|
|
21
23
|
|
|
@@ -35,8 +37,22 @@ export type GlassBody = {
|
|
|
35
37
|
density: number;
|
|
36
38
|
/** Lightness the body tints toward. */
|
|
37
39
|
tintLuma: number;
|
|
40
|
+
/** The part of the density the ink's legibility asked for, and the pole it asked toward. */
|
|
41
|
+
need: number;
|
|
42
|
+
pole: number;
|
|
38
43
|
};
|
|
39
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The body is lighter than its surroundings whatever the glyph on it is: glass concentrates light
|
|
47
|
+
* (M 2:29), and the reference keeps a lighter body under a white glyph over sky as much as over
|
|
48
|
+
* dark. It goes to the ink's own pole only where the ink's legibility is what holds the density —
|
|
49
|
+
* or a system setting has raised it past anything a presence floor asks for.
|
|
50
|
+
*/
|
|
51
|
+
function tintFor(density: number, need: number, pole: number): number {
|
|
52
|
+
const asked = Math.max(clamp(need / Math.max(density, 1e-4), 0, 1), smoothstep(PRESENCE_DEMAND, INK_POLE_TO, density));
|
|
53
|
+
return mix(TINT_LIGHT, pole, asked);
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
/**
|
|
41
57
|
* `ink` is POLARITY, 1 light and 0 dark, matching the shader's uniform. The requirements are
|
|
42
58
|
* evaluated at both ends and blended by it — computing at the current polarity alone makes the
|
|
@@ -54,11 +70,7 @@ export function resolveBody(optics: VireGlassOptics, sample: BodyBackdrop, ink:
|
|
|
54
70
|
const mean = clamp(sample.luma, 0, 1);
|
|
55
71
|
const busy = Math.max(sample.busy, 0);
|
|
56
72
|
|
|
57
|
-
|
|
58
|
-
// gradient a local threshold cuts a diagonal step across the body.
|
|
59
|
-
const darkSide = smoothstep(DARK_SIDE_FROM, DARK_SIDE_TO, mean);
|
|
60
|
-
const away = mix(TINT_LIGHT, TINT_DARK, darkSide);
|
|
61
|
-
const tintLuma = mix(away, mix(TINT_LIGHT, TINT_DARK, pol), demand);
|
|
73
|
+
const pole = mix(TINT_LIGHT, TINT_DARK, pol);
|
|
62
74
|
|
|
63
75
|
// How much medium it takes to push the lightness under the ink past its threshold, measured
|
|
64
76
|
// from whichever edge of the backdrop's spread is closer to the ink — a light ink drowns in a
|
|
@@ -68,46 +80,88 @@ export function resolveBody(optics: VireGlassOptics, sample: BodyBackdrop, ink:
|
|
|
68
80
|
const inkLo = Math.max(mean - edge, 0);
|
|
69
81
|
const needForLight = inkHi > capLight ? clamp((inkHi - capLight) / Math.max(inkHi - TINT_DARK, 1e-4), 0, MAX_DEMAND) : 0;
|
|
70
82
|
const needForDark = inkLo < floorDark ? clamp((floorDark - inkLo) / Math.max(TINT_LIGHT - inkLo, 1e-4), 0, MAX_DEMAND) : 0;
|
|
71
|
-
|
|
83
|
+
// Over a flat backdrop the ink asks for nothing: polarity flips before it drowns. It is spread
|
|
84
|
+
// under the ink that the body has to answer.
|
|
85
|
+
const structured = smoothstep(INK_BUSY_FROM, INK_BUSY_TO, busy);
|
|
86
|
+
const needForInk = mix(needForDark, needForLight, pol) * demand * structured;
|
|
72
87
|
|
|
73
88
|
// Fine texture the body has to stop being a window for, the same way roughness does it.
|
|
74
89
|
const ground = busy * optics.legibility * GROUND_SPAN;
|
|
75
90
|
|
|
76
|
-
|
|
91
|
+
const density = Math.max(Math.max(optics.bodyDensity, ground), needForInk);
|
|
92
|
+
return { density, tintLuma: tintFor(density, needForInk, pole), need: needForInk, pole };
|
|
77
93
|
}
|
|
78
94
|
|
|
79
95
|
/**
|
|
80
96
|
* PRESENCE (§3): over a uniform backdrop there is nothing to refract and the glass honestly
|
|
81
|
-
* disappears — right for a piece of background, wrong for a control. It is a MINIMUM
|
|
82
|
-
*
|
|
97
|
+
* disappears — right for a piece of background, wrong for a control. It is a MINIMUM on the
|
|
98
|
+
* density, toward light whatever the ink, and it does not read the backdrop: the lift it gives is
|
|
99
|
+
* `presence` times the room left under the light pole, so it fades out as the room does and has no
|
|
100
|
+
* step anywhere on the scale.
|
|
101
|
+
*/
|
|
102
|
+
export function withPresence(body: GlassBody, optics: VireGlassOptics, _sample?: BodyBackdrop): GlassBody {
|
|
103
|
+
if (optics.presence <= 0) return body;
|
|
104
|
+
const density = Math.max(body.density, Math.min(optics.presence / TINT_LIGHT, PRESENCE_DEMAND));
|
|
105
|
+
return { ...body, density, tintLuma: tintFor(density, body.need, body.pole) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type BodyLayer = {
|
|
109
|
+
/** Straight (not premultiplied) colour, 0…1 per channel. */
|
|
110
|
+
r: number;
|
|
111
|
+
g: number;
|
|
112
|
+
b: number;
|
|
113
|
+
alpha: number;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** The lens's `vgHue`: colour without lightness, and white below the lightness it has none at. */
|
|
117
|
+
function hueOf(c: readonly [number, number, number]): [number, number, number] {
|
|
118
|
+
const l = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
|
|
119
|
+
if (l < 0.02) return [1, 1, 1];
|
|
120
|
+
return [clamp(c[0] / l, 0, 2), clamp(c[1] / l, 0, 2), clamp(c[2] / l, 0, 2)];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The body as ONE translucent layer over the backdrop, which is the form the DOM can draw it in.
|
|
83
125
|
*
|
|
84
|
-
* The
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
126
|
+
* The lens does not paint a body, it transforms what it refracts, in a chain that stays affine in
|
|
127
|
+
* the backdrop: a mix toward the tint at `density`, the matte lift, the medium's pull toward its
|
|
128
|
+
* own lightness, the ambient spill and the concentration of light (§3, `MEDIUM`). Composing it
|
|
129
|
+
* gives `out = A * backdrop + B` per channel, and a layer of colour `B / (1 - A)` at opacity
|
|
130
|
+
* `1 - A` over the backdrop is that, except where the colour clamps to 0..1: over a saturated dark
|
|
131
|
+
* backdrop (dark blue) a channel is off by up to 0.024.
|
|
90
132
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
133
|
+
* This is what lifts the body over a dark backdrop. The tint alone, under light ink, is darker
|
|
134
|
+
* than the dark it sits on; the medium's pull, the matte lift and the concentration are what make
|
|
135
|
+
* it read lighter, as in the reference (M 2:29, A 2:17).
|
|
136
|
+
*
|
|
137
|
+
* `appear` multiplies the layers exactly where the lens's `u_appear` does, so `optics` is the
|
|
138
|
+
* element's optics BEFORE `applyAppear`: scaling both would count it twice.
|
|
96
139
|
*/
|
|
97
|
-
export function
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
140
|
+
export function bodyLayer(
|
|
141
|
+
optics: VireGlassOptics,
|
|
142
|
+
sample: BodyBackdrop,
|
|
143
|
+
ink: number,
|
|
144
|
+
ambient: readonly [number, number, number],
|
|
145
|
+
appear = 1,
|
|
146
|
+
): BodyLayer {
|
|
147
|
+
const a = clamp(appear, 0, 1);
|
|
148
|
+
const body = withPresence(resolveBody(optics, sample, ink), optics, sample);
|
|
149
|
+
const hue = hueOf(ambient);
|
|
150
|
+
const pickup = clamp(optics.colorPickup, 0, 1);
|
|
151
|
+
const density = clamp(body.density * a, 0, 1);
|
|
152
|
+
const matte = optics.legibility * MEDIUM.matteLift * a;
|
|
153
|
+
const pull = MEDIUM.pull * a;
|
|
154
|
+
const spill = optics.edgeLight * MEDIUM.ambientSpill * a;
|
|
155
|
+
const lift = MEDIUM.concentrate * a;
|
|
156
|
+
|
|
157
|
+
const alpha = 1 - (1 - lift) * (1 - pull) * (1 - density);
|
|
158
|
+
const channel = (i: 0 | 1 | 2): number => {
|
|
159
|
+
const tintHue = mix(1, hue[i], pickup);
|
|
160
|
+
const added =
|
|
161
|
+
(1 - pull) * (density * tintHue * body.tintLuma + matte * tintHue) +
|
|
162
|
+
pull * hue[i] * MEDIUM.luma +
|
|
163
|
+
ambient[i] * spill;
|
|
164
|
+
return alpha > 1e-6 ? clamp(((1 - lift) * added + lift) / alpha, 0, 1) : 0;
|
|
112
165
|
};
|
|
166
|
+
return { r: channel(0), g: channel(1), b: channel(2), alpha };
|
|
113
167
|
}
|
package/src/dom/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { resolveOptics, VIREGLASS_MATERIAL, type VireGlassMaterial, type VireGla
|
|
|
20
20
|
import { probeBackdrop } from './probe';
|
|
21
21
|
import { supportsSvgBackdropFilter } from './support';
|
|
22
22
|
import { boxShadowCss } from './shadow';
|
|
23
|
-
import { resolveBody, withPresence } from './body';
|
|
23
|
+
import { bodyLayer, resolveBody, withPresence } from './body';
|
|
24
24
|
import { systemAccessibility, watchAccessibility } from './preferences';
|
|
25
25
|
import { applyAccessibility, contrastRimLuma, elasticAllowed, type VireGlassAccessibility } from '../accessibility';
|
|
26
26
|
import { applyGlassScale, GLASS_SCALE_DEFAULT } from '../glass-scale';
|
|
@@ -30,6 +30,7 @@ import type { VireGlassAccent } from '../adapters';
|
|
|
30
30
|
import { concentricRadius } from '../concentric';
|
|
31
31
|
import { RIM_WIDTH_PX, contrastRimCss, rimGradientCss } from './rim';
|
|
32
32
|
import { GLASS_ATTR, warnIfNested } from './nesting';
|
|
33
|
+
import { holdProperty } from './style-hold';
|
|
33
34
|
import { REST_LIGHT } from '../adapters';
|
|
34
35
|
import { refractionStrength } from '../optics';
|
|
35
36
|
import { DISPERSION, TOUCH } from '../law';
|
|
@@ -289,7 +290,7 @@ function rgbCss(r: number, g: number, b: number): string {
|
|
|
289
290
|
|
|
290
291
|
/**
|
|
291
292
|
* The body, from the model's own law (`./body`, ported from the lens shader §3): density is what
|
|
292
|
-
* legibility and presence demand over THIS backdrop,
|
|
293
|
+
* legibility and presence demand over THIS backdrop, the tint is the lightness it tints toward,
|
|
293
294
|
* and the hue is the surroundings' — glass has no colour of its own, only the colour of what lies
|
|
294
295
|
* beneath it. A flat fill in place of a tint is what breaks the material.
|
|
295
296
|
*/
|
|
@@ -297,22 +298,22 @@ function bodyCss(
|
|
|
297
298
|
optics: VireGlassOptics,
|
|
298
299
|
sample: BackdropSample,
|
|
299
300
|
ink: number,
|
|
300
|
-
accent
|
|
301
|
+
accent: VireGlassAccent | undefined,
|
|
302
|
+
appear: number,
|
|
301
303
|
): string {
|
|
302
|
-
const body = withPresence(resolveBody(optics, sample, ink), optics, sample);
|
|
303
|
-
|
|
304
304
|
// A tinted element takes its colour from the accent rather than from its surroundings, but it is
|
|
305
305
|
// still the MEDIUM that is coloured: the amount stays short of one so the content keeps coming
|
|
306
306
|
// through. A fill at full opacity is the thing §7 says breaks the material.
|
|
307
307
|
if (accent) {
|
|
308
|
+
const settled = applyAppear(optics, appear);
|
|
309
|
+
const body = withPresence(resolveBody(settled, sample, ink), settled, sample);
|
|
308
310
|
const [r, g, b] = accentTone(accent.color, sample.luma);
|
|
309
311
|
const amount = Math.max(accentAmount(accent), body.density);
|
|
310
312
|
return `rgba(${to255(r)}, ${to255(g)}, ${to255(b)}, ${amount.toFixed(3)})`;
|
|
311
313
|
}
|
|
312
314
|
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
return `rgba(${to255(hue(r))}, ${to255(hue(g))}, ${to255(hue(b))}, ${body.density.toFixed(3)})`;
|
|
315
|
+
const layer = bodyLayer(optics, sample, ink, ambientFrom(sample), appear);
|
|
316
|
+
return `rgba(${to255(layer.r)}, ${to255(layer.g)}, ${to255(layer.b)}, ${layer.alpha.toFixed(3)})`;
|
|
316
317
|
}
|
|
317
318
|
|
|
318
319
|
export type AttachGlassOptions = {
|
|
@@ -354,9 +355,19 @@ export type AttachGlassOptions = {
|
|
|
354
355
|
onGlow?: (glow: { pageX: number; pageY: number; strength: number } | null) => void;
|
|
355
356
|
/** Pointer response (§5). On by default. */
|
|
356
357
|
interactive?: boolean;
|
|
357
|
-
/**
|
|
358
|
-
*
|
|
358
|
+
/**
|
|
359
|
+
* Overrides the probe entirely. The probe reads declared backgrounds, so images, video and
|
|
360
|
+
* canvases are invisible to it; a host that knows its backdrop (a cover's accent, a gradient)
|
|
361
|
+
* states it here, and changes it later with `setSample`. `statsFromSamples` builds one from
|
|
362
|
+
* a handful of colours.
|
|
363
|
+
*/
|
|
359
364
|
sample?: BackdropSample;
|
|
365
|
+
/**
|
|
366
|
+
* Whether `attachGlass` paints the body as the element's `background-color`. On by default;
|
|
367
|
+
* turn it off if the host owns that property — `--vireglass-body-color` is always emitted
|
|
368
|
+
* either way and carries the same colour.
|
|
369
|
+
*/
|
|
370
|
+
body?: boolean;
|
|
360
371
|
};
|
|
361
372
|
|
|
362
373
|
/**
|
|
@@ -395,6 +406,11 @@ export type GlassHandle = {
|
|
|
395
406
|
/** A glow that started on a neighbour, in page coordinates (§5). Null clears it. */
|
|
396
407
|
setNeighbourGlow(glow: { pageX: number; pageY: number; strength: number } | null): void;
|
|
397
408
|
setMorph(morph: GlassMorph | null): void;
|
|
409
|
+
/**
|
|
410
|
+
* Replaces the backdrop the glass adapts to, without re-attaching, and repaints. The same
|
|
411
|
+
* object as `sample` in the options; `null` goes back to probing declared backgrounds.
|
|
412
|
+
*/
|
|
413
|
+
setSample(sample: BackdropSample | null): void;
|
|
398
414
|
destroy(): void;
|
|
399
415
|
};
|
|
400
416
|
|
|
@@ -418,6 +434,7 @@ export function attachGlass(el: HTMLElement, opts: AttachGlassOptions = {}): Gla
|
|
|
418
434
|
const onlyWhenTouched = opts.variant === 'interactive';
|
|
419
435
|
let appear = onlyWhenTouched ? 0 : (opts.appear ?? 1);
|
|
420
436
|
let accent = opts.accent;
|
|
437
|
+
let supplied = opts.sample;
|
|
421
438
|
// Read once at attach, then followed: a setting turned on while the page is open has to reach
|
|
422
439
|
// the material, not wait for a reload.
|
|
423
440
|
let a11y = systemAccessibility();
|
|
@@ -436,6 +453,7 @@ export function attachGlass(el: HTMLElement, opts: AttachGlassOptions = {}): Gla
|
|
|
436
453
|
el.setAttribute(GLASS_ATTR, '');
|
|
437
454
|
// `inset: 0` on the rim needs a positioned ancestor. Only taken over when the host left it at
|
|
438
455
|
// the initial value, and put back on destroy.
|
|
456
|
+
const restoreBackground = holdProperty(el.style, 'background-color');
|
|
439
457
|
const positionWasStatic = getComputedStyle(el).position === 'static';
|
|
440
458
|
if (positionWasStatic) el.style.setProperty('position', 'relative');
|
|
441
459
|
|
|
@@ -445,13 +463,16 @@ export function attachGlass(el: HTMLElement, opts: AttachGlassOptions = {}): Gla
|
|
|
445
463
|
* edge of the scale under increased contrast, because the user needs contrast more than they
|
|
446
464
|
* need the aesthetic.
|
|
447
465
|
*/
|
|
448
|
-
function
|
|
466
|
+
function settledOptics(patch?: Partial<VireGlassMaterial>): VireGlassOptics {
|
|
449
467
|
const base = resolveOptics(patch ?? opts.material);
|
|
450
468
|
const scaled = applyGlassScale(base, opts.scale ?? GLASS_SCALE_DEFAULT);
|
|
451
|
-
|
|
469
|
+
return applyAccessibility(scaled, opts.accessibility ?? a11y);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function optics(patch?: Partial<VireGlassMaterial>): VireGlassOptics {
|
|
452
473
|
// Appearance last, and by modulating the lens rather than by fading the element: §1 is
|
|
453
474
|
// explicit that opacity is the wrong instrument for this.
|
|
454
|
-
return applyAppear(
|
|
475
|
+
return applyAppear(settledOptics(patch), appear);
|
|
455
476
|
}
|
|
456
477
|
|
|
457
478
|
function morphState() {
|
|
@@ -470,7 +491,10 @@ export function attachGlass(el: HTMLElement, opts: AttachGlassOptions = {}): Gla
|
|
|
470
491
|
if (destroyed) return;
|
|
471
492
|
const geometry = opts.geometry ?? measureGeometry(el);
|
|
472
493
|
const opticsNow = optics();
|
|
473
|
-
const sample =
|
|
494
|
+
const sample = supplied ?? probeBackdrop(el);
|
|
495
|
+
const light = shouldInkBeLight(sample, wasLight);
|
|
496
|
+
wasLight = light;
|
|
497
|
+
const body = opts.body === false ? null : bodyCss(settledOptics(), sample, light ? 1 : 0, accent, appear);
|
|
474
498
|
|
|
475
499
|
if (svgSupported) {
|
|
476
500
|
// The map depends only on geometry, optics and density — never on the backdrop. Rebuilding
|
|
@@ -495,11 +519,9 @@ export function attachGlass(el: HTMLElement, opts: AttachGlassOptions = {}): Gla
|
|
|
495
519
|
} else {
|
|
496
520
|
el.style.setProperty('backdrop-filter', fallbackFilterCss(opticsNow));
|
|
497
521
|
el.style.setProperty('-webkit-backdrop-filter', fallbackFilterCss(opticsNow));
|
|
498
|
-
el.style.backgroundColor = bodyCss(opticsNow, sample, wasLight ? 1 : 0, accent);
|
|
499
522
|
}
|
|
523
|
+
if (body) el.style.backgroundColor = body;
|
|
500
524
|
|
|
501
|
-
const light = shouldInkBeLight(sample, wasLight);
|
|
502
|
-
wasLight = light;
|
|
503
525
|
const [tr, tg, tb] = ambientFrom(sample);
|
|
504
526
|
el.style.setProperty('--vireglass-ink', light ? '1' : '0');
|
|
505
527
|
el.style.setProperty('--vireglass-tint', `${tr} ${tg} ${tb}`);
|
|
@@ -511,7 +533,10 @@ export function attachGlass(el: HTMLElement, opts: AttachGlassOptions = {}): Gla
|
|
|
511
533
|
const inkLevel = Math.round((light ? INK_LIGHT : INK_DARK) * 255);
|
|
512
534
|
el.style.setProperty('--vireglass-ink-color', `rgb(${inkLevel} ${inkLevel} ${inkLevel})`);
|
|
513
535
|
el.style.setProperty('--vireglass-tint-color', rgbCss(tr, tg, tb));
|
|
514
|
-
el.style.setProperty(
|
|
536
|
+
el.style.setProperty(
|
|
537
|
+
'--vireglass-body-color',
|
|
538
|
+
body ?? bodyCss(settledOptics(), sample, light ? 1 : 0, accent, appear),
|
|
539
|
+
);
|
|
515
540
|
|
|
516
541
|
// Rim and shadow follow the SAMPLE, not just the geometry, so they sit outside the map's
|
|
517
542
|
// cache key: the environment they take their colour and density from moves under a scroll
|
|
@@ -616,7 +641,7 @@ export function attachGlass(el: HTMLElement, opts: AttachGlassOptions = {}): Gla
|
|
|
616
641
|
el.style.setProperty('--vireglass-glow-opacity', '0');
|
|
617
642
|
return;
|
|
618
643
|
}
|
|
619
|
-
const [gr, gg, gb] = ambientFrom(
|
|
644
|
+
const [gr, gg, gb] = ambientFrom(supplied ?? probeBackdrop(el));
|
|
620
645
|
const cx = ((x + geometry.width / 2) / geometry.width) * 100;
|
|
621
646
|
const cy = ((y + geometry.height / 2) / geometry.height) * 100;
|
|
622
647
|
const reach = halfMinDp(geometry) * TOUCH_RADIUS_FRACTION;
|
|
@@ -725,7 +750,7 @@ export function attachGlass(el: HTMLElement, opts: AttachGlassOptions = {}): Gla
|
|
|
725
750
|
mapKey = '';
|
|
726
751
|
el.style.removeProperty('backdrop-filter');
|
|
727
752
|
el.style.removeProperty('-webkit-backdrop-filter');
|
|
728
|
-
|
|
753
|
+
if (opts.body !== false) restoreBackground();
|
|
729
754
|
el.style.removeProperty('--vireglass-ink');
|
|
730
755
|
el.style.removeProperty('--vireglass-tint');
|
|
731
756
|
el.style.removeProperty('--vireglass-body-density');
|
|
@@ -774,6 +799,10 @@ export function attachGlass(el: HTMLElement, opts: AttachGlassOptions = {}): Gla
|
|
|
774
799
|
mapKey = '';
|
|
775
800
|
apply();
|
|
776
801
|
},
|
|
802
|
+
setSample(next) {
|
|
803
|
+
supplied = next ?? undefined;
|
|
804
|
+
apply();
|
|
805
|
+
},
|
|
777
806
|
destroy,
|
|
778
807
|
};
|
|
779
808
|
attached.set(el, handle);
|
package/src/dom/probe.ts
CHANGED
|
@@ -40,7 +40,7 @@ function luma(r: number, g: number, b: number): number {
|
|
|
40
40
|
* formula here would mean the ink-polarity decision quietly drifts between renderers on the same
|
|
41
41
|
* page. `samples` are 0..255 per channel, matching what `parseColor` returns.
|
|
42
42
|
*/
|
|
43
|
-
export function statsFromSamples(samples: readonly RgbColor[]): BackdropSample {
|
|
43
|
+
export function statsFromSamples(samples: readonly (Omit<RgbColor, 'a'> & { a?: number })[]): BackdropSample {
|
|
44
44
|
const n = samples.length;
|
|
45
45
|
if (n === 0) return { luma: 0.5, busy: 0, lo: 0.5, hi: 0.5, r: 0.5, g: 0.5, b: 0.5 };
|
|
46
46
|
|
package/src/dom/rim.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
// Rim light (docs/reference.md §2): a hairline along the silhouette — "not a band and not a
|
|
2
|
-
// bevel" —
|
|
3
|
-
// taken from the environment.
|
|
1
|
+
// Rim light (docs/reference.md §2): a hairline along the whole silhouette — "not a band and not a
|
|
2
|
+
// bevel" — brighter toward the key light, with a second, weaker arc opposite it, outlined by a thin
|
|
3
|
+
// dark line, its colour taken from the environment and its strength from how light that is.
|
|
4
4
|
//
|
|
5
|
-
// Every number here is the lens shader's own. The
|
|
6
|
-
//
|
|
7
|
-
// darkening at the silhouette). The only thing that differs is the primitive: a conic gradient
|
|
8
|
-
// sampled at fixed angles instead of a per-pixel normal.
|
|
5
|
+
// Every number here is the lens shader's own, read from the law. The only thing that differs is
|
|
6
|
+
// the primitive: a conic gradient sampled at fixed angles instead of a per-pixel normal.
|
|
9
7
|
import { RIM } from '../law';
|
|
10
8
|
import type { VireGlassOptics } from '../material';
|
|
11
9
|
|
|
12
|
-
const {
|
|
10
|
+
const {
|
|
11
|
+
lobeExponent: LOBE_EXPONENT,
|
|
12
|
+
opposingArc: OPPOSING_ARC,
|
|
13
|
+
darkEdge: DARK_EDGE,
|
|
14
|
+
contour: CONTOUR,
|
|
15
|
+
whiteShare: WHITE_SHARE,
|
|
16
|
+
reflectanceRef: REFLECTANCE_REF,
|
|
17
|
+
} = RIM;
|
|
13
18
|
|
|
14
19
|
/** One stop every 7.5°. Fine enough that a cubic lobe reads as a smooth arc rather than a fan. */
|
|
15
20
|
const STOPS = 48;
|
|
@@ -39,13 +44,45 @@ const to255 = (v: number) => Math.round(clamp01(v) * 255);
|
|
|
39
44
|
|
|
40
45
|
export type RimAmbient = readonly [number, number, number];
|
|
41
46
|
|
|
47
|
+
const mixTo = (a: number, b: number, t: number) => a + (b - a) * t;
|
|
48
|
+
|
|
49
|
+
function smoothstep(edge0: number, edge1: number, x: number): number {
|
|
50
|
+
const t = clamp01((x - edge0) / (edge1 - edge0));
|
|
51
|
+
return t * t * (3 - 2 * t);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Lightness of an ambient colour, the weights the lens uses. */
|
|
55
|
+
export function ambientLuma(ambient: RimAmbient): number {
|
|
56
|
+
return 0.2126 * ambient[0] + 0.7152 * ambient[1] + 0.0722 * ambient[2];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** How strongly the line is drawn over a surround of this lightness (§2, §6). */
|
|
60
|
+
export function rimStrength(luma: number): number {
|
|
61
|
+
return mixTo(RIM.strengthDark, RIM.strengthLight, smoothstep(RIM.ambientFrom, RIM.ambientTo, luma));
|
|
62
|
+
}
|
|
63
|
+
|
|
42
64
|
/**
|
|
43
|
-
* The
|
|
44
|
-
*
|
|
45
|
-
*
|
|
65
|
+
* The line's opacity where the rim's normal sits `deltaDeg` from the light: a floor all the way
|
|
66
|
+
* round and the key-light lobe on top of it, all of it scaled by the medium's reflectance up to
|
|
67
|
+
* the reference material's.
|
|
68
|
+
*/
|
|
69
|
+
export function rimLineAlpha(deltaDeg: number, optics: VireGlassOptics, luma: number): number {
|
|
70
|
+
const reflect = Math.min(optics.fresnel / REFLECTANCE_REF, 1);
|
|
71
|
+
const damping = optics.fresnel > 0 ? optics.specular / optics.fresnel : 0;
|
|
72
|
+
const lobe = rimLobe(deltaDeg, damping);
|
|
73
|
+
return clamp01(rimStrength(luma) * reflect * (CONTOUR + (1 - CONTOUR) * lobe));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The gradient for the hairline: a light line round the whole contour, brighter toward the key
|
|
78
|
+
* light, drawn over the dark edge. Its colour comes from the environment — "over a yellow flower
|
|
79
|
+
* the rim is yellow, over sky it is blue" (§2) — lifted toward white, and its strength follows how
|
|
80
|
+
* light the environment is.
|
|
46
81
|
*
|
|
47
82
|
* The opposing arc carries an iridescent fringe (§2), so `iridescence` pulls its channels apart
|
|
48
83
|
* rather than lifting them together.
|
|
84
|
+
*
|
|
85
|
+
* Two layers, the line first: `--vireglass-rim` is a background list and paints as one.
|
|
49
86
|
*/
|
|
50
87
|
export function rimGradientCss(
|
|
51
88
|
optics: VireGlassOptics,
|
|
@@ -56,31 +93,29 @@ export function rimGradientCss(
|
|
|
56
93
|
const present = clamp01(appear);
|
|
57
94
|
const start = lightConicAngle(light);
|
|
58
95
|
const [ar, ag, ab] = ambient;
|
|
59
|
-
const
|
|
96
|
+
const luma = ambientLuma(ambient);
|
|
97
|
+
const line: string[] = [];
|
|
60
98
|
|
|
61
99
|
for (let i = 0; i <= STOPS; i += 1) {
|
|
62
100
|
const angle = (i / STOPS) * 360;
|
|
63
|
-
const key = rimLobe(angle, optics.specular);
|
|
64
101
|
const facing = Math.cos((angle * Math.PI) / 180);
|
|
65
102
|
// The far arc is where dispersion shows as a fringe; the near one is a clean specular.
|
|
66
103
|
const fringe = facing < 0 ? optics.iridescence * OPPOSING_ARC : 0;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
// Scaled, not floored: the dark edge is the one part of the rim that survives facing away from
|
|
74
|
-
// the light, so leaving it unscaled leaves a hairline drawn around an element that is not there.
|
|
75
|
-
const alpha = clamp01(DARK_EDGE + lift * (1 - DARK_EDGE)) * present;
|
|
76
|
-
// Below the arcs the line is the dark edge itself, not a dim copy of the environment.
|
|
77
|
-
const colour = lift > 0.001
|
|
78
|
-
? `rgba(${to255(r)},${to255(g)},${to255(b)},${alpha.toFixed(3)})`
|
|
79
|
-
: `rgba(0,0,0,${alpha.toFixed(3)})`;
|
|
80
|
-
stops.push(`${colour} ${angle.toFixed(1)}deg`);
|
|
104
|
+
|
|
105
|
+
const r = ar + (1 - ar) * clamp01(WHITE_SHARE + fringe);
|
|
106
|
+
const g = ag + (1 - ag) * WHITE_SHARE;
|
|
107
|
+
const b = ab + (1 - ab) * clamp01(WHITE_SHARE - fringe);
|
|
108
|
+
const alpha = rimLineAlpha(angle, optics, luma) * present;
|
|
109
|
+
line.push(`rgba(${to255(r)},${to255(g)},${to255(b)},${alpha.toFixed(3)}) ${angle.toFixed(1)}deg`);
|
|
81
110
|
}
|
|
82
111
|
|
|
83
|
-
|
|
112
|
+
// Scaled by presence, not floored: the dark edge would otherwise outline an element that is not
|
|
113
|
+
// there yet.
|
|
114
|
+
const dark = `rgba(0,0,0,${(DARK_EDGE * present).toFixed(3)})`;
|
|
115
|
+
return (
|
|
116
|
+
`conic-gradient(from ${start.toFixed(1)}deg, ${line.join(', ')}), ` +
|
|
117
|
+
`conic-gradient(${dark} 0deg, ${dark} 360deg)`
|
|
118
|
+
);
|
|
84
119
|
}
|
|
85
120
|
|
|
86
121
|
export const RIM_WIDTH_PX = RIM.widthPx;
|