@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.
Files changed (285) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -2
  3. package/THIRD_PARTY_NOTICES.md +145 -0
  4. package/dist/Appear.d.ts +24 -0
  5. package/dist/Appear.d.ts.map +1 -0
  6. package/dist/Appear.js +25 -0
  7. package/dist/Appear.js.map +1 -0
  8. package/dist/Color.d.ts +14 -0
  9. package/dist/Color.d.ts.map +1 -0
  10. package/dist/Color.js +16 -0
  11. package/dist/Color.js.map +1 -0
  12. package/dist/DomAnnouncer.d.ts +13 -0
  13. package/dist/DomAnnouncer.d.ts.map +1 -0
  14. package/dist/DomAnnouncer.js +14 -0
  15. package/dist/DomAnnouncer.js.map +1 -0
  16. package/dist/DomClock.d.ts +11 -0
  17. package/dist/DomClock.d.ts.map +1 -0
  18. package/dist/DomClock.js +12 -0
  19. package/dist/DomClock.js.map +1 -0
  20. package/dist/DomDefaultTransition.d.ts +12 -0
  21. package/dist/DomDefaultTransition.d.ts.map +1 -0
  22. package/dist/DomDefaultTransition.js +13 -0
  23. package/dist/DomDefaultTransition.js.map +1 -0
  24. package/dist/DomInput.d.ts +13 -0
  25. package/dist/DomInput.d.ts.map +1 -0
  26. package/dist/DomInput.js +15 -0
  27. package/dist/DomInput.js.map +1 -0
  28. package/dist/DomInterpolators.d.ts +13 -0
  29. package/dist/DomInterpolators.d.ts.map +1 -0
  30. package/dist/DomInterpolators.js +14 -0
  31. package/dist/DomInterpolators.js.map +1 -0
  32. package/dist/DomKeyRules.d.ts +13 -0
  33. package/dist/DomKeyRules.d.ts.map +1 -0
  34. package/dist/DomKeyRules.js +14 -0
  35. package/dist/DomKeyRules.js.map +1 -0
  36. package/dist/DomPositionalKeys.d.ts +13 -0
  37. package/dist/DomPositionalKeys.d.ts.map +1 -0
  38. package/dist/DomPositionalKeys.js +14 -0
  39. package/dist/DomPositionalKeys.js.map +1 -0
  40. package/dist/DomProbe.d.ts +13 -0
  41. package/dist/DomProbe.d.ts.map +1 -0
  42. package/dist/DomProbe.js +14 -0
  43. package/dist/DomProbe.js.map +1 -0
  44. package/dist/DomProjectionSystem.d.ts +28 -0
  45. package/dist/DomProjectionSystem.d.ts.map +1 -0
  46. package/dist/DomProjectionSystem.js +41 -0
  47. package/dist/DomProjectionSystem.js.map +1 -0
  48. package/dist/DomRenderer.d.ts +13 -0
  49. package/dist/DomRenderer.d.ts.map +1 -0
  50. package/dist/DomRenderer.js +15 -0
  51. package/dist/DomRenderer.js.map +1 -0
  52. package/dist/FlushGate.d.ts +48 -0
  53. package/dist/FlushGate.d.ts.map +1 -0
  54. package/dist/FlushGate.js +62 -0
  55. package/dist/FlushGate.js.map +1 -0
  56. package/dist/MeasuredPair.d.ts +14 -0
  57. package/dist/MeasuredPair.d.ts.map +1 -0
  58. package/dist/MeasuredPair.js +16 -0
  59. package/dist/MeasuredPair.js.map +1 -0
  60. package/dist/OwnValues.d.ts +14 -0
  61. package/dist/OwnValues.d.ts.map +1 -0
  62. package/dist/OwnValues.js +15 -0
  63. package/dist/OwnValues.js.map +1 -0
  64. package/dist/ProjectionSystem.d.ts +34 -0
  65. package/dist/ProjectionSystem.d.ts.map +1 -0
  66. package/dist/ProjectionSystem.js +29 -0
  67. package/dist/ProjectionSystem.js.map +1 -0
  68. package/dist/ReorderController.d.ts +41 -0
  69. package/dist/ReorderController.d.ts.map +1 -0
  70. package/dist/ReorderController.js +44 -0
  71. package/dist/ReorderController.js.map +1 -0
  72. package/dist/Report.d.ts +12 -0
  73. package/dist/Report.d.ts.map +1 -0
  74. package/dist/Report.js +13 -0
  75. package/dist/Report.js.map +1 -0
  76. package/dist/ScrollOffset.d.ts +34 -0
  77. package/dist/ScrollOffset.d.ts.map +1 -0
  78. package/dist/ScrollOffset.js +42 -0
  79. package/dist/ScrollOffset.js.map +1 -0
  80. package/dist/Style.d.ts +15 -0
  81. package/dist/Style.d.ts.map +1 -0
  82. package/dist/Style.js +17 -0
  83. package/dist/Style.js.map +1 -0
  84. package/dist/Svg.d.ts +13 -0
  85. package/dist/Svg.d.ts.map +1 -0
  86. package/dist/Svg.js +14 -0
  87. package/dist/Svg.js.map +1 -0
  88. package/dist/Variables.d.ts +13 -0
  89. package/dist/Variables.d.ts.map +1 -0
  90. package/dist/Variables.js +14 -0
  91. package/dist/Variables.js.map +1 -0
  92. package/dist/index.d.ts +72 -0
  93. package/dist/index.d.ts.map +1 -0
  94. package/dist/index.js +60 -0
  95. package/dist/index.js.map +1 -0
  96. package/dist/internal/announcer.d.ts +50 -0
  97. package/dist/internal/announcer.d.ts.map +1 -0
  98. package/dist/internal/announcer.js +115 -0
  99. package/dist/internal/announcer.js.map +1 -0
  100. package/dist/internal/appear.d.ts +169 -0
  101. package/dist/internal/appear.d.ts.map +1 -0
  102. package/dist/internal/appear.js +499 -0
  103. package/dist/internal/appear.js.map +1 -0
  104. package/dist/internal/auto-scroll.d.ts +24 -0
  105. package/dist/internal/auto-scroll.d.ts.map +1 -0
  106. package/dist/internal/auto-scroll.js +124 -0
  107. package/dist/internal/auto-scroll.js.map +1 -0
  108. package/dist/internal/clock.d.ts +19 -0
  109. package/dist/internal/clock.d.ts.map +1 -0
  110. package/dist/internal/clock.js +39 -0
  111. package/dist/internal/clock.js.map +1 -0
  112. package/dist/internal/colors.d.ts +45 -0
  113. package/dist/internal/colors.d.ts.map +1 -0
  114. package/dist/internal/colors.js +193 -0
  115. package/dist/internal/colors.js.map +1 -0
  116. package/dist/internal/complex.d.ts +24 -0
  117. package/dist/internal/complex.d.ts.map +1 -0
  118. package/dist/internal/complex.js +28 -0
  119. package/dist/internal/complex.js.map +1 -0
  120. package/dist/internal/content-watch.d.ts +20 -0
  121. package/dist/internal/content-watch.d.ts.map +1 -0
  122. package/dist/internal/content-watch.js +76 -0
  123. package/dist/internal/content-watch.js.map +1 -0
  124. package/dist/internal/default-transition.d.ts +25 -0
  125. package/dist/internal/default-transition.d.ts.map +1 -0
  126. package/dist/internal/default-transition.js +41 -0
  127. package/dist/internal/default-transition.js.map +1 -0
  128. package/dist/internal/drag-defaults.d.ts +18 -0
  129. package/dist/internal/drag-defaults.d.ts.map +1 -0
  130. package/dist/internal/drag-defaults.js +38 -0
  131. package/dist/internal/drag-defaults.js.map +1 -0
  132. package/dist/internal/gate.d.ts +186 -0
  133. package/dist/internal/gate.d.ts.map +1 -0
  134. package/dist/internal/gate.js +453 -0
  135. package/dist/internal/gate.js.map +1 -0
  136. package/dist/internal/input.d.ts +41 -0
  137. package/dist/internal/input.d.ts.map +1 -0
  138. package/dist/internal/input.js +476 -0
  139. package/dist/internal/input.js.map +1 -0
  140. package/dist/internal/interpolators.d.ts +19 -0
  141. package/dist/internal/interpolators.d.ts.map +1 -0
  142. package/dist/internal/interpolators.js +30 -0
  143. package/dist/internal/interpolators.js.map +1 -0
  144. package/dist/internal/key-rules.d.ts +11 -0
  145. package/dist/internal/key-rules.d.ts.map +1 -0
  146. package/dist/internal/key-rules.js +244 -0
  147. package/dist/internal/key-rules.js.map +1 -0
  148. package/dist/internal/measure.d.ts +150 -0
  149. package/dist/internal/measure.d.ts.map +1 -0
  150. package/dist/internal/measure.js +278 -0
  151. package/dist/internal/measure.js.map +1 -0
  152. package/dist/internal/measured-pairs.d.ts +38 -0
  153. package/dist/internal/measured-pairs.d.ts.map +1 -0
  154. package/dist/internal/measured-pairs.js +251 -0
  155. package/dist/internal/measured-pairs.js.map +1 -0
  156. package/dist/internal/own-values.d.ts +35 -0
  157. package/dist/internal/own-values.d.ts.map +1 -0
  158. package/dist/internal/own-values.js +117 -0
  159. package/dist/internal/own-values.js.map +1 -0
  160. package/dist/internal/pass-ends.d.ts +21 -0
  161. package/dist/internal/pass-ends.d.ts.map +1 -0
  162. package/dist/internal/pass-ends.js +14 -0
  163. package/dist/internal/pass-ends.js.map +1 -0
  164. package/dist/internal/positional-keys.d.ts +16 -0
  165. package/dist/internal/positional-keys.d.ts.map +1 -0
  166. package/dist/internal/positional-keys.js +21 -0
  167. package/dist/internal/positional-keys.js.map +1 -0
  168. package/dist/internal/probe.d.ts +10 -0
  169. package/dist/internal/probe.d.ts.map +1 -0
  170. package/dist/internal/probe.js +117 -0
  171. package/dist/internal/probe.js.map +1 -0
  172. package/dist/internal/projection-defaults.d.ts +34 -0
  173. package/dist/internal/projection-defaults.d.ts.map +1 -0
  174. package/dist/internal/projection-defaults.js +21 -0
  175. package/dist/internal/projection-defaults.js.map +1 -0
  176. package/dist/internal/projection.d.ts +529 -0
  177. package/dist/internal/projection.d.ts.map +1 -0
  178. package/dist/internal/projection.js +4990 -0
  179. package/dist/internal/projection.js.map +1 -0
  180. package/dist/internal/renderer.d.ts +86 -0
  181. package/dist/internal/renderer.d.ts.map +1 -0
  182. package/dist/internal/renderer.js +616 -0
  183. package/dist/internal/renderer.js.map +1 -0
  184. package/dist/internal/reorder.d.ts +159 -0
  185. package/dist/internal/reorder.d.ts.map +1 -0
  186. package/dist/internal/reorder.js +437 -0
  187. package/dist/internal/reorder.js.map +1 -0
  188. package/dist/internal/report.d.ts +30 -0
  189. package/dist/internal/report.d.ts.map +1 -0
  190. package/dist/internal/report.js +60 -0
  191. package/dist/internal/report.js.map +1 -0
  192. package/dist/internal/require-global.d.ts +11 -0
  193. package/dist/internal/require-global.d.ts.map +1 -0
  194. package/dist/internal/require-global.js +31 -0
  195. package/dist/internal/require-global.js.map +1 -0
  196. package/dist/internal/reveal.d.ts +16 -0
  197. package/dist/internal/reveal.d.ts.map +1 -0
  198. package/dist/internal/reveal.js +66 -0
  199. package/dist/internal/reveal.js.map +1 -0
  200. package/dist/internal/runner.d.ts +21 -0
  201. package/dist/internal/runner.d.ts.map +1 -0
  202. package/dist/internal/runner.js +48 -0
  203. package/dist/internal/runner.js.map +1 -0
  204. package/dist/internal/scale-correction.d.ts +80 -0
  205. package/dist/internal/scale-correction.d.ts.map +1 -0
  206. package/dist/internal/scale-correction.js +227 -0
  207. package/dist/internal/scale-correction.js.map +1 -0
  208. package/dist/internal/scroll.d.ts +35 -0
  209. package/dist/internal/scroll.d.ts.map +1 -0
  210. package/dist/internal/scroll.js +244 -0
  211. package/dist/internal/scroll.js.map +1 -0
  212. package/dist/internal/scrollers.d.ts +35 -0
  213. package/dist/internal/scrollers.d.ts.map +1 -0
  214. package/dist/internal/scrollers.js +38 -0
  215. package/dist/internal/scrollers.js.map +1 -0
  216. package/dist/internal/starting-style.d.ts +141 -0
  217. package/dist/internal/starting-style.d.ts.map +1 -0
  218. package/dist/internal/starting-style.js +300 -0
  219. package/dist/internal/starting-style.js.map +1 -0
  220. package/dist/internal/svg.d.ts +60 -0
  221. package/dist/internal/svg.d.ts.map +1 -0
  222. package/dist/internal/svg.js +257 -0
  223. package/dist/internal/svg.js.map +1 -0
  224. package/dist/internal/variables.d.ts +45 -0
  225. package/dist/internal/variables.d.ts.map +1 -0
  226. package/dist/internal/variables.js +92 -0
  227. package/dist/internal/variables.js.map +1 -0
  228. package/package.json +65 -4
  229. package/src/Appear.ts +45 -0
  230. package/src/Color.ts +24 -0
  231. package/src/DomAnnouncer.ts +22 -0
  232. package/src/DomClock.ts +13 -0
  233. package/src/DomDefaultTransition.ts +16 -0
  234. package/src/DomInput.ts +22 -0
  235. package/src/DomInterpolators.ts +16 -0
  236. package/src/DomKeyRules.ts +17 -0
  237. package/src/DomPositionalKeys.ts +16 -0
  238. package/src/DomProbe.ts +15 -0
  239. package/src/DomProjectionSystem.ts +44 -0
  240. package/src/DomRenderer.ts +26 -0
  241. package/src/FlushGate.ts +81 -0
  242. package/src/MeasuredPair.ts +15 -0
  243. package/src/OwnValues.ts +19 -0
  244. package/src/ProjectionSystem.ts +51 -0
  245. package/src/ReorderController.ts +72 -0
  246. package/src/Report.ts +17 -0
  247. package/src/ScrollOffset.ts +54 -0
  248. package/src/Style.ts +37 -0
  249. package/src/Svg.ts +21 -0
  250. package/src/Variables.ts +17 -0
  251. package/src/index.ts +106 -0
  252. package/src/internal/announcer.ts +132 -0
  253. package/src/internal/appear.ts +739 -0
  254. package/src/internal/auto-scroll.ts +176 -0
  255. package/src/internal/clock.ts +45 -0
  256. package/src/internal/colors.ts +258 -0
  257. package/src/internal/complex.ts +29 -0
  258. package/src/internal/content-watch.ts +84 -0
  259. package/src/internal/culori.d.ts +58 -0
  260. package/src/internal/default-transition.ts +50 -0
  261. package/src/internal/drag-defaults.ts +54 -0
  262. package/src/internal/gate.ts +593 -0
  263. package/src/internal/input.ts +579 -0
  264. package/src/internal/interpolators.ts +33 -0
  265. package/src/internal/key-rules.ts +357 -0
  266. package/src/internal/measure.ts +357 -0
  267. package/src/internal/measured-pairs.ts +297 -0
  268. package/src/internal/own-values.ts +166 -0
  269. package/src/internal/pass-ends.ts +23 -0
  270. package/src/internal/positional-keys.ts +24 -0
  271. package/src/internal/probe.ts +176 -0
  272. package/src/internal/projection-defaults.ts +45 -0
  273. package/src/internal/projection.ts +5999 -0
  274. package/src/internal/renderer.ts +646 -0
  275. package/src/internal/reorder.ts +604 -0
  276. package/src/internal/report.ts +69 -0
  277. package/src/internal/require-global.ts +46 -0
  278. package/src/internal/reveal.ts +69 -0
  279. package/src/internal/runner.ts +64 -0
  280. package/src/internal/scale-correction.ts +310 -0
  281. package/src/internal/scroll.ts +356 -0
  282. package/src/internal/scrollers.ts +51 -0
  283. package/src/internal/starting-style.ts +396 -0
  284. package/src/internal/svg.ts +300 -0
  285. package/src/internal/variables.ts +145 -0
@@ -0,0 +1,357 @@
1
+ /**
2
+ * The DOM Environment's **Key rules** (CONTEXT.md; ADR-0035): how a CSS property's pair blends,
3
+ * by the property's name, which core's `InterpolatorRegistry` asks before blending a pair by value.
4
+ * Core holds none. Both rules take a key by its CSS name or in camelCase (`box-shadow`, `boxShadow`):
5
+ * `WardicValues` types keys as CSS names, and camelCase works as well.
6
+ *
7
+ * - **`visibility`** (ADR-0024, decision 4): `display` and `visibility` turning to `none` or
8
+ * `hidden` show their start until the run's last Frame (Motion's `mixVisibility`), keyed on the
9
+ * Frame's `done`, so a spring crossing its target can't flicker them.
10
+ * - **`animatableNone`** (ADR-0015, *Amendment (#61)*): an empty end (`"none"`, `0`, `"0"`, a zero
11
+ * with a unit) facing a complex value or a lone colour stands for that value at zero, numbers 0
12
+ * and colours transparent (Motion's `getAnimatableNone`). A filter puts each function at its
13
+ * identity and a mask keeps its colours opaque. A plain length isn't a template: `none` isn't 0
14
+ * for `max-height`. A shadow list (`box-shadow`, `text-shadow`) blends shadow by shadow: a length
15
+ * missing from the start's shadow counts as 0, and a missing shadow grows from its zero. Either
16
+ * way the run's last Frame writes the target as given, so a shadow, filter or fill animated to
17
+ * `none` rests at `none`, with no stacking context, containing block or painted fill left.
18
+ *
19
+ * Each rule declines synchronously, allocating nothing, for a key it can't take: the registry asks
20
+ * every rule for every key of every leg it builds. The blends they hand back are on the per-frame
21
+ * path (ADR-0025): statement `if`s, and a counted loop for a shadow list.
22
+ *
23
+ * @module
24
+ */
25
+ import * as Interpolator from "@wardic/core/Interpolator"
26
+ import * as InterpolatorRegistry from "@wardic/core/InterpolatorRegistry"
27
+ import * as Arr from "effect/Array"
28
+ import * as Bool from "effect/Boolean"
29
+ import * as Effect from "effect/Effect"
30
+ import { pipe } from "effect/Function"
31
+ import * as Match from "effect/Match"
32
+ import * as Option from "effect/Option"
33
+ import * as Predicate from "effect/Predicate"
34
+ import * as Str from "effect/String"
35
+ import { type Color, cssColor, formatColor, parseColor } from "./colors.ts"
36
+ import { cssComplex } from "./complex.ts"
37
+ import { dashCase } from "./svg.ts"
38
+
39
+ /** A value a key blends: a number or a string. */
40
+ type Value = number | string
41
+
42
+ /** A pair the registry blends by value. */
43
+ type Pair = readonly [from: Value, to: Value]
44
+
45
+ /** A key's pairs, from the Animatable none rule: one, or, for a shadow list, one per shadow. */
46
+ interface Prepared {
47
+ readonly pairs: ReadonlyArray<Pair>
48
+ }
49
+
50
+ // The DOM's own Interpolators' `parse` and `format` (ADR-0036; plain functions since #511): a
51
+ // template's colours are tokens and its `var()`s text, as the DOM's registry reads them, so a zero
52
+ // keeps a variable's name as written (#160).
53
+ const decodeComplex = cssComplex.parse
54
+ const encodeComplex = cssComplex.format
55
+ const decodeColor = parseColor
56
+ const encodeColor = formatColor
57
+
58
+ /** The keys whose empty end takes each filter function's identity (Motion's `filter` type, and the backdrop it leaves out). */
59
+ const FILTER_KEYS: ReadonlySet<string> = new Set(["filter", "-webkit-filter", "backdrop-filter", "-webkit-backdrop-filter"])
60
+ /** The keys whose empty end keeps its colours opaque (Motion's `mask` type, and the images it leaves out). */
61
+ const MASK_KEYS: ReadonlySet<string> = new Set(["mask", "-webkit-mask", "mask-image", "-webkit-mask-image"])
62
+ /** The keys matched shadow by shadow. */
63
+ const SHADOW_KEYS: ReadonlySet<string> = new Set(["box-shadow", "text-shadow"])
64
+
65
+ /** Whether `key`, written either way, is in `keys`. */
66
+ const isOneOf =
67
+ (keys: ReadonlySet<string>): Predicate.Predicate<string> =>
68
+ (key) =>
69
+ keys.has(dashCase(key))
70
+
71
+ // Built once (#460): `mayApply` asks of every key, each leg.
72
+ const isFilterKey = isOneOf(FILTER_KEYS)
73
+ const isMaskKey = isOneOf(MASK_KEYS)
74
+ const isShadowKey = isOneOf(SHADOW_KEYS)
75
+
76
+ /** The filter functions whose identity is 1 (or 100%); every other is at its identity at 0 (Motion's `maxDefaults`). */
77
+ const IDENTITY_ONE: ReadonlySet<string> = new Set(["brightness", "contrast", "saturate", "opacity"])
78
+
79
+ /** The words an empty end can be. */
80
+ const EMPTY_WORDS: ReadonlySet<string> = new Set(["none", "0"])
81
+ /** A zero with a unit: `"0px"`, `"0%"`, `"0deg"` (Motion's `isZeroValueString`). */
82
+ const ZERO_WITH_UNIT_RE = /^0[^.\s]+$/
83
+
84
+ /** An empty string end: `"none"`, `"0"`, or a zero with a unit. */
85
+ const isEmptyWord: Predicate.Predicate<string> = Predicate.mapInput(
86
+ Predicate.or((word: string) => EMPTY_WORDS.has(word), (word: string) => ZERO_WITH_UNIT_RE.test(word)),
87
+ Str.trim,
88
+ )
89
+
90
+ /** The number 0. */
91
+ const isZero: Predicate.Predicate<Value> = (value) => value === 0
92
+
93
+ /** An empty end (Motion's `isNone`): the number 0, `"none"`, `"0"`, or a zero with a unit. */
94
+ const isEmptyEnd: Predicate.Predicate<Value> = Predicate.or(isZero, (value: Value) => Predicate.isString(value) && isEmptyWord(value))
95
+
96
+ /** The first number in a filter function's arguments. */
97
+ const FIRST_NUMBER_RE = /-?\d*\.?\d+(?:e[-+]?\d+)?/i
98
+
99
+ /** How an empty end's colours turn: transparent, or opaque in a mask. */
100
+ type ColorTurn = (color: Color) => Color
101
+
102
+ const transparent: ColorTurn = (color) => ({ ...color, alpha: 0 })
103
+ const opaque: ColorTurn = (color) => ({ ...color, alpha: 1 })
104
+
105
+ /** A number written straight after a `/`: a `calc()` divisor, kept, since a 0 there is NaN (Motion's `convertToZero`). */
106
+ const isDivisor = (partBefore: string): boolean => Str.endsWith("/")(Str.trim(partBefore))
107
+
108
+ /** A colour token of the DOM's complex tier. */
109
+ const isColorToken = (token: Interpolator.KindToken): token is { readonly kind: string; readonly value: Color } => token.kind === cssColor.name
110
+
111
+ /** A complex value at zero: every number 0 (but a divisor), every colour turned. */
112
+ const zeroComplex = (complex: Interpolator.Complex, turn: ColorTurn): string =>
113
+ encodeComplex({
114
+ parts: complex.parts,
115
+ tokens: Arr.map(complex.tokens, (token, i): Interpolator.ComplexToken =>
116
+ Match.value(token).pipe(
117
+ Match.when(Predicate.isNumber, (n) =>
118
+ Bool.match(isDivisor(complex.parts[i]), { onTrue: () => n, onFalse: () => 0 })),
119
+ Match.when(isColorToken, (color) => ({ kind: color.kind, value: turn(color.value) })),
120
+ Match.orElse((token) => token),
121
+ )),
122
+ })
123
+
124
+ /** A value at zero, as a complex value or a lone colour; `None` when it's neither. */
125
+ const zeroOf = (value: string, turn: ColorTurn): Option.Option<string> =>
126
+ Option.orElse(
127
+ Option.map(decodeComplex(value), (complex) => zeroComplex(complex, turn)),
128
+ () => Option.map(decodeColor(value), (color) => encodeColor(turn(color))),
129
+ )
130
+
131
+ /** How many numbers a complex value holds outside its colours: what complex matching counts. 0 for one that isn't. */
132
+ const numberCountOf = (value: string): number =>
133
+ Option.match(decodeComplex(value), {
134
+ onNone: () => 0,
135
+ onSome: (complex) => Arr.filter(complex.tokens, Predicate.isNumber).length,
136
+ })
137
+
138
+ /** A top-level comma: one not inside a bracket (a shadow's colour, `rgba(…)`, holds commas). One level deep, as shadows are. */
139
+ const TOP_LEVEL_COMMA_RE = /,(?![^(]*\))/
140
+
141
+ /** A filter's functions, each with what's in its brackets, one bracket level deep (`drop-shadow(0px 0px 4px rgb(…))`). */
142
+ const FILTER_FUNCTION_RE = /[a-z-]+\((?:[^()]|\([^()]*\))*\)/gi
143
+
144
+ /** A shadow list's shadows, trimmed. */
145
+ const shadowsOf = (value: string): ReadonlyArray<string> => Arr.filter(Arr.map(value.split(TOP_LEVEL_COMMA_RE), Str.trim), Str.isNonEmpty)
146
+
147
+ /** A filter's functions. */
148
+ const filterFunctionsOf = (value: string): ReadonlyArray<string> => Option.getOrElse(Str.match(FILTER_FUNCTION_RE)(value), () => [])
149
+
150
+ /**
151
+ * One filter function at its identity (Motion's `applyDefaultFilter`): its first number becomes 1
152
+ * (or 100) for `brightness`, `contrast`, `saturate` and `opacity`, 0 for the rest, keeping its unit.
153
+ * A `drop-shadow()` grows from a transparent zero, as a shadow does (#61; Motion keeps it as is).
154
+ * One with no number (`url(#f)`) stays as it is.
155
+ */
156
+ const filterFunctionAtIdentity = (fn: string): string => {
157
+ const name = Str.trim(fn.slice(0, Math.max(0, fn.indexOf("("))))
158
+ const args = fn.slice(fn.indexOf("(") + 1, fn.lastIndexOf(")"))
159
+ return Bool.match(name === "drop-shadow", {
160
+ onTrue: () => Option.getOrElse(zeroOf(fn, transparent), () => fn),
161
+ onFalse: () =>
162
+ Option.match(Option.fromNullishOr(FIRST_NUMBER_RE.exec(args)), {
163
+ onNone: () => fn,
164
+ onSome: ([number]) => {
165
+ const unit = Str.replace(number, "")(args)
166
+ const one = Bool.match(Str.isEmpty(unit), { onTrue: () => 1, onFalse: () => 100 })
167
+ const identity = Bool.match(IDENTITY_ONE.has(name), { onTrue: () => one, onFalse: () => 0 })
168
+ return `${name}(${identity}${unit})`
169
+ },
170
+ }),
171
+ })
172
+ }
173
+
174
+ /** A filter at its identity: each top-level function at its own (Motion's `filter.getAnimatableNone`). */
175
+ const filterAtIdentity = (value: string): Option.Option<string> =>
176
+ Option.map(decodeComplex(value), () => Arr.join(Arr.map(filterFunctionsOf(value), filterFunctionAtIdentity), " "))
177
+
178
+ /** What `key`'s empty end stands for, facing `template` (CONTEXT.md's Animatable none); `None` when `template` can't be one. */
179
+ const noneOf = (key: string, template: string): Option.Option<string> =>
180
+ Bool.match(isFilterKey(key), {
181
+ onTrue: () => filterAtIdentity(template),
182
+ onFalse: () => zeroOf(template, Bool.match(isMaskKey(key), { onTrue: () => opaque, onFalse: () => transparent })),
183
+ })
184
+
185
+ /** `auto`, which carries nothing to grow from. */
186
+ const isAuto: Predicate.Predicate<string> = (s) => Str.trim(s) === "auto"
187
+
188
+ /** An end a template can come from: a string other than an empty end or `auto`. */
189
+ const templateOf = (value: Value): Option.Option<string> =>
190
+ Option.filter(Option.liftPredicate(value, Predicate.isString), Predicate.not(Predicate.or(isEmptyWord, isAuto)))
191
+
192
+ /** `from` empty and `to` a template: the pair from `to`'s Animatable none. */
193
+ const fromNone = (key: string, from: Value, to: Value): Option.Option<Prepared> =>
194
+ pipe(
195
+ Option.liftPredicate(from, isEmptyEnd),
196
+ Option.flatMap(() => templateOf(to)),
197
+ Option.flatMap((template) => noneOf(key, template)),
198
+ Option.map((none): Prepared => ({ pairs: [[none, to]] })),
199
+ )
200
+
201
+ /** `to` empty and `from` a template: the pair to `from`'s Animatable none, landing on `to` as given. */
202
+ const toNone = (key: string, from: Value, to: Value): Option.Option<Prepared> =>
203
+ pipe(
204
+ Option.liftPredicate(to, isEmptyEnd),
205
+ Option.flatMap(() => templateOf(from)),
206
+ Option.flatMap((template) => noneOf(key, template)),
207
+ Option.map((none): Prepared => ({ pairs: [[from, none]] })),
208
+ )
209
+
210
+
211
+ /**
212
+ * One shadow's pair: a shadow missing from one end is the other's Animatable none, and a start
213
+ * with fewer numbers than its target takes 0s for the missing lengths, appended (CSS's default
214
+ * for an omitted blur or spread). Matching is kind by kind in order, so where they go in the text
215
+ * doesn't matter.
216
+ */
217
+ const shadowPair = (from: Option.Option<string>, to: Option.Option<string>): Pair => {
218
+ const a = Option.getOrElse(from, () => Option.getOrElse(Option.flatMap(to, (t) => zeroOf(t, transparent)), () => "none"))
219
+ const b = Option.getOrElse(to, () => Option.getOrElse(Option.flatMap(from, (f) => zeroOf(f, transparent)), () => "none"))
220
+ const missing = Math.max(0, numberCountOf(b) - numberCountOf(a))
221
+ return [a + Str.repeat(missing)(" 0px"), b]
222
+ }
223
+
224
+ /** One pair, `from` → `to` as given: matching shadow by shadow changed nothing. */
225
+ const isAsGiven =
226
+ (from: Value, to: Value): Predicate.Predicate<ReadonlyArray<Pair>> =>
227
+ (pairs) =>
228
+ Arr.match(pairs, {
229
+ onEmpty: () => false,
230
+ onNonEmpty: ([[a, b], ...rest]) => Bool.and(rest.length === 0, Bool.and(a === from, b === to)),
231
+ })
232
+
233
+ /**
234
+ * A shadow list's pairs, shadow by shadow (#61): `None` unless both ends are strings and matching
235
+ * them shadow by shadow changes anything (more than one shadow, or a start short of numbers).
236
+ */
237
+ const shadows = (key: string, from: Value, to: Value): Option.Option<Prepared> =>
238
+ pipe(
239
+ Option.liftPredicate(key, isShadowKey),
240
+ Option.flatMap(() => Option.all([Option.liftPredicate(from, Predicate.isString), Option.liftPredicate(to, Predicate.isString)])),
241
+ Option.map(([a, b]) => {
242
+ const as = shadowsOf(a)
243
+ const bs = shadowsOf(b)
244
+ return Arr.makeBy(Math.max(as.length, bs.length), (i) => shadowPair(Arr.get(as, i), Arr.get(bs, i)))
245
+ }),
246
+ Option.filter(Predicate.not(isAsGiven(from, to))),
247
+ Option.map((pairs): Prepared => ({ pairs })),
248
+ )
249
+
250
+ /** An end a template can come from, checked cheaply: a string other than an empty end or `auto`. */
251
+ const isTemplateEnd: Predicate.Predicate<Value> = (value) => Predicate.isString(value) && !isEmptyWord(value) && !isAuto(value)
252
+
253
+ /**
254
+ * Whether any rule here can apply: an empty end facing a template, or a shadow key between two
255
+ * strings. Checked first, allocating nothing, so a pair no rule applies to (`opacity` from `0`,
256
+ * `pointer-events` to `none`) never reaches the `Option` chains below: their calls at stream
257
+ * build, through library functions the frame path shares, cost Node's `animateValues` about 5%
258
+ * per frame when it ran after other workloads (#61's bench). Per key, each leg: statements (#460).
259
+ */
260
+ const mayApply = (key: string, from: Value, to: Value): boolean => {
261
+ if (isEmptyEnd(from) && isTemplateEnd(to)) return true
262
+ if (isEmptyEnd(to) && isTemplateEnd(from)) return true
263
+ return isShadowKey(key) && Predicate.isString(from) && Predicate.isString(to)
264
+ }
265
+
266
+ /**
267
+ * `key`'s pairs (#61): from or to its Animatable none when one end is empty, or shadow by shadow
268
+ * for a shadow list. `None` when neither applies: the rule declines.
269
+ */
270
+ const prepare = (key: string, from: Value, to: Value): Option.Option<Prepared> => {
271
+ if (!mayApply(key, from, to)) return Option.none()
272
+ return pipe(
273
+ fromNone(key, from, to),
274
+ Option.orElse(() => toNone(key, from, to)),
275
+ Option.orElse(() => shadows(key, from, to)),
276
+ )
277
+ }
278
+
279
+ // --- The blends the rules hand back: on the per-frame path ---------------------------------------
280
+
281
+ /** One pair's blend, by value. */
282
+ type Blend = (t: number) => number | string
283
+
284
+ /** A hiding pair's blend: its start until the run's last Frame, then its target. Per frame: a statement `if`. */
285
+ const holdUntilDone =
286
+ (from: Value, to: Value): InterpolatorRegistry.KeyBlend =>
287
+ (_t, done) => {
288
+ if (done) return to
289
+ return from
290
+ }
291
+
292
+ /**
293
+ * One pair's blend that writes its target as given on the run's last Frame (#61): a pair from or to
294
+ * its Animatable none rests at `"none"` itself, not its zero. Per frame: a statement `if`.
295
+ */
296
+ const landOn =
297
+ (blend: Blend, to: Value): InterpolatorRegistry.KeyBlend =>
298
+ (t, done) => {
299
+ if (done) return to
300
+ return blend(t)
301
+ }
302
+
303
+ /**
304
+ * A shadow list's blend (#61): each shadow's blend, joined with `", "`, and its target as given on
305
+ * the run's last Frame. Per frame: a counted loop building the string.
306
+ */
307
+ const joinShadows =
308
+ (blends: ReadonlyArray<Blend>, to: Value): InterpolatorRegistry.KeyBlend =>
309
+ (t, done) => {
310
+ if (done) return to
311
+ let out = String(blends[0](t))
312
+ for (let i = 1; i < blends.length; i++) out += ", " + String(blends[i](t))
313
+ return out
314
+ }
315
+
316
+ // --- The rules ------------------------------------------------------------------------------------
317
+
318
+ /** The keys whose turn to `none` or `hidden` waits for the run's last Frame (ADR-0024, decision 4). */
319
+ const VISIBILITY_KEYS: ReadonlySet<string> = new Set(["display", "visibility"])
320
+ const INVISIBLE: ReadonlySet<string> = new Set(["none", "hidden"])
321
+
322
+ /** A `none` or `hidden` value: what a `display` or `visibility` key turns to when it hides. */
323
+ const isInvisible: Predicate.Predicate<Value> = (value) => Predicate.isString(value) && INVISIBLE.has(Str.trim(value))
324
+
325
+ /** A pair that hides: from a visible value to `none` or `hidden`. */
326
+ const hides = (from: Value, to: Value): boolean => Bool.and(isInvisible(to), !isInvisible(from))
327
+
328
+ /** `display` and `visibility`, turning to `none` or `hidden`, show their start until the run's last Frame. */
329
+ export const visibility: InterpolatorRegistry.KeyRule = {
330
+ name: "visibility",
331
+ blend: (key, from, to) => {
332
+ if (!VISIBILITY_KEYS.has(key)) return Option.none()
333
+ if (!hides(from, to)) return Option.none()
334
+ return Option.some(Effect.succeed(holdUntilDone(from, to)))
335
+ },
336
+ }
337
+
338
+ /**
339
+ * An empty end facing a complex value or a colour grows from, or goes back to, its Animatable none,
340
+ * and a shadow list blends shadow by shadow; the run's last Frame writes the target as given.
341
+ */
342
+ export const animatableNone: InterpolatorRegistry.KeyRule = {
343
+ name: "animatable none",
344
+ blend: (key, from, to, resolve) =>
345
+ Option.map(prepare(key, from, to), (prepared) =>
346
+ Effect.map(
347
+ Effect.forEach(prepared.pairs, ([a, b]) => resolve(a, b)),
348
+ (blends): InterpolatorRegistry.KeyBlend =>
349
+ Bool.match(blends.length > 1, {
350
+ onTrue: () => joinShadows(blends, to),
351
+ onFalse: () => landOn(blends[0], to),
352
+ }),
353
+ )),
354
+ }
355
+
356
+ /** The DOM's Key rules, in the order they're asked. */
357
+ export const keyRules: ReadonlyArray<InterpolatorRegistry.KeyRule> = [visibility, animatableNone]