@wardic/dom 0.0.0-stage → 1.0.0-rc.2
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/LICENSE +21 -0
- package/README.md +8 -2
- package/THIRD_PARTY_NOTICES.md +145 -0
- package/dist/Appear.d.ts +24 -0
- package/dist/Appear.d.ts.map +1 -0
- package/dist/Appear.js +25 -0
- package/dist/Appear.js.map +1 -0
- package/dist/Color.d.ts +14 -0
- package/dist/Color.d.ts.map +1 -0
- package/dist/Color.js +16 -0
- package/dist/Color.js.map +1 -0
- package/dist/DomAnnouncer.d.ts +13 -0
- package/dist/DomAnnouncer.d.ts.map +1 -0
- package/dist/DomAnnouncer.js +14 -0
- package/dist/DomAnnouncer.js.map +1 -0
- package/dist/DomClock.d.ts +11 -0
- package/dist/DomClock.d.ts.map +1 -0
- package/dist/DomClock.js +12 -0
- package/dist/DomClock.js.map +1 -0
- package/dist/DomDefaultTransition.d.ts +12 -0
- package/dist/DomDefaultTransition.d.ts.map +1 -0
- package/dist/DomDefaultTransition.js +13 -0
- package/dist/DomDefaultTransition.js.map +1 -0
- package/dist/DomInput.d.ts +13 -0
- package/dist/DomInput.d.ts.map +1 -0
- package/dist/DomInput.js +15 -0
- package/dist/DomInput.js.map +1 -0
- package/dist/DomInterpolators.d.ts +13 -0
- package/dist/DomInterpolators.d.ts.map +1 -0
- package/dist/DomInterpolators.js +14 -0
- package/dist/DomInterpolators.js.map +1 -0
- package/dist/DomKeyRules.d.ts +13 -0
- package/dist/DomKeyRules.d.ts.map +1 -0
- package/dist/DomKeyRules.js +14 -0
- package/dist/DomKeyRules.js.map +1 -0
- package/dist/DomPositionalKeys.d.ts +13 -0
- package/dist/DomPositionalKeys.d.ts.map +1 -0
- package/dist/DomPositionalKeys.js +14 -0
- package/dist/DomPositionalKeys.js.map +1 -0
- package/dist/DomProbe.d.ts +13 -0
- package/dist/DomProbe.d.ts.map +1 -0
- package/dist/DomProbe.js +14 -0
- package/dist/DomProbe.js.map +1 -0
- package/dist/DomProjectionSystem.d.ts +28 -0
- package/dist/DomProjectionSystem.d.ts.map +1 -0
- package/dist/DomProjectionSystem.js +41 -0
- package/dist/DomProjectionSystem.js.map +1 -0
- package/dist/DomRenderer.d.ts +13 -0
- package/dist/DomRenderer.d.ts.map +1 -0
- package/dist/DomRenderer.js +15 -0
- package/dist/DomRenderer.js.map +1 -0
- package/dist/FlushGate.d.ts +48 -0
- package/dist/FlushGate.d.ts.map +1 -0
- package/dist/FlushGate.js +62 -0
- package/dist/FlushGate.js.map +1 -0
- package/dist/MeasuredPair.d.ts +14 -0
- package/dist/MeasuredPair.d.ts.map +1 -0
- package/dist/MeasuredPair.js +16 -0
- package/dist/MeasuredPair.js.map +1 -0
- package/dist/OwnValues.d.ts +14 -0
- package/dist/OwnValues.d.ts.map +1 -0
- package/dist/OwnValues.js +15 -0
- package/dist/OwnValues.js.map +1 -0
- package/dist/ProjectionSystem.d.ts +34 -0
- package/dist/ProjectionSystem.d.ts.map +1 -0
- package/dist/ProjectionSystem.js +29 -0
- package/dist/ProjectionSystem.js.map +1 -0
- package/dist/ReorderController.d.ts +41 -0
- package/dist/ReorderController.d.ts.map +1 -0
- package/dist/ReorderController.js +44 -0
- package/dist/ReorderController.js.map +1 -0
- package/dist/Report.d.ts +12 -0
- package/dist/Report.d.ts.map +1 -0
- package/dist/Report.js +13 -0
- package/dist/Report.js.map +1 -0
- package/dist/ScrollOffset.d.ts +34 -0
- package/dist/ScrollOffset.d.ts.map +1 -0
- package/dist/ScrollOffset.js +42 -0
- package/dist/ScrollOffset.js.map +1 -0
- package/dist/Style.d.ts +15 -0
- package/dist/Style.d.ts.map +1 -0
- package/dist/Style.js +17 -0
- package/dist/Style.js.map +1 -0
- package/dist/Svg.d.ts +13 -0
- package/dist/Svg.d.ts.map +1 -0
- package/dist/Svg.js +14 -0
- package/dist/Svg.js.map +1 -0
- package/dist/Variables.d.ts +13 -0
- package/dist/Variables.d.ts.map +1 -0
- package/dist/Variables.js +14 -0
- package/dist/Variables.js.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/announcer.d.ts +50 -0
- package/dist/internal/announcer.d.ts.map +1 -0
- package/dist/internal/announcer.js +115 -0
- package/dist/internal/announcer.js.map +1 -0
- package/dist/internal/appear.d.ts +169 -0
- package/dist/internal/appear.d.ts.map +1 -0
- package/dist/internal/appear.js +499 -0
- package/dist/internal/appear.js.map +1 -0
- package/dist/internal/auto-scroll.d.ts +24 -0
- package/dist/internal/auto-scroll.d.ts.map +1 -0
- package/dist/internal/auto-scroll.js +124 -0
- package/dist/internal/auto-scroll.js.map +1 -0
- package/dist/internal/clock.d.ts +19 -0
- package/dist/internal/clock.d.ts.map +1 -0
- package/dist/internal/clock.js +39 -0
- package/dist/internal/clock.js.map +1 -0
- package/dist/internal/colors.d.ts +45 -0
- package/dist/internal/colors.d.ts.map +1 -0
- package/dist/internal/colors.js +193 -0
- package/dist/internal/colors.js.map +1 -0
- package/dist/internal/complex.d.ts +24 -0
- package/dist/internal/complex.d.ts.map +1 -0
- package/dist/internal/complex.js +28 -0
- package/dist/internal/complex.js.map +1 -0
- package/dist/internal/content-watch.d.ts +20 -0
- package/dist/internal/content-watch.d.ts.map +1 -0
- package/dist/internal/content-watch.js +76 -0
- package/dist/internal/content-watch.js.map +1 -0
- package/dist/internal/default-transition.d.ts +25 -0
- package/dist/internal/default-transition.d.ts.map +1 -0
- package/dist/internal/default-transition.js +41 -0
- package/dist/internal/default-transition.js.map +1 -0
- package/dist/internal/drag-defaults.d.ts +18 -0
- package/dist/internal/drag-defaults.d.ts.map +1 -0
- package/dist/internal/drag-defaults.js +38 -0
- package/dist/internal/drag-defaults.js.map +1 -0
- package/dist/internal/gate.d.ts +186 -0
- package/dist/internal/gate.d.ts.map +1 -0
- package/dist/internal/gate.js +453 -0
- package/dist/internal/gate.js.map +1 -0
- package/dist/internal/input.d.ts +41 -0
- package/dist/internal/input.d.ts.map +1 -0
- package/dist/internal/input.js +476 -0
- package/dist/internal/input.js.map +1 -0
- package/dist/internal/interpolators.d.ts +19 -0
- package/dist/internal/interpolators.d.ts.map +1 -0
- package/dist/internal/interpolators.js +30 -0
- package/dist/internal/interpolators.js.map +1 -0
- package/dist/internal/key-rules.d.ts +11 -0
- package/dist/internal/key-rules.d.ts.map +1 -0
- package/dist/internal/key-rules.js +244 -0
- package/dist/internal/key-rules.js.map +1 -0
- package/dist/internal/measure.d.ts +150 -0
- package/dist/internal/measure.d.ts.map +1 -0
- package/dist/internal/measure.js +278 -0
- package/dist/internal/measure.js.map +1 -0
- package/dist/internal/measured-pairs.d.ts +38 -0
- package/dist/internal/measured-pairs.d.ts.map +1 -0
- package/dist/internal/measured-pairs.js +251 -0
- package/dist/internal/measured-pairs.js.map +1 -0
- package/dist/internal/own-values.d.ts +35 -0
- package/dist/internal/own-values.d.ts.map +1 -0
- package/dist/internal/own-values.js +117 -0
- package/dist/internal/own-values.js.map +1 -0
- package/dist/internal/pass-ends.d.ts +21 -0
- package/dist/internal/pass-ends.d.ts.map +1 -0
- package/dist/internal/pass-ends.js +14 -0
- package/dist/internal/pass-ends.js.map +1 -0
- package/dist/internal/positional-keys.d.ts +16 -0
- package/dist/internal/positional-keys.d.ts.map +1 -0
- package/dist/internal/positional-keys.js +21 -0
- package/dist/internal/positional-keys.js.map +1 -0
- package/dist/internal/probe.d.ts +10 -0
- package/dist/internal/probe.d.ts.map +1 -0
- package/dist/internal/probe.js +117 -0
- package/dist/internal/probe.js.map +1 -0
- package/dist/internal/projection-defaults.d.ts +34 -0
- package/dist/internal/projection-defaults.d.ts.map +1 -0
- package/dist/internal/projection-defaults.js +21 -0
- package/dist/internal/projection-defaults.js.map +1 -0
- package/dist/internal/projection.d.ts +529 -0
- package/dist/internal/projection.d.ts.map +1 -0
- package/dist/internal/projection.js +4990 -0
- package/dist/internal/projection.js.map +1 -0
- package/dist/internal/renderer.d.ts +86 -0
- package/dist/internal/renderer.d.ts.map +1 -0
- package/dist/internal/renderer.js +616 -0
- package/dist/internal/renderer.js.map +1 -0
- package/dist/internal/reorder.d.ts +159 -0
- package/dist/internal/reorder.d.ts.map +1 -0
- package/dist/internal/reorder.js +437 -0
- package/dist/internal/reorder.js.map +1 -0
- package/dist/internal/report.d.ts +30 -0
- package/dist/internal/report.d.ts.map +1 -0
- package/dist/internal/report.js +60 -0
- package/dist/internal/report.js.map +1 -0
- package/dist/internal/require-global.d.ts +11 -0
- package/dist/internal/require-global.d.ts.map +1 -0
- package/dist/internal/require-global.js +31 -0
- package/dist/internal/require-global.js.map +1 -0
- package/dist/internal/reveal.d.ts +16 -0
- package/dist/internal/reveal.d.ts.map +1 -0
- package/dist/internal/reveal.js +66 -0
- package/dist/internal/reveal.js.map +1 -0
- package/dist/internal/runner.d.ts +21 -0
- package/dist/internal/runner.d.ts.map +1 -0
- package/dist/internal/runner.js +48 -0
- package/dist/internal/runner.js.map +1 -0
- package/dist/internal/scale-correction.d.ts +80 -0
- package/dist/internal/scale-correction.d.ts.map +1 -0
- package/dist/internal/scale-correction.js +227 -0
- package/dist/internal/scale-correction.js.map +1 -0
- package/dist/internal/scroll.d.ts +35 -0
- package/dist/internal/scroll.d.ts.map +1 -0
- package/dist/internal/scroll.js +244 -0
- package/dist/internal/scroll.js.map +1 -0
- package/dist/internal/scrollers.d.ts +35 -0
- package/dist/internal/scrollers.d.ts.map +1 -0
- package/dist/internal/scrollers.js +38 -0
- package/dist/internal/scrollers.js.map +1 -0
- package/dist/internal/starting-style.d.ts +141 -0
- package/dist/internal/starting-style.d.ts.map +1 -0
- package/dist/internal/starting-style.js +300 -0
- package/dist/internal/starting-style.js.map +1 -0
- package/dist/internal/svg.d.ts +60 -0
- package/dist/internal/svg.d.ts.map +1 -0
- package/dist/internal/svg.js +257 -0
- package/dist/internal/svg.js.map +1 -0
- package/dist/internal/variables.d.ts +45 -0
- package/dist/internal/variables.d.ts.map +1 -0
- package/dist/internal/variables.js +92 -0
- package/dist/internal/variables.js.map +1 -0
- package/package.json +65 -4
- package/src/Appear.ts +45 -0
- package/src/Color.ts +24 -0
- package/src/DomAnnouncer.ts +22 -0
- package/src/DomClock.ts +13 -0
- package/src/DomDefaultTransition.ts +16 -0
- package/src/DomInput.ts +22 -0
- package/src/DomInterpolators.ts +16 -0
- package/src/DomKeyRules.ts +17 -0
- package/src/DomPositionalKeys.ts +16 -0
- package/src/DomProbe.ts +15 -0
- package/src/DomProjectionSystem.ts +44 -0
- package/src/DomRenderer.ts +26 -0
- package/src/FlushGate.ts +81 -0
- package/src/MeasuredPair.ts +15 -0
- package/src/OwnValues.ts +19 -0
- package/src/ProjectionSystem.ts +51 -0
- package/src/ReorderController.ts +72 -0
- package/src/Report.ts +17 -0
- package/src/ScrollOffset.ts +54 -0
- package/src/Style.ts +37 -0
- package/src/Svg.ts +21 -0
- package/src/Variables.ts +17 -0
- package/src/index.ts +106 -0
- package/src/internal/announcer.ts +132 -0
- package/src/internal/appear.ts +739 -0
- package/src/internal/auto-scroll.ts +176 -0
- package/src/internal/clock.ts +45 -0
- package/src/internal/colors.ts +258 -0
- package/src/internal/complex.ts +29 -0
- package/src/internal/content-watch.ts +84 -0
- package/src/internal/culori.d.ts +58 -0
- package/src/internal/default-transition.ts +50 -0
- package/src/internal/drag-defaults.ts +54 -0
- package/src/internal/gate.ts +593 -0
- package/src/internal/input.ts +579 -0
- package/src/internal/interpolators.ts +33 -0
- package/src/internal/key-rules.ts +357 -0
- package/src/internal/measure.ts +357 -0
- package/src/internal/measured-pairs.ts +297 -0
- package/src/internal/own-values.ts +166 -0
- package/src/internal/pass-ends.ts +23 -0
- package/src/internal/positional-keys.ts +24 -0
- package/src/internal/probe.ts +176 -0
- package/src/internal/projection-defaults.ts +45 -0
- package/src/internal/projection.ts +5999 -0
- package/src/internal/renderer.ts +646 -0
- package/src/internal/reorder.ts +604 -0
- package/src/internal/report.ts +69 -0
- package/src/internal/require-global.ts +46 -0
- package/src/internal/reveal.ts +69 -0
- package/src/internal/runner.ts +64 -0
- package/src/internal/scale-correction.ts +310 -0
- package/src/internal/scroll.ts +356 -0
- package/src/internal/scrollers.ts +51 -0
- package/src/internal/starting-style.ts +396 -0
- package/src/internal/svg.ts +300 -0
- package/src/internal/variables.ts +145 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG realization for the DOM renderer (ADR-0014). The core value pipeline +
|
|
3
|
+
* the pure `project` stay AGNOSTIC and flat: SVG numeric attrs (`cx`/`r`/…)
|
|
4
|
+
* already spring as numbers and `fill`/`stroke` already interpolate as colors.
|
|
5
|
+
* This module is the DOM-API ROUTING layer — the one place that decides whether
|
|
6
|
+
* a resolved value is written as a CSS `style` or an `setAttribute`, plus the
|
|
7
|
+
* SVG-specific shaping (`transform-box`, `pathLength`→dash). It is the seam a
|
|
8
|
+
* non-DOM SVG renderer (e.g. react-native-svg) would replace wholesale; nothing
|
|
9
|
+
* here is shared with `@wardic/core`.
|
|
10
|
+
*
|
|
11
|
+
* Model (Motion's `buildSVGAttrs`): for an svg-INNER element every key is an
|
|
12
|
+
* ATTRIBUTE except `transform`/`transformOrigin`/`transformBox` + the CSS
|
|
13
|
+
* motion-path props, which stay STYLE, and a custom property (`--…`), which is style
|
|
14
|
+
* under its own name on every element (#151). For an HTML element or the `<svg>` ROOT
|
|
15
|
+
* everything is style (the root has a CSS box; only `viewBox` would be an attr —
|
|
16
|
+
* deferred). So the HTML path is unchanged: its attribute map is always empty.
|
|
17
|
+
*/
|
|
18
|
+
import type * as Renderer from "@wardic/core/Renderer"
|
|
19
|
+
import * as Bool from "effect/Boolean"
|
|
20
|
+
import * as Option from "effect/Option"
|
|
21
|
+
import * as Predicate from "effect/Predicate"
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Keys that remain CSS `style` on an svg-inner element; everything else routes
|
|
25
|
+
* to an attribute. `transform` is applied as a CSS transform (with `transform-box`
|
|
26
|
+
* below); the motion-path props are CSS-only on SVG too (Motion parity).
|
|
27
|
+
*/
|
|
28
|
+
const SVG_STYLE_KEYS: ReadonlySet<string> = new Set([
|
|
29
|
+
"transform",
|
|
30
|
+
"transformOrigin",
|
|
31
|
+
"transform-origin",
|
|
32
|
+
"transformBox",
|
|
33
|
+
"transform-box",
|
|
34
|
+
"offsetDistance",
|
|
35
|
+
"offsetPath",
|
|
36
|
+
"offsetRotate",
|
|
37
|
+
"offsetAnchor",
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* SVG attribute names always written camelCase (never dash-cased) — Motion's
|
|
42
|
+
* `camelCaseAttributes`. Everything else is dash-cased (`strokeWidth` →
|
|
43
|
+
* `stroke-width`).
|
|
44
|
+
*/
|
|
45
|
+
const CAMEL_CASE_ATTRS: ReadonlySet<string> = new Set([
|
|
46
|
+
"baseFrequency",
|
|
47
|
+
"diffuseConstant",
|
|
48
|
+
"kernelMatrix",
|
|
49
|
+
"kernelUnitLength",
|
|
50
|
+
"keySplines",
|
|
51
|
+
"keyTimes",
|
|
52
|
+
"limitingConeAngle",
|
|
53
|
+
"markerHeight",
|
|
54
|
+
"markerWidth",
|
|
55
|
+
"numOctaves",
|
|
56
|
+
"targetX",
|
|
57
|
+
"targetY",
|
|
58
|
+
"surfaceScale",
|
|
59
|
+
"specularConstant",
|
|
60
|
+
"specularExponent",
|
|
61
|
+
"stdDeviation",
|
|
62
|
+
"tableValues",
|
|
63
|
+
"viewBox",
|
|
64
|
+
"gradientTransform",
|
|
65
|
+
"pathLength",
|
|
66
|
+
"startOffset",
|
|
67
|
+
"textLength",
|
|
68
|
+
"lengthAdjust",
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Escape hatches: `x`/`y` are transform shorthands in Wardic (translateX/Y), so
|
|
73
|
+
* the SVG geometry ATTRIBUTE form is the explicit `attr*` key (Motion's
|
|
74
|
+
* `attrX`/`attrY`/`attrScale`). Maps to the bare attribute name.
|
|
75
|
+
*/
|
|
76
|
+
const ATTR_ALIASES: Record<string, string> = { attrX: "x", attrY: "y", attrScale: "scale" }
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Each key's dash-cased name, computed once (ADR-0034): the commit dash-cases every key of
|
|
80
|
+
* every patch, each frame. Keys come from animation configs, so the set stays small.
|
|
81
|
+
*/
|
|
82
|
+
// Module state (#460, Q7): a cache keyed by what it caches: a bounded set of names.
|
|
83
|
+
const DASHED = new Map<string, string>()
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether `key` is a CSS custom property (`--brandColor`): it keeps its name, which is
|
|
87
|
+
* case-sensitive, and is always a style property, on SVG too (ADR-0014, *Amendment (#151)*;
|
|
88
|
+
* Motion's `isCSSVariableName`).
|
|
89
|
+
*/
|
|
90
|
+
const isCustomProperty = (key: string): boolean => key.startsWith("--")
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* camelCase → dash-case for a CSS/attribute name (`transformBox` → `transform-box`); a
|
|
94
|
+
* custom property keeps its name (`--brandColor`).
|
|
95
|
+
*/
|
|
96
|
+
export const dashCase = (key: string): string => {
|
|
97
|
+
const cached = DASHED.get(key)
|
|
98
|
+
if (Predicate.isNotUndefined(cached)) return cached
|
|
99
|
+
const dashed = Bool.match(isCustomProperty(key), {
|
|
100
|
+
onTrue: () => key,
|
|
101
|
+
onFalse: () => key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`),
|
|
102
|
+
})
|
|
103
|
+
DASHED.set(key, dashed)
|
|
104
|
+
return dashed
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The attribute name for an engine key: dash-cased unless it's a known camelCase attr. */
|
|
108
|
+
const attrName = (key: string): string => {
|
|
109
|
+
if (CAMEL_CASE_ATTRS.has(key)) return key
|
|
110
|
+
return dashCase(key)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Whether a key collapses into the stroke dash: `pathLength`, `pathSpacing` or `pathOffset`. */
|
|
114
|
+
export const isPathKey = (key: string): boolean => key === "pathLength" || key === "pathSpacing" || key === "pathOffset"
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The attribute an svg-inner element's `key` is written as (the routing below), or
|
|
118
|
+
* `None` for a key that stays style ({@link SVG_STYLE_KEYS}, a custom property) or collapses into the
|
|
119
|
+
* stroke dash (`pathLength` / `pathSpacing` / `pathOffset`). Internal: the Own-value read
|
|
120
|
+
* reads the attribute a key is written as (ADR-0022).
|
|
121
|
+
*/
|
|
122
|
+
export const svgAttrNameOf = (key: string): Option.Option<string> =>
|
|
123
|
+
Bool.match(isPathKey(key) || isCustomProperty(key), {
|
|
124
|
+
onTrue: () => Option.none(),
|
|
125
|
+
onFalse: () =>
|
|
126
|
+
Option.orElse(Option.fromUndefinedOr(ATTR_ALIASES[key]), () =>
|
|
127
|
+
Option.map(Option.liftPredicate(key, (k: string) => Bool.not(SVG_STYLE_KEYS.has(k))), attrName)
|
|
128
|
+
),
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
/** Whether `el` is an `<svg>` root, which is styled as HTML is: it has a CSS box. */
|
|
132
|
+
export const isSvgRoot = (el: unknown): boolean => {
|
|
133
|
+
// No SVG classes on a server. Per frame (a flush's split): plain checks.
|
|
134
|
+
const SvgRoot = (globalThis as { readonly SVGSVGElement?: typeof SVGSVGElement }).SVGSVGElement
|
|
135
|
+
return Predicate.isNotUndefined(SvgRoot) && el instanceof SvgRoot
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Whether `el` is an SVG element inside an `<svg>`: any SVG element but the root. */
|
|
139
|
+
export const isSvgInner = (el: unknown): boolean => {
|
|
140
|
+
const Svg = (globalThis as { readonly SVGElement?: typeof SVGElement }).SVGElement
|
|
141
|
+
return Predicate.isNotUndefined(Svg) && el instanceof Svg && !isSvgRoot(el)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** A style patch, split: the keys written as CSS, and the ones written as attributes. */
|
|
145
|
+
export interface SplitPatch {
|
|
146
|
+
/** The keys written as CSS: the whole patch for HTML and an `<svg>` root, the very object given. */
|
|
147
|
+
readonly style: Readonly<Record<string, unknown>>
|
|
148
|
+
/** The keys written with `setAttribute`: none for HTML and an `<svg>` root. */
|
|
149
|
+
readonly attrs: Readonly<Record<string, string>>
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** An HTML element's attributes from a patch: none, one shared frozen record (#618: one a split). */
|
|
153
|
+
const NO_ATTRS: Readonly<Record<string, string>> = Object.freeze({})
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A style patch split for `el`: an HTML element's, or an `<svg>` root's, is all style. An SVG element
|
|
157
|
+
* inside one takes every key as an attribute but the keys CSS styles and a custom property; `attrX`,
|
|
158
|
+
* `attrY` and `attrScale` as its `x`, `y` and `scale` attributes; a `transform` with `transform-box:
|
|
159
|
+
* fill-box` and `transform-origin: 50% 50%`, unless the values set them, so it turns about its own centre
|
|
160
|
+
* as an HTML element does; and `pathLength`, `pathSpacing` and `pathOffset` as a normalised
|
|
161
|
+
* `stroke-dasharray` and `stroke-dashoffset`, with `pathLength="1"`.
|
|
162
|
+
*/
|
|
163
|
+
// The CSS keys: `SVG_STYLE_KEYS`.
|
|
164
|
+
export const splitPatch = (el: unknown, patch: Renderer.Patch): SplitPatch => {
|
|
165
|
+
if (isSvgInner(el)) return routeSvgInner(patch)
|
|
166
|
+
return { style: patch as Readonly<Record<string, unknown>>, attrs: NO_ATTRS }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* SVG's element names that HTML doesn't share, lowercase (Motion's list,
|
|
171
|
+
* framer-motion/src/render/svg/lowercase-elements.ts @ 16c6122, without the `svg` root).
|
|
172
|
+
* A name with a capital letter (`clipPath`, `linearGradient`, `feGaussianBlur` …) is SVG
|
|
173
|
+
* too, as Motion's `isSVGComponent` decides; `a`, `script`, `style` and `title` count as
|
|
174
|
+
* HTML, since a server has no namespace to tell them apart.
|
|
175
|
+
*/
|
|
176
|
+
const SVG_INNER_TAGS: ReadonlySet<string> = new Set([
|
|
177
|
+
"animate",
|
|
178
|
+
"circle",
|
|
179
|
+
"defs",
|
|
180
|
+
"desc",
|
|
181
|
+
"ellipse",
|
|
182
|
+
"g",
|
|
183
|
+
"image",
|
|
184
|
+
"line",
|
|
185
|
+
"filter",
|
|
186
|
+
"marker",
|
|
187
|
+
"mask",
|
|
188
|
+
"metadata",
|
|
189
|
+
"path",
|
|
190
|
+
"pattern",
|
|
191
|
+
"polygon",
|
|
192
|
+
"polyline",
|
|
193
|
+
"rect",
|
|
194
|
+
"stop",
|
|
195
|
+
"switch",
|
|
196
|
+
"symbol",
|
|
197
|
+
"text",
|
|
198
|
+
"tspan",
|
|
199
|
+
"use",
|
|
200
|
+
"view",
|
|
201
|
+
])
|
|
202
|
+
|
|
203
|
+
/** Whether `tag` names an SVG element inside an `<svg>`, by its name alone. */
|
|
204
|
+
export const isSvgInnerTag = (tag: string): boolean => !tag.includes("-") && (SVG_INNER_TAGS.has(tag) || /[A-Z]/.test(tag))
|
|
205
|
+
|
|
206
|
+
/** `splitPatch` by tag name, for where there's no element yet: a server render, a starting style. `undefined` is an HTML element. */
|
|
207
|
+
// ADR-0021.
|
|
208
|
+
export const splitPatchForTag = (tag: string | undefined, patch: Renderer.Patch): SplitPatch =>
|
|
209
|
+
Bool.match(Predicate.isNotUndefined(tag) && isSvgInnerTag(tag), {
|
|
210
|
+
onTrue: () => routeSvgInner(patch),
|
|
211
|
+
onFalse: () => ({ style: patch as Readonly<Record<string, unknown>>, attrs: NO_ATTRS }),
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
/** The svg-inner routing {@link splitPatch} and {@link splitPatchForTag} share. */
|
|
215
|
+
const routeSvgInner = (patch: Renderer.Patch): SplitPatch => {
|
|
216
|
+
const style: Record<string, unknown> = {}
|
|
217
|
+
const attrs: Record<string, string> = {}
|
|
218
|
+
let hasTransform = false
|
|
219
|
+
let pathLength: unknown
|
|
220
|
+
let pathSpacing: unknown
|
|
221
|
+
let pathOffset: unknown
|
|
222
|
+
|
|
223
|
+
for (const key in patch) {
|
|
224
|
+
const value = patch[key]
|
|
225
|
+
if (Predicate.isUndefined(value)) continue
|
|
226
|
+
if (key === "pathLength") {
|
|
227
|
+
pathLength = value
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
if (key === "pathSpacing") {
|
|
231
|
+
pathSpacing = value
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
if (key === "pathOffset") {
|
|
235
|
+
pathOffset = value
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
const alias: string | undefined = ATTR_ALIASES[key]
|
|
239
|
+
if (Predicate.isNotUndefined(alias)) {
|
|
240
|
+
attrs[alias] = String(value)
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
if (SVG_STYLE_KEYS.has(key)) {
|
|
244
|
+
style[key] = value
|
|
245
|
+
if (key === "transform") hasTransform = true
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
// A custom property is a style property (ADR-0014, *Amendment (#151)*).
|
|
249
|
+
if (isCustomProperty(key)) {
|
|
250
|
+
style[key] = value
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
attrs[attrName(key)] = String(value)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// SVG's transform-origin defaults to the document origin (0 0), not the element
|
|
257
|
+
// centre — so a CSS transform on SVG reads differently from HTML. `fill-box` +
|
|
258
|
+
// a centred origin restore HTML-like semantics (Motion). User-set wins.
|
|
259
|
+
if (hasTransform) {
|
|
260
|
+
if (Predicate.isUndefined(style["transformBox"]) && Predicate.isUndefined(style["transform-box"])) {
|
|
261
|
+
style["transform-box"] = "fill-box"
|
|
262
|
+
}
|
|
263
|
+
if (Predicate.isUndefined(style["transformOrigin"]) && Predicate.isUndefined(style["transform-origin"])) {
|
|
264
|
+
style["transform-origin"] = "50% 50%"
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// pathLength → normalised stroke dash (Motion's buildSVGPath). Unitless: Safari
|
|
269
|
+
// mis-scales px dash values when the page is zoomed.
|
|
270
|
+
if (Predicate.isNotUndefined(pathLength)) {
|
|
271
|
+
attrs["pathLength"] = "1"
|
|
272
|
+
attrs["stroke-dasharray"] = `${pathLength} ${spacingOf(pathSpacing)}`
|
|
273
|
+
attrs["stroke-dashoffset"] = String(dashOffsetOf(offsetOf(pathOffset)))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { style, attrs }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// The stroke dash's parts, per frame: early returns.
|
|
280
|
+
/** `pathSpacing`, 1 when none is given. */
|
|
281
|
+
const spacingOf = (spacing: unknown): unknown => {
|
|
282
|
+
if (Predicate.isNullish(spacing)) return 1
|
|
283
|
+
return spacing
|
|
284
|
+
}
|
|
285
|
+
/** `pathOffset` as a number, 0 when none is given. */
|
|
286
|
+
const offsetOf = (offset: unknown): number => {
|
|
287
|
+
if (Predicate.isNumber(offset)) return offset
|
|
288
|
+
if (Predicate.isNullish(offset)) return 0
|
|
289
|
+
return Number(offset)
|
|
290
|
+
}
|
|
291
|
+
/** The dash offset an offset draws: its negation, 0 for one that isn't finite. */
|
|
292
|
+
const dashOffsetOf = (offset: number): number => {
|
|
293
|
+
if (Number.isFinite(offset)) return -offset
|
|
294
|
+
return 0
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Writes each attribute onto `el` with `setAttribute`; nothing for an empty map. */
|
|
298
|
+
export const setSvgAttrs = (el: Element, attrs: Readonly<Record<string, string>>): void => {
|
|
299
|
+
for (const name in attrs) el.setAttribute(name, attrs[name])
|
|
300
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variable values in the DOM (#55, ADR-0035 *Amendment (#55)*; CONTEXT.md): a value that is wholly a
|
|
3
|
+
* `var(--name)`, with or without a fallback, stands for that custom property's value on the element.
|
|
4
|
+
* `variableValues(element)` is a Key rule bound to one element, which a binding adds to each leg it
|
|
5
|
+
* builds (`InterpolatorRegistry.withKeyRules`). Motion's rule (`getVariableValue`,
|
|
6
|
+
* css-variables-conversion.ts:18-53, and `DOMKeyframesResolver.readKeyframes` at 16c6122):
|
|
7
|
+
*
|
|
8
|
+
* - Either end that is a Variable value is resolved against the element, once, when the leg is
|
|
9
|
+
* built: the custom property's computed value, else its fallback, following a fallback that is
|
|
10
|
+
* itself a Variable value up to 4 deep. A numeric result is a number.
|
|
11
|
+
* - The resolved pair goes on through `next`, so the rules after this one still apply (`"none"` →
|
|
12
|
+
* `var(--shadow)` grows from its Animatable none), then blends by value.
|
|
13
|
+
* - The leg's last frame writes the target as written, so at rest the element follows the custom
|
|
14
|
+
* property again; a change mid-leg is followed from there.
|
|
15
|
+
* - A Variable value that doesn't resolve leaves the pair as written: the rule declines, and it
|
|
16
|
+
* switches as a Discrete pair, as Motion's does. A `var()` inside a larger value is text (#158).
|
|
17
|
+
*
|
|
18
|
+
* Client-only: it reads the element's computed style, after mount (ADR-0021).
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import type * as InterpolatorRegistry from "@wardic/core/InterpolatorRegistry"
|
|
23
|
+
import * as Arr from "effect/Array"
|
|
24
|
+
import * as Bool from "effect/Boolean"
|
|
25
|
+
import * as Effect from "effect/Effect"
|
|
26
|
+
import { pipe } from "effect/Function"
|
|
27
|
+
import * as Option from "effect/Option"
|
|
28
|
+
import * as Predicate from "effect/Predicate"
|
|
29
|
+
import * as Str from "effect/String"
|
|
30
|
+
|
|
31
|
+
/** A value a key blends: a number or a string. */
|
|
32
|
+
/** What a Variable value resolves to: a number or a string. */
|
|
33
|
+
type Value = number | string
|
|
34
|
+
|
|
35
|
+
/** A Variable value's custom property, and its fallback when it has one (Motion's `parseCSSVariable`). */
|
|
36
|
+
const PARTS_RE = /^var\(\s*(--[\w-]+)\s*(?:,\s*([\s\S]*?))?\s*\)$/
|
|
37
|
+
|
|
38
|
+
/** The cheap first check: a string that starts as a `var()` does. */
|
|
39
|
+
const VAR_START_RE = /^\s*var\(/
|
|
40
|
+
|
|
41
|
+
/** How a character moves the bracket depth. */
|
|
42
|
+
const depthStep = (ch: string): number => Bool.match(ch === "(", { onTrue: () => 1, onFalse: () => Bool.match(ch === ")", { onTrue: () => -1, onFalse: () => 0 }) })
|
|
43
|
+
|
|
44
|
+
/** Brackets that close as they open: a fallback that is one value, not the rest of a longer string. */
|
|
45
|
+
const isBalanced = (text: string): boolean => {
|
|
46
|
+
const end = Arr.reduce(Str.split(text, ""), 0, (depth, ch) => Bool.match(depth < 0, { onTrue: () => depth, onFalse: () => depth + depthStep(ch) }))
|
|
47
|
+
return end === 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A Variable value's parts: its custom property's name and its fallback. */
|
|
51
|
+
interface Parts {
|
|
52
|
+
readonly name: string
|
|
53
|
+
readonly fallback: Option.Option<string>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A Variable value's parts, or `None` for any other value: `var(--name)` or `var(--name, fallback)`,
|
|
58
|
+
* the fallback's brackets balanced, at any depth of nested `var()`s (a pattern alone stops at one).
|
|
59
|
+
* So `var(--a, 1px) var(--b)` isn't one: two values side by side.
|
|
60
|
+
*/
|
|
61
|
+
const partsOf = (value: string): Option.Option<Parts> => {
|
|
62
|
+
// Asked of a string end of every key, each leg (#460): one that doesn't start as one builds nothing.
|
|
63
|
+
if (!VAR_START_RE.test(value)) return Option.none()
|
|
64
|
+
return pipe(
|
|
65
|
+
Option.fromNullishOr(PARTS_RE.exec(Str.trim(value))),
|
|
66
|
+
Option.map(([, name, fallback]): Parts => ({ name, fallback: Option.map(Option.fromUndefinedOr(fallback), Str.trim) })),
|
|
67
|
+
Option.filter((parts) => Option.match(parts.fallback, { onNone: () => true, onSome: isBalanced })),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** How deep a fallback that is itself a Variable value is followed (Motion's `maxDepth`). */
|
|
72
|
+
const MAX_DEPTH = 4
|
|
73
|
+
|
|
74
|
+
/** A numeric string, which a resolved value becomes a number from (Motion's `isNumericalString`). */
|
|
75
|
+
const NUMERIC_RE = /^-?\d*\.?\d+(?:e[-+]?\d+)?$/i
|
|
76
|
+
|
|
77
|
+
/** Whether `value` is a Variable value: a string that's wholly a `var()`. */
|
|
78
|
+
// CONTEXT.md's Variable value.
|
|
79
|
+
export const isVariableValue = (value: unknown): value is string => Predicate.isString(value) && Option.isSome(partsOf(value))
|
|
80
|
+
|
|
81
|
+
/** A resolved value as a key blends it: a number when it reads as one, else the string. */
|
|
82
|
+
const valueOf = (raw: string): Value => Bool.match(NUMERIC_RE.test(raw), { onTrue: () => Number.parseFloat(raw), onFalse: () => raw })
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* What a Variable value stands for on `element`: its custom property's computed value, else its
|
|
86
|
+
* fallback, a fallback that is itself a Variable value followed up to 4 deep. `None` when nothing
|
|
87
|
+
* resolves. Reads the element's computed style.
|
|
88
|
+
*/
|
|
89
|
+
export const resolveVariable = (element: Element, value: string): Option.Option<Value> => resolveAt(element, value, 1)
|
|
90
|
+
|
|
91
|
+
/** {@link resolveVariable} at `depth`, following a fallback that's itself a Variable value. */
|
|
92
|
+
// #618 (F7): the depth counter was the public function's third parameter.
|
|
93
|
+
const resolveAt = (element: Element, value: string, depth: number): Option.Option<Value> => {
|
|
94
|
+
if (depth > MAX_DEPTH) return Option.none()
|
|
95
|
+
return Option.flatMap(partsOf(value), ({ name, fallback }) => {
|
|
96
|
+
const own = Str.trim(getComputedStyle(element).getPropertyValue(name))
|
|
97
|
+
if (Str.isNonEmpty(own)) return Option.some(valueOf(own))
|
|
98
|
+
return pipe(
|
|
99
|
+
fallback,
|
|
100
|
+
Option.filter(Str.isNonEmpty),
|
|
101
|
+
Option.flatMap((rest) =>
|
|
102
|
+
Bool.match(isVariableValue(rest), {
|
|
103
|
+
onTrue: () => resolveAt(element, rest, depth + 1),
|
|
104
|
+
onFalse: () => Option.some(valueOf(rest)),
|
|
105
|
+
})),
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** An end as the leg blends it: a Variable value resolved on `element`, any other value as it is. */
|
|
111
|
+
const endOf = (element: Element, value: Value): Option.Option<Value> =>
|
|
112
|
+
Bool.match(isVariableValue(value), {
|
|
113
|
+
onTrue: () => resolveVariable(element, value as string),
|
|
114
|
+
onFalse: () => Option.some(value),
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A blend that writes its target as written on the run's last Frame: a Variable value rests as the
|
|
119
|
+
* `var()` itself, so the element follows its custom property again. Per frame: a statement `if`.
|
|
120
|
+
*/
|
|
121
|
+
const landOn =
|
|
122
|
+
(blend: InterpolatorRegistry.KeyBlend, to: Value): InterpolatorRegistry.KeyBlend =>
|
|
123
|
+
(t, done) => {
|
|
124
|
+
if (done) return to
|
|
125
|
+
return blend(t, done)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The Key rule for `element`'s Variable values: a pair with one at either end blends what they resolve
|
|
130
|
+
* to on `element`, through the rules after it and then by value, and lands on its target as written. It
|
|
131
|
+
* declines a pair with none, and one that doesn't resolve.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* yield* buildLegs.pipe(InterpolatorRegistry.withKeyRules([Variables.values(element)]))
|
|
135
|
+
*/
|
|
136
|
+
// #55.
|
|
137
|
+
export const variableValues = (element: Element): InterpolatorRegistry.KeyRule => ({
|
|
138
|
+
name: "variable values",
|
|
139
|
+
blend: (key, from, to, _resolve, next) => {
|
|
140
|
+
// Per key, each leg (#460): a statement.
|
|
141
|
+
if (!isVariableValue(from) && !isVariableValue(to)) return Option.none()
|
|
142
|
+
return Option.map(Option.all([endOf(element, from), endOf(element, to)]), ([a, b]) =>
|
|
143
|
+
Effect.map(next(key, a, b), (blend) => landOn(blend, to)))
|
|
144
|
+
},
|
|
145
|
+
})
|