@unrulysystems/native-motion-core 0.1.0-alpha.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.
Files changed (326) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +48 -0
  4. package/dist/clock.cjs +71 -0
  5. package/dist/clock.d.cts +23 -0
  6. package/dist/clock.d.ts +23 -0
  7. package/dist/clock.js +66 -0
  8. package/dist/collect-reads.cjs +68 -0
  9. package/dist/collect-reads.d.cts +23 -0
  10. package/dist/collect-reads.d.ts +23 -0
  11. package/dist/collect-reads.js +62 -0
  12. package/dist/component/boundedArray.cjs +74 -0
  13. package/dist/component/boundedArray.d.cts +25 -0
  14. package/dist/component/boundedArray.d.ts +25 -0
  15. package/dist/component/boundedArray.js +68 -0
  16. package/dist/component/index.cjs +57 -0
  17. package/dist/component/index.d.cts +12 -0
  18. package/dist/component/index.d.ts +12 -0
  19. package/dist/component/index.js +22 -0
  20. package/dist/component/orchestration.cjs +129 -0
  21. package/dist/component/orchestration.d.cts +66 -0
  22. package/dist/component/orchestration.d.ts +66 -0
  23. package/dist/component/orchestration.js +124 -0
  24. package/dist/component/resolve.cjs +104 -0
  25. package/dist/component/resolve.d.cts +11 -0
  26. package/dist/component/resolve.d.ts +11 -0
  27. package/dist/component/resolve.js +97 -0
  28. package/dist/component/transition.cjs +352 -0
  29. package/dist/component/transition.d.cts +33 -0
  30. package/dist/component/transition.d.ts +33 -0
  31. package/dist/component/transition.js +339 -0
  32. package/dist/component/types.cjs +114 -0
  33. package/dist/component/types.d.cts +60 -0
  34. package/dist/component/types.d.ts +60 -0
  35. package/dist/component/types.js +111 -0
  36. package/dist/component/validate.cjs +1015 -0
  37. package/dist/component/validate.d.cts +37 -0
  38. package/dist/component/validate.d.ts +37 -0
  39. package/dist/component/validate.js +1002 -0
  40. package/dist/component/variants.cjs +333 -0
  41. package/dist/component/variants.d.cts +106 -0
  42. package/dist/component/variants.d.ts +106 -0
  43. package/dist/component/variants.js +321 -0
  44. package/dist/config/constants.cjs +41 -0
  45. package/dist/config/constants.d.cts +29 -0
  46. package/dist/config/constants.d.ts +29 -0
  47. package/dist/config/constants.js +38 -0
  48. package/dist/delay.cjs +72 -0
  49. package/dist/delay.d.cts +26 -0
  50. package/dist/delay.d.ts +26 -0
  51. package/dist/delay.js +70 -0
  52. package/dist/derived.cjs +143 -0
  53. package/dist/derived.d.cts +41 -0
  54. package/dist/derived.d.ts +41 -0
  55. package/dist/derived.js +139 -0
  56. package/dist/driver/index.cjs +22 -0
  57. package/dist/driver/index.d.cts +7 -0
  58. package/dist/driver/index.d.ts +7 -0
  59. package/dist/driver/index.js +14 -0
  60. package/dist/driver/keyframeTiming.cjs +130 -0
  61. package/dist/driver/keyframeTiming.d.cts +18 -0
  62. package/dist/driver/keyframeTiming.d.ts +18 -0
  63. package/dist/driver/keyframeTiming.js +124 -0
  64. package/dist/driver/keyframeTimingConfig.cjs +24 -0
  65. package/dist/driver/keyframeTimingConfig.d.cts +5 -0
  66. package/dist/driver/keyframeTimingConfig.d.ts +5 -0
  67. package/dist/driver/keyframeTimingConfig.js +22 -0
  68. package/dist/driver/prepare.cjs +459 -0
  69. package/dist/driver/prepare.d.cts +4 -0
  70. package/dist/driver/prepare.d.ts +4 -0
  71. package/dist/driver/prepare.js +454 -0
  72. package/dist/driver/reference.cjs +762 -0
  73. package/dist/driver/reference.d.cts +53 -0
  74. package/dist/driver/reference.d.ts +53 -0
  75. package/dist/driver/reference.js +757 -0
  76. package/dist/driver/step.cjs +55 -0
  77. package/dist/driver/step.d.cts +9 -0
  78. package/dist/driver/step.d.ts +9 -0
  79. package/dist/driver/step.js +52 -0
  80. package/dist/driver/tiers.cjs +40 -0
  81. package/dist/driver/tiers.d.cts +2 -0
  82. package/dist/driver/tiers.d.ts +2 -0
  83. package/dist/driver/tiers.js +37 -0
  84. package/dist/driver/types.cjs +9 -0
  85. package/dist/driver/types.d.cts +68 -0
  86. package/dist/driver/types.d.ts +68 -0
  87. package/dist/driver/types.js +8 -0
  88. package/dist/external-animation-ledger.cjs +1250 -0
  89. package/dist/external-animation-ledger.d.cts +416 -0
  90. package/dist/external-animation-ledger.d.ts +416 -0
  91. package/dist/external-animation-ledger.js +1243 -0
  92. package/dist/gesture/directionLock.cjs +25 -0
  93. package/dist/gesture/directionLock.d.cts +8 -0
  94. package/dist/gesture/directionLock.d.ts +8 -0
  95. package/dist/gesture/directionLock.js +21 -0
  96. package/dist/gesture/dragConfig.cjs +634 -0
  97. package/dist/gesture/dragConfig.d.cts +298 -0
  98. package/dist/gesture/dragConfig.d.ts +298 -0
  99. package/dist/gesture/dragConfig.js +624 -0
  100. package/dist/gesture/elastic.cjs +44 -0
  101. package/dist/gesture/elastic.d.cts +4 -0
  102. package/dist/gesture/elastic.d.ts +4 -0
  103. package/dist/gesture/elastic.js +39 -0
  104. package/dist/gesture/handoffSession.cjs +161 -0
  105. package/dist/gesture/handoffSession.d.cts +35 -0
  106. package/dist/gesture/handoffSession.d.ts +35 -0
  107. package/dist/gesture/handoffSession.js +158 -0
  108. package/dist/gesture/index.cjs +37 -0
  109. package/dist/gesture/index.d.cts +12 -0
  110. package/dist/gesture/index.d.ts +12 -0
  111. package/dist/gesture/index.js +11 -0
  112. package/dist/gesture/projection.cjs +95 -0
  113. package/dist/gesture/projection.d.cts +7 -0
  114. package/dist/gesture/projection.d.ts +7 -0
  115. package/dist/gesture/projection.js +87 -0
  116. package/dist/gesture/session.cjs +162 -0
  117. package/dist/gesture/session.d.cts +29 -0
  118. package/dist/gesture/session.d.ts +29 -0
  119. package/dist/gesture/session.js +158 -0
  120. package/dist/gesture/types.cjs +5 -0
  121. package/dist/gesture/types.d.cts +13 -0
  122. package/dist/gesture/types.d.ts +13 -0
  123. package/dist/gesture/types.js +4 -0
  124. package/dist/gesture/viewportConstraints.cjs +38 -0
  125. package/dist/gesture/viewportConstraints.d.cts +6 -0
  126. package/dist/gesture/viewportConstraints.d.ts +6 -0
  127. package/dist/gesture/viewportConstraints.js +34 -0
  128. package/dist/graph.cjs +226 -0
  129. package/dist/graph.d.cts +2 -0
  130. package/dist/graph.d.ts +2 -0
  131. package/dist/graph.js +223 -0
  132. package/dist/index.cjs +140 -0
  133. package/dist/index.d.cts +33 -0
  134. package/dist/index.d.ts +33 -0
  135. package/dist/index.js +70 -0
  136. package/dist/inertia.cjs +214 -0
  137. package/dist/inertia.d.cts +53 -0
  138. package/dist/inertia.d.ts +53 -0
  139. package/dist/inertia.js +212 -0
  140. package/dist/instant.cjs +66 -0
  141. package/dist/instant.d.cts +16 -0
  142. package/dist/instant.d.ts +16 -0
  143. package/dist/instant.js +63 -0
  144. package/dist/internal-driver.cjs +81 -0
  145. package/dist/internal-driver.d.cts +19 -0
  146. package/dist/internal-driver.d.ts +19 -0
  147. package/dist/internal-driver.js +35 -0
  148. package/dist/keyframes.cjs +191 -0
  149. package/dist/keyframes.d.cts +13 -0
  150. package/dist/keyframes.d.ts +13 -0
  151. package/dist/keyframes.js +188 -0
  152. package/dist/layout/commitDetector.cjs +67 -0
  153. package/dist/layout/commitDetector.d.cts +21 -0
  154. package/dist/layout/commitDetector.d.ts +21 -0
  155. package/dist/layout/commitDetector.js +64 -0
  156. package/dist/layout/compose.cjs +67 -0
  157. package/dist/layout/compose.d.cts +30 -0
  158. package/dist/layout/compose.d.ts +30 -0
  159. package/dist/layout/compose.js +65 -0
  160. package/dist/layout/constants.cjs +13 -0
  161. package/dist/layout/constants.d.cts +6 -0
  162. package/dist/layout/constants.d.ts +6 -0
  163. package/dist/layout/constants.js +10 -0
  164. package/dist/layout/identity.cjs +700 -0
  165. package/dist/layout/identity.d.cts +67 -0
  166. package/dist/layout/identity.d.ts +67 -0
  167. package/dist/layout/identity.js +698 -0
  168. package/dist/layout/index.cjs +36 -0
  169. package/dist/layout/index.d.cts +18 -0
  170. package/dist/layout/index.d.ts +18 -0
  171. package/dist/layout/index.js +13 -0
  172. package/dist/layout/measure.cjs +81 -0
  173. package/dist/layout/measure.d.cts +43 -0
  174. package/dist/layout/measure.d.ts +43 -0
  175. package/dist/layout/measure.js +78 -0
  176. package/dist/layout/projection.cjs +99 -0
  177. package/dist/layout/projection.d.cts +23 -0
  178. package/dist/layout/projection.d.ts +23 -0
  179. package/dist/layout/projection.js +97 -0
  180. package/dist/layout/scroll.cjs +27 -0
  181. package/dist/layout/scroll.d.cts +13 -0
  182. package/dist/layout/scroll.d.ts +13 -0
  183. package/dist/layout/scroll.js +24 -0
  184. package/dist/layout/session.cjs +207 -0
  185. package/dist/layout/session.d.cts +73 -0
  186. package/dist/layout/session.d.ts +73 -0
  187. package/dist/layout/session.js +205 -0
  188. package/dist/layout/tree.cjs +826 -0
  189. package/dist/layout/tree.d.cts +70 -0
  190. package/dist/layout/tree.d.ts +70 -0
  191. package/dist/layout/tree.js +823 -0
  192. package/dist/layout/types.cjs +36 -0
  193. package/dist/layout/types.d.cts +31 -0
  194. package/dist/layout/types.d.ts +31 -0
  195. package/dist/layout/types.js +35 -0
  196. package/dist/motion-arc.cjs +184 -0
  197. package/dist/motion-arc.d.cts +78 -0
  198. package/dist/motion-arc.d.ts +78 -0
  199. package/dist/motion-arc.js +183 -0
  200. package/dist/motion-mix.cjs +205 -0
  201. package/dist/motion-mix.d.cts +3 -0
  202. package/dist/motion-mix.d.ts +3 -0
  203. package/dist/motion-mix.js +202 -0
  204. package/dist/motion-value-driver-port.cjs +636 -0
  205. package/dist/motion-value-driver-port.d.cts +406 -0
  206. package/dist/motion-value-driver-port.d.ts +406 -0
  207. package/dist/motion-value-driver-port.js +627 -0
  208. package/dist/motion-value.cjs +189 -0
  209. package/dist/motion-value.d.cts +51 -0
  210. package/dist/motion-value.d.ts +51 -0
  211. package/dist/motion-value.js +185 -0
  212. package/dist/presence/controller.cjs +657 -0
  213. package/dist/presence/controller.d.cts +6 -0
  214. package/dist/presence/controller.d.ts +6 -0
  215. package/dist/presence/controller.js +652 -0
  216. package/dist/presence/index.cjs +19 -0
  217. package/dist/presence/index.d.cts +4 -0
  218. package/dist/presence/index.d.ts +4 -0
  219. package/dist/presence/index.js +12 -0
  220. package/dist/presence/machine.cjs +50 -0
  221. package/dist/presence/machine.d.cts +10 -0
  222. package/dist/presence/machine.d.ts +10 -0
  223. package/dist/presence/machine.js +46 -0
  224. package/dist/presence/types.cjs +6 -0
  225. package/dist/presence/types.d.cts +32 -0
  226. package/dist/presence/types.d.ts +32 -0
  227. package/dist/presence/types.js +5 -0
  228. package/dist/repeat.cjs +311 -0
  229. package/dist/repeat.d.cts +174 -0
  230. package/dist/repeat.d.ts +174 -0
  231. package/dist/repeat.js +300 -0
  232. package/dist/spring.cjs +128 -0
  233. package/dist/spring.d.cts +18 -0
  234. package/dist/spring.d.ts +18 -0
  235. package/dist/spring.js +125 -0
  236. package/dist/subscriptions.cjs +74 -0
  237. package/dist/subscriptions.d.cts +19 -0
  238. package/dist/subscriptions.d.ts +19 -0
  239. package/dist/subscriptions.js +69 -0
  240. package/dist/subset/index.cjs +23 -0
  241. package/dist/subset/index.d.cts +6 -0
  242. package/dist/subset/index.d.ts +6 -0
  243. package/dist/subset/index.js +13 -0
  244. package/dist/subset/normalize.cjs +90 -0
  245. package/dist/subset/normalize.d.cts +10 -0
  246. package/dist/subset/normalize.d.ts +10 -0
  247. package/dist/subset/normalize.js +85 -0
  248. package/dist/subset/registry.cjs +259 -0
  249. package/dist/subset/registry.d.cts +25 -0
  250. package/dist/subset/registry.d.ts +25 -0
  251. package/dist/subset/registry.js +256 -0
  252. package/dist/subset/resolve.cjs +98 -0
  253. package/dist/subset/resolve.d.cts +24 -0
  254. package/dist/subset/resolve.d.ts +24 -0
  255. package/dist/subset/resolve.js +90 -0
  256. package/dist/timing.cjs +206 -0
  257. package/dist/timing.d.cts +17 -0
  258. package/dist/timing.d.ts +17 -0
  259. package/dist/timing.js +201 -0
  260. package/dist/transformTemplate.cjs +219 -0
  261. package/dist/transformTemplate.d.cts +29 -0
  262. package/dist/transformTemplate.d.ts +29 -0
  263. package/dist/transformTemplate.js +215 -0
  264. package/dist/transition.cjs +231 -0
  265. package/dist/transition.d.cts +89 -0
  266. package/dist/transition.d.ts +89 -0
  267. package/dist/transition.js +223 -0
  268. package/dist/types.cjs +4 -0
  269. package/dist/types.d.cts +175 -0
  270. package/dist/types.d.ts +175 -0
  271. package/dist/types.js +3 -0
  272. package/dist/value-types/color.cjs +220 -0
  273. package/dist/value-types/color.d.cts +19 -0
  274. package/dist/value-types/color.d.ts +19 -0
  275. package/dist/value-types/color.js +218 -0
  276. package/dist/value-types/complex.cjs +160 -0
  277. package/dist/value-types/complex.d.cts +19 -0
  278. package/dist/value-types/complex.d.ts +19 -0
  279. package/dist/value-types/complex.js +155 -0
  280. package/dist/value-types/constants.cjs +15 -0
  281. package/dist/value-types/constants.d.cts +6 -0
  282. package/dist/value-types/constants.d.ts +6 -0
  283. package/dist/value-types/constants.js +12 -0
  284. package/dist/value-types/discrete.cjs +62 -0
  285. package/dist/value-types/discrete.d.cts +4 -0
  286. package/dist/value-types/discrete.d.ts +4 -0
  287. package/dist/value-types/discrete.js +56 -0
  288. package/dist/value-types/index.cjs +59 -0
  289. package/dist/value-types/index.d.cts +14 -0
  290. package/dist/value-types/index.d.ts +14 -0
  291. package/dist/value-types/index.js +24 -0
  292. package/dist/value-types/measure-resolve.cjs +325 -0
  293. package/dist/value-types/measure-resolve.d.cts +91 -0
  294. package/dist/value-types/measure-resolve.d.ts +91 -0
  295. package/dist/value-types/measure-resolve.js +313 -0
  296. package/dist/value-types/mix.cjs +90 -0
  297. package/dist/value-types/mix.d.cts +27 -0
  298. package/dist/value-types/mix.d.ts +27 -0
  299. package/dist/value-types/mix.js +85 -0
  300. package/dist/value-types/named-colors.cjs +61 -0
  301. package/dist/value-types/named-colors.d.cts +2 -0
  302. package/dist/value-types/named-colors.d.ts +2 -0
  303. package/dist/value-types/named-colors.js +58 -0
  304. package/dist/value-types/numeric.cjs +86 -0
  305. package/dist/value-types/numeric.d.cts +16 -0
  306. package/dist/value-types/numeric.d.ts +16 -0
  307. package/dist/value-types/numeric.js +79 -0
  308. package/dist/worklet-layout/config/constants.js +39 -0
  309. package/dist/worklet-layout/layout/constants.js +11 -0
  310. package/dist/worklet-layout/layout/identity.js +699 -0
  311. package/dist/worklet-layout/layout/projection.js +98 -0
  312. package/dist/worklet-layout/layout/session.js +206 -0
  313. package/dist/worklet-layout/layout/tree.js +824 -0
  314. package/dist/worklet-layout/layout/types.js +36 -0
  315. package/dist/worklet-layout/spring.js +125 -0
  316. package/dist/worklet-layout/timing.js +201 -0
  317. package/dist/worklet-layout/transition.js +223 -0
  318. package/dist/worklet-layout.cjs +47 -0
  319. package/dist/worklet-layout.d.cts +15 -0
  320. package/dist/worklet-layout.d.ts +15 -0
  321. package/dist/worklet-layout.js +28 -0
  322. package/dist/wrap.cjs +7 -0
  323. package/dist/wrap.d.cts +1 -0
  324. package/dist/wrap.d.ts +1 -0
  325. package/dist/wrap.js +4 -0
  326. package/package.json +45 -0
@@ -0,0 +1,174 @@
1
+ import type { Generator } from "./types.cjs";
2
+ export type RepeatType = 'loop' | 'reverse' | 'mirror';
3
+ /**
4
+ * How an ODD iteration plays. `mirror` carries its own second generator rather than reflecting the
5
+ * first: the pin builds it from the same factory with REVERSED KEYFRAMES and a NEGATED seed
6
+ * velocity (`JSAnimation:147-153`), and `times`/`ease` pass through untouched — so a mirrored leg is
7
+ * genuinely a different trajectory, not a time-reflection of the forward one. Carrying the
8
+ * generator in the variant makes a mirror fold without its second generator UNREPRESENTABLE, which
9
+ * is what keeps this frame-path module free of a runtime guard it could only report by throwing on
10
+ * the UI runtime.
11
+ */
12
+ export type RepeatLeg = {
13
+ readonly type: 'loop';
14
+ } | {
15
+ readonly type: 'reverse';
16
+ } | {
17
+ readonly type: 'mirror';
18
+ readonly mirrored: Generator;
19
+ };
20
+ export interface RepeatFoldConfig {
21
+ /** Additional plays after the first. A non-negative integer, or `Infinity` for endless. */
22
+ readonly repeat: number;
23
+ readonly leg: RepeatLeg;
24
+ /** Dead time appended to each iteration, in MILLISECONDS (the public seconds convert upstream). */
25
+ readonly repeatDelayMs: number;
26
+ /**
27
+ * One play's length in ms — the pin's `calculatedDuration`: a tween/keyframe array's authored
28
+ * duration, and for a PHYSICS spring the `calcIterationDurationMs` scan below.
29
+ */
30
+ readonly iterationDurationMs: number;
31
+ }
32
+ export interface RepeatFoldGeometry {
33
+ /** One iteration INCLUDING its trailing repeat delay (the pin's `resolvedDuration`). */
34
+ readonly resolvedDurationMs: number;
35
+ /** The whole fold; `Infinity` for an endless repeat (the pin's `totalDuration`). */
36
+ readonly totalDurationMs: number;
37
+ }
38
+ export declare const ITERATION_SCAN_STEP_MS = 50;
39
+ export declare const MAX_ITERATION_DURATION_MS = 20000;
40
+ /**
41
+ * The iteration length of a generator that does not know its own duration (a physics or
42
+ * visualDuration spring — the pin's `calculatedDuration === null` case). Samples on the pin's 50ms
43
+ * grid until the trajectory reports done, and reports `Infinity` at the cap rather than scanning
44
+ * forever. Safe to run against a live generator: core's generators are pure functions of elapsed
45
+ * time (they latch no state, only reuse their sample record).
46
+ */
47
+ export declare function calcIterationDurationMs(generator: Generator): number;
48
+ /**
49
+ * Refuse a fold the pin cannot execute (packet L3). The fold divides by the RESOLVED iteration
50
+ * length (play + repeatDelay), so a zero-length play with NOTHING separating it yields NaN at the
51
+ * pin and is refused here. A zero-length play separated by a `repeatDelay` is a different shape:
52
+ * the pin totals the delay gaps around the zero-length plays (`JSAnimation.initAnimation` —
53
+ * `totalDuration = calculatedDuration * (repeat + 1) + repeatDelay * repeat`), so the fold
54
+ * EXECUTES, holding each play's final keyframe through the gaps (transition-default-selection
55
+ * F5). An unbounded iteration still emits NaN for the rest of the animation and is refused either
56
+ * way. The refusal is CROSS-FIELD — either length is legal without `repeat`, where nothing
57
+ * divides. Returns the loud refusal or null (the `springKeyframeCountRefusal` shape: this module
58
+ * crosses to the UI runtime, where a throw is a bare `std::terminate`).
59
+ */
60
+ export declare function repeatIterationRefusal(iterationDurationMs: number, repeat: number, repeatDelayMs?: number): Error | null;
61
+ /**
62
+ * WHICH measurement decides a fold's iteration length. This is the rule that has now been
63
+ * re-derived, and got wrong, four times — three parallel copies of it exist by necessity
64
+ * (validation, the reference backend, the worklet backend), and every round a copy drifted:
65
+ *
66
+ * round 1 validation used `type==='tween' || duration || ease` -> measured springs as tweens
67
+ * round 2 validation used "physics keys present" -> measured tweens as springs
68
+ * round 3 validation added the tween short-circuit but stayed target-BLIND -> disagreed with
69
+ * both backends for every keyframe-ARRAY target, because the lane depends on the TARGET
70
+ * SHAPE as well as the transition
71
+ *
72
+ * So the decision itself lives here, once, over primitives — the three callers differ only in the
73
+ * types they hold (`Transition`, the driver's `PropTransition`, the UI runtime's plain-data
74
+ * `PlainTransition`), never in the rule. Worklet-safe by construction: primitives in, string out.
75
+ *
76
+ * `targetIsNumeric` is the load-bearing parameter validation cannot supply, because the boundary
77
+ * validator sees a transition and no target. That is not a gap to paper over — it is why validation
78
+ * must evaluate the rule for EVERY target shape and refuse only when they agree (REQ-TIMING-003).
79
+ *
80
+ * INERTIA is not a lane here. It has no repeatable iteration at all, and each caller must already
81
+ * narrow it off its own union before this point — so an `isInertia` parameter would have been dead
82
+ * at all three call sites while implying the rule covered a case it does not (round-5 review MINOR).
83
+ * Callers return early; this answers only the question it can actually decide.
84
+ */
85
+ export type IterationMeasurementLane = 'keyframes' | 'timing' | 'spring';
86
+ export declare function iterationMeasurementLane(targetIsNumeric: boolean, isExplicitSpring: boolean, isTween: boolean): IterationMeasurementLane;
87
+ /**
88
+ * The refusal's identity, shared by BOTH backends (T18-a fix-up, review MAJOR).
89
+ *
90
+ * `repeatIterationRefusal` returns a bare `Error` because it crosses to the UI runtime. The
91
+ * reference backend re-wraps it as an `InvalidTransitionError`, which names the offending property
92
+ * instead of surfacing an anonymous driver-lane fault. The worklet backend cannot: it must not
93
+ * construct a core class inside a worklet, and the lane marshals only `{name, message}` to RN
94
+ * anyway, so the class identity could not survive the crossing even if it could be built.
95
+ *
96
+ * What CAN be identical across the crossing is exactly what the crossing carries — the name and the
97
+ * message. Both backends therefore build them here, so a developer sees one refusal whichever
98
+ * engine they are on, and `repeatRefusalParity` pins the two against each other.
99
+ */
100
+ export declare const REPEAT_REFUSAL_NAME = "InvalidTransitionError";
101
+ /** The offending property a fold refusal names — `InvalidTransitionError.key` on the JS backend. */
102
+ export declare const REPEAT_REFUSAL_KEY = "repeat";
103
+ /**
104
+ * A fold refusal, as a CONSUMER can recognize it after any transport.
105
+ *
106
+ * The severity router used `error instanceof InvalidTransitionError && error.key === 'repeat'`,
107
+ * which is decidable only on the reference backend: the UI runtime cannot construct a core class,
108
+ * and the crossing marshals plain data, so the shipping backend's refusal arrived as a bare `Error`
109
+ * and the router silently stopped firing (T18-a round-9 review BLOCKING 1). Class identity is not
110
+ * available across the crossing, so the recognition contract cannot be built on it — it is
111
+ * STRUCTURAL by necessity, and it lives here, beside the name and the message it completes, so the
112
+ * whole identity is one thing that one floor can hold.
113
+ *
114
+ * `isRepeatFoldRefusalIdentity` is the two-field core, so the UI lane — which holds the refusal as a
115
+ * marshalled `{name, message, key}` record, never an `Error` — decides it with the SAME rule the
116
+ * consumer applies to the rethrown error rather than a second copy that can drift.
117
+ */
118
+ export declare function isRepeatFoldRefusalIdentity(name: unknown, key: unknown): boolean;
119
+ export declare function isRepeatFoldRefusal(error: unknown): error is Error & {
120
+ readonly key: string;
121
+ };
122
+ export declare function repeatRefusalMessage(repeat: number, reason: string): string;
123
+ /**
124
+ * The fold's geometry, verbatim from the pin (`JSAnimation.initAnimation:169-170`). The trailing
125
+ * delay of the LAST iteration is subtracted: a repeat delay separates plays, it does not extend
126
+ * the animation past its final one.
127
+ */
128
+ export declare function repeatFoldGeometry(config: {
129
+ readonly repeat: number;
130
+ readonly repeatDelayMs: number;
131
+ readonly iterationDurationMs: number;
132
+ }): RepeatFoldGeometry;
133
+ /**
134
+ * Wrap `base` in the repeat fold (REQ-TIMING-003). The returned generator is a `Generator` like any
135
+ * other — the driver steps it without knowing it repeats.
136
+ *
137
+ * Two laws diverge deliberately from a naive port of the pin's `tick`:
138
+ *
139
+ * - A REVERSED leg negates the reported velocity. Our `GeneratorSample` carries the ANALYTIC
140
+ * velocity that the C1 retarget seam and `liveFor` read (REQ-SPRING-009 / REQ-DRIVER-025),
141
+ * whereas the pin's user-visible velocity is a finite-diff of the value — correctly signed by
142
+ * construction even though its generator's internal velocity on a reversed leg is not.
143
+ * Reproducing the pin's internal sign would seed a mid-reverse retarget in the wrong direction.
144
+ * - `done` follows the fold's `totalDuration`, never the base trajectory's own settle. The base
145
+ * settles once per iteration; only the last one ends the animation, and an endless repeat is
146
+ * never done at all (packet L5 — such an element intentionally never quiesces).
147
+ */
148
+ export declare function repeatGenerator(base: Generator, config: RepeatFoldConfig): Generator;
149
+ /**
150
+ * Build the folded generator for a repeated transition — the ONE implementation every driver seam
151
+ * calls (both drivers' start AND retarget seams). Sharing this call does not by itself prevent
152
+ * skew: the fix-up review found both backends sharing it while both omitted it from retarget.
153
+ * Parity is held by the executing floor, `driverParity.differential.test.ts`. Each driver supplies
154
+ * only what it alone knows:
155
+ *
156
+ * - `base` / `leg` — its own already-CONSTRUCTED generators. The mirrored trajectory rides inside
157
+ * the `mirror` leg (origin/target swapped, seed velocity negated, keyframe list reversed), so a
158
+ * mirror fold without one stays unrepresentable.
159
+ * - `knownIterationDurationMs` — the transition's own resolved duration when it has one (a tween,
160
+ * a keyframe array, or a duration-resolved spring, mirroring the pin's `calculatedDuration`), or
161
+ * null for a physics/visualDuration spring, whose length is measured by the scan. A known ZERO
162
+ * is honored exactly: an authored `duration: 0` under a repeatDelay fold has genuinely
163
+ * zero-length plays (transition-default-selection F5) — scanning the base instead would invent
164
+ * a 50ms iteration the pin does not have.
165
+ *
166
+ * Generators are passed as VALUES, never as a factory callback: synchronously invoking a captured
167
+ * function parameter on the UI runtime is the wrong-runtime crash class, and `check:worklet-closures`
168
+ * fails closed on it. Both drivers therefore build their own legs eagerly and hand them over.
169
+ *
170
+ * Returns the folded generator, or the loud refusal for a fold the pin cannot execute (L3). The
171
+ * caller routes that refusal through its severity boundary — this never throws, because it runs on
172
+ * the UI runtime where a throw is a bare `std::terminate`.
173
+ */
174
+ export declare function buildRepeatedGenerator(repeat: number, leg: RepeatLeg, repeatDelayMs: number, base: Generator, knownIterationDurationMs: number | null): Generator | Error;
@@ -0,0 +1,174 @@
1
+ import type { Generator } from "./types.js";
2
+ export type RepeatType = 'loop' | 'reverse' | 'mirror';
3
+ /**
4
+ * How an ODD iteration plays. `mirror` carries its own second generator rather than reflecting the
5
+ * first: the pin builds it from the same factory with REVERSED KEYFRAMES and a NEGATED seed
6
+ * velocity (`JSAnimation:147-153`), and `times`/`ease` pass through untouched — so a mirrored leg is
7
+ * genuinely a different trajectory, not a time-reflection of the forward one. Carrying the
8
+ * generator in the variant makes a mirror fold without its second generator UNREPRESENTABLE, which
9
+ * is what keeps this frame-path module free of a runtime guard it could only report by throwing on
10
+ * the UI runtime.
11
+ */
12
+ export type RepeatLeg = {
13
+ readonly type: 'loop';
14
+ } | {
15
+ readonly type: 'reverse';
16
+ } | {
17
+ readonly type: 'mirror';
18
+ readonly mirrored: Generator;
19
+ };
20
+ export interface RepeatFoldConfig {
21
+ /** Additional plays after the first. A non-negative integer, or `Infinity` for endless. */
22
+ readonly repeat: number;
23
+ readonly leg: RepeatLeg;
24
+ /** Dead time appended to each iteration, in MILLISECONDS (the public seconds convert upstream). */
25
+ readonly repeatDelayMs: number;
26
+ /**
27
+ * One play's length in ms — the pin's `calculatedDuration`: a tween/keyframe array's authored
28
+ * duration, and for a PHYSICS spring the `calcIterationDurationMs` scan below.
29
+ */
30
+ readonly iterationDurationMs: number;
31
+ }
32
+ export interface RepeatFoldGeometry {
33
+ /** One iteration INCLUDING its trailing repeat delay (the pin's `resolvedDuration`). */
34
+ readonly resolvedDurationMs: number;
35
+ /** The whole fold; `Infinity` for an endless repeat (the pin's `totalDuration`). */
36
+ readonly totalDurationMs: number;
37
+ }
38
+ export declare const ITERATION_SCAN_STEP_MS = 50;
39
+ export declare const MAX_ITERATION_DURATION_MS = 20000;
40
+ /**
41
+ * The iteration length of a generator that does not know its own duration (a physics or
42
+ * visualDuration spring — the pin's `calculatedDuration === null` case). Samples on the pin's 50ms
43
+ * grid until the trajectory reports done, and reports `Infinity` at the cap rather than scanning
44
+ * forever. Safe to run against a live generator: core's generators are pure functions of elapsed
45
+ * time (they latch no state, only reuse their sample record).
46
+ */
47
+ export declare function calcIterationDurationMs(generator: Generator): number;
48
+ /**
49
+ * Refuse a fold the pin cannot execute (packet L3). The fold divides by the RESOLVED iteration
50
+ * length (play + repeatDelay), so a zero-length play with NOTHING separating it yields NaN at the
51
+ * pin and is refused here. A zero-length play separated by a `repeatDelay` is a different shape:
52
+ * the pin totals the delay gaps around the zero-length plays (`JSAnimation.initAnimation` —
53
+ * `totalDuration = calculatedDuration * (repeat + 1) + repeatDelay * repeat`), so the fold
54
+ * EXECUTES, holding each play's final keyframe through the gaps (transition-default-selection
55
+ * F5). An unbounded iteration still emits NaN for the rest of the animation and is refused either
56
+ * way. The refusal is CROSS-FIELD — either length is legal without `repeat`, where nothing
57
+ * divides. Returns the loud refusal or null (the `springKeyframeCountRefusal` shape: this module
58
+ * crosses to the UI runtime, where a throw is a bare `std::terminate`).
59
+ */
60
+ export declare function repeatIterationRefusal(iterationDurationMs: number, repeat: number, repeatDelayMs?: number): Error | null;
61
+ /**
62
+ * WHICH measurement decides a fold's iteration length. This is the rule that has now been
63
+ * re-derived, and got wrong, four times — three parallel copies of it exist by necessity
64
+ * (validation, the reference backend, the worklet backend), and every round a copy drifted:
65
+ *
66
+ * round 1 validation used `type==='tween' || duration || ease` -> measured springs as tweens
67
+ * round 2 validation used "physics keys present" -> measured tweens as springs
68
+ * round 3 validation added the tween short-circuit but stayed target-BLIND -> disagreed with
69
+ * both backends for every keyframe-ARRAY target, because the lane depends on the TARGET
70
+ * SHAPE as well as the transition
71
+ *
72
+ * So the decision itself lives here, once, over primitives — the three callers differ only in the
73
+ * types they hold (`Transition`, the driver's `PropTransition`, the UI runtime's plain-data
74
+ * `PlainTransition`), never in the rule. Worklet-safe by construction: primitives in, string out.
75
+ *
76
+ * `targetIsNumeric` is the load-bearing parameter validation cannot supply, because the boundary
77
+ * validator sees a transition and no target. That is not a gap to paper over — it is why validation
78
+ * must evaluate the rule for EVERY target shape and refuse only when they agree (REQ-TIMING-003).
79
+ *
80
+ * INERTIA is not a lane here. It has no repeatable iteration at all, and each caller must already
81
+ * narrow it off its own union before this point — so an `isInertia` parameter would have been dead
82
+ * at all three call sites while implying the rule covered a case it does not (round-5 review MINOR).
83
+ * Callers return early; this answers only the question it can actually decide.
84
+ */
85
+ export type IterationMeasurementLane = 'keyframes' | 'timing' | 'spring';
86
+ export declare function iterationMeasurementLane(targetIsNumeric: boolean, isExplicitSpring: boolean, isTween: boolean): IterationMeasurementLane;
87
+ /**
88
+ * The refusal's identity, shared by BOTH backends (T18-a fix-up, review MAJOR).
89
+ *
90
+ * `repeatIterationRefusal` returns a bare `Error` because it crosses to the UI runtime. The
91
+ * reference backend re-wraps it as an `InvalidTransitionError`, which names the offending property
92
+ * instead of surfacing an anonymous driver-lane fault. The worklet backend cannot: it must not
93
+ * construct a core class inside a worklet, and the lane marshals only `{name, message}` to RN
94
+ * anyway, so the class identity could not survive the crossing even if it could be built.
95
+ *
96
+ * What CAN be identical across the crossing is exactly what the crossing carries — the name and the
97
+ * message. Both backends therefore build them here, so a developer sees one refusal whichever
98
+ * engine they are on, and `repeatRefusalParity` pins the two against each other.
99
+ */
100
+ export declare const REPEAT_REFUSAL_NAME = "InvalidTransitionError";
101
+ /** The offending property a fold refusal names — `InvalidTransitionError.key` on the JS backend. */
102
+ export declare const REPEAT_REFUSAL_KEY = "repeat";
103
+ /**
104
+ * A fold refusal, as a CONSUMER can recognize it after any transport.
105
+ *
106
+ * The severity router used `error instanceof InvalidTransitionError && error.key === 'repeat'`,
107
+ * which is decidable only on the reference backend: the UI runtime cannot construct a core class,
108
+ * and the crossing marshals plain data, so the shipping backend's refusal arrived as a bare `Error`
109
+ * and the router silently stopped firing (T18-a round-9 review BLOCKING 1). Class identity is not
110
+ * available across the crossing, so the recognition contract cannot be built on it — it is
111
+ * STRUCTURAL by necessity, and it lives here, beside the name and the message it completes, so the
112
+ * whole identity is one thing that one floor can hold.
113
+ *
114
+ * `isRepeatFoldRefusalIdentity` is the two-field core, so the UI lane — which holds the refusal as a
115
+ * marshalled `{name, message, key}` record, never an `Error` — decides it with the SAME rule the
116
+ * consumer applies to the rethrown error rather than a second copy that can drift.
117
+ */
118
+ export declare function isRepeatFoldRefusalIdentity(name: unknown, key: unknown): boolean;
119
+ export declare function isRepeatFoldRefusal(error: unknown): error is Error & {
120
+ readonly key: string;
121
+ };
122
+ export declare function repeatRefusalMessage(repeat: number, reason: string): string;
123
+ /**
124
+ * The fold's geometry, verbatim from the pin (`JSAnimation.initAnimation:169-170`). The trailing
125
+ * delay of the LAST iteration is subtracted: a repeat delay separates plays, it does not extend
126
+ * the animation past its final one.
127
+ */
128
+ export declare function repeatFoldGeometry(config: {
129
+ readonly repeat: number;
130
+ readonly repeatDelayMs: number;
131
+ readonly iterationDurationMs: number;
132
+ }): RepeatFoldGeometry;
133
+ /**
134
+ * Wrap `base` in the repeat fold (REQ-TIMING-003). The returned generator is a `Generator` like any
135
+ * other — the driver steps it without knowing it repeats.
136
+ *
137
+ * Two laws diverge deliberately from a naive port of the pin's `tick`:
138
+ *
139
+ * - A REVERSED leg negates the reported velocity. Our `GeneratorSample` carries the ANALYTIC
140
+ * velocity that the C1 retarget seam and `liveFor` read (REQ-SPRING-009 / REQ-DRIVER-025),
141
+ * whereas the pin's user-visible velocity is a finite-diff of the value — correctly signed by
142
+ * construction even though its generator's internal velocity on a reversed leg is not.
143
+ * Reproducing the pin's internal sign would seed a mid-reverse retarget in the wrong direction.
144
+ * - `done` follows the fold's `totalDuration`, never the base trajectory's own settle. The base
145
+ * settles once per iteration; only the last one ends the animation, and an endless repeat is
146
+ * never done at all (packet L5 — such an element intentionally never quiesces).
147
+ */
148
+ export declare function repeatGenerator(base: Generator, config: RepeatFoldConfig): Generator;
149
+ /**
150
+ * Build the folded generator for a repeated transition — the ONE implementation every driver seam
151
+ * calls (both drivers' start AND retarget seams). Sharing this call does not by itself prevent
152
+ * skew: the fix-up review found both backends sharing it while both omitted it from retarget.
153
+ * Parity is held by the executing floor, `driverParity.differential.test.ts`. Each driver supplies
154
+ * only what it alone knows:
155
+ *
156
+ * - `base` / `leg` — its own already-CONSTRUCTED generators. The mirrored trajectory rides inside
157
+ * the `mirror` leg (origin/target swapped, seed velocity negated, keyframe list reversed), so a
158
+ * mirror fold without one stays unrepresentable.
159
+ * - `knownIterationDurationMs` — the transition's own resolved duration when it has one (a tween,
160
+ * a keyframe array, or a duration-resolved spring, mirroring the pin's `calculatedDuration`), or
161
+ * null for a physics/visualDuration spring, whose length is measured by the scan. A known ZERO
162
+ * is honored exactly: an authored `duration: 0` under a repeatDelay fold has genuinely
163
+ * zero-length plays (transition-default-selection F5) — scanning the base instead would invent
164
+ * a 50ms iteration the pin does not have.
165
+ *
166
+ * Generators are passed as VALUES, never as a factory callback: synchronously invoking a captured
167
+ * function parameter on the UI runtime is the wrong-runtime crash class, and `check:worklet-closures`
168
+ * fails closed on it. Both drivers therefore build their own legs eagerly and hand them over.
169
+ *
170
+ * Returns the folded generator, or the loud refusal for a fold the pin cannot execute (L3). The
171
+ * caller routes that refusal through its severity boundary — this never throws, because it runs on
172
+ * the UI runtime where a throw is a bare `std::terminate`.
173
+ */
174
+ export declare function buildRepeatedGenerator(repeat: number, leg: RepeatLeg, repeatDelayMs: number, base: Generator, knownIterationDurationMs: number | null): Generator | Error;
package/dist/repeat.js ADDED
@@ -0,0 +1,300 @@
1
+ 'worklet';
2
+ // @frame-path — executes inside the UI-runtime frame step; allocation-gated (REQ-DRIVER-021).
3
+ // T18-a / REQ-TIMING-003 — the repeat elapsed-time FOLD. `repeat`/`repeatType`/`repeatDelay` are not
4
+ // generator features in the pinned Motion: they are a pure `t → t'` map wrapped AROUND a generator
5
+ // (motion-dom@12.42.2 `JSAnimation.initAnimation` + `tick`). This module is that map, so every
6
+ // generator core owns — spring, tween, keyframe array — repeats identically and none of them learns
7
+ // about repetition. Host-agnostic (REQ-CORE-003): relative imports only. Numerics are pinned by
8
+ // per-sample goldens in repeat.test.ts.
9
+ //
10
+ // The fold divides by the RESOLVED iteration length (play + repeatDelay), so it is TOTAL only
11
+ // when that is finite and positive: a zero-length play with no repeatDelay produces NaN at the
12
+ // pin and is refused upstream (`repeatIterationRefusal`), as is an unbounded one. Zero-length
13
+ // plays separated by a repeatDelay are the pin's executable shape (transition-default-selection
14
+ // F5) and fold fine.
15
+ // The pin's `calcGeneratorDuration` scan constants (motion-dom keyframes/calc-duration.ts). The
16
+ // 50ms grid is why a physics spring's iteration length is COARSE — it is the pin's number, not an
17
+ // analytic settle time, and reproducing it exactly is what keeps repeated springs in parity.
18
+ export const ITERATION_SCAN_STEP_MS = 50;
19
+ export const MAX_ITERATION_DURATION_MS = 20_000;
20
+ /**
21
+ * The iteration length of a generator that does not know its own duration (a physics or
22
+ * visualDuration spring — the pin's `calculatedDuration === null` case). Samples on the pin's 50ms
23
+ * grid until the trajectory reports done, and reports `Infinity` at the cap rather than scanning
24
+ * forever. Safe to run against a live generator: core's generators are pure functions of elapsed
25
+ * time (they latch no state, only reuse their sample record).
26
+ */
27
+ // alloc-ok: lifecycle-edge — the scan runs once per animate command, never per frame.
28
+ export function calcIterationDurationMs(generator) {
29
+ let duration = 0;
30
+ let state = generator.sample(duration);
31
+ while (!state.done && duration < MAX_ITERATION_DURATION_MS) {
32
+ duration += ITERATION_SCAN_STEP_MS;
33
+ state = generator.sample(duration);
34
+ }
35
+ return duration >= MAX_ITERATION_DURATION_MS ? Infinity : duration;
36
+ }
37
+ /**
38
+ * Refuse a fold the pin cannot execute (packet L3). The fold divides by the RESOLVED iteration
39
+ * length (play + repeatDelay), so a zero-length play with NOTHING separating it yields NaN at the
40
+ * pin and is refused here. A zero-length play separated by a `repeatDelay` is a different shape:
41
+ * the pin totals the delay gaps around the zero-length plays (`JSAnimation.initAnimation` —
42
+ * `totalDuration = calculatedDuration * (repeat + 1) + repeatDelay * repeat`), so the fold
43
+ * EXECUTES, holding each play's final keyframe through the gaps (transition-default-selection
44
+ * F5). An unbounded iteration still emits NaN for the rest of the animation and is refused either
45
+ * way. The refusal is CROSS-FIELD — either length is legal without `repeat`, where nothing
46
+ * divides. Returns the loud refusal or null (the `springKeyframeCountRefusal` shape: this module
47
+ * crosses to the UI runtime, where a throw is a bare `std::terminate`).
48
+ */
49
+ // alloc-ok: lifecycle-edge — a refusal built at command/validation time, never per frame.
50
+ export function repeatIterationRefusal(iterationDurationMs, repeat, repeatDelayMs = 0) {
51
+ if (repeat <= 0)
52
+ return null;
53
+ if (iterationDurationMs <= 0 && repeatDelayMs <= 0) {
54
+ return new Error('a repeated transition cannot fold a zero-length iteration — `repeat` divides by the ' +
55
+ "iteration's duration, so a zero-duration play repeats nothing (the pinned Motion " +
56
+ 'silently holds the final keyframe instead). Give the transition a duration, or drop ' +
57
+ '`repeat` (REQ-TIMING-003, T18-a L3).');
58
+ }
59
+ if (!Number.isFinite(iterationDurationMs)) {
60
+ return new Error(`a repeated transition cannot fold an iteration that never settles — this trajectory is ` +
61
+ `still moving after ${MAX_ITERATION_DURATION_MS}ms, past the point where the pinned ` +
62
+ 'Motion stops measuring it and starts emitting NaN. Bound the transition (duration, or ' +
63
+ 'stiffer physics), or drop `repeat` (REQ-TIMING-003, T18-a L3).');
64
+ }
65
+ return null;
66
+ }
67
+ export function iterationMeasurementLane(targetIsNumeric, isExplicitSpring, isTween) {
68
+ // A keyframe ARRAY target is played by the keyframes generator unless the transition explicitly
69
+ // asked for a spring — so an untyped or tween-typed transition changes lane with the target.
70
+ if (!targetIsNumeric && !isExplicitSpring)
71
+ return 'keyframes';
72
+ if (isTween)
73
+ return 'timing';
74
+ return 'spring';
75
+ }
76
+ /**
77
+ * The refusal's identity, shared by BOTH backends (T18-a fix-up, review MAJOR).
78
+ *
79
+ * `repeatIterationRefusal` returns a bare `Error` because it crosses to the UI runtime. The
80
+ * reference backend re-wraps it as an `InvalidTransitionError`, which names the offending property
81
+ * instead of surfacing an anonymous driver-lane fault. The worklet backend cannot: it must not
82
+ * construct a core class inside a worklet, and the lane marshals only `{name, message}` to RN
83
+ * anyway, so the class identity could not survive the crossing even if it could be built.
84
+ *
85
+ * What CAN be identical across the crossing is exactly what the crossing carries — the name and the
86
+ * message. Both backends therefore build them here, so a developer sees one refusal whichever
87
+ * engine they are on, and `repeatRefusalParity` pins the two against each other.
88
+ */
89
+ export const REPEAT_REFUSAL_NAME = 'InvalidTransitionError';
90
+ /** The offending property a fold refusal names — `InvalidTransitionError.key` on the JS backend. */
91
+ export const REPEAT_REFUSAL_KEY = 'repeat';
92
+ /**
93
+ * A fold refusal, as a CONSUMER can recognize it after any transport.
94
+ *
95
+ * The severity router used `error instanceof InvalidTransitionError && error.key === 'repeat'`,
96
+ * which is decidable only on the reference backend: the UI runtime cannot construct a core class,
97
+ * and the crossing marshals plain data, so the shipping backend's refusal arrived as a bare `Error`
98
+ * and the router silently stopped firing (T18-a round-9 review BLOCKING 1). Class identity is not
99
+ * available across the crossing, so the recognition contract cannot be built on it — it is
100
+ * STRUCTURAL by necessity, and it lives here, beside the name and the message it completes, so the
101
+ * whole identity is one thing that one floor can hold.
102
+ *
103
+ * `isRepeatFoldRefusalIdentity` is the two-field core, so the UI lane — which holds the refusal as a
104
+ * marshalled `{name, message, key}` record, never an `Error` — decides it with the SAME rule the
105
+ * consumer applies to the rethrown error rather than a second copy that can drift.
106
+ */
107
+ export function isRepeatFoldRefusalIdentity(name, key) {
108
+ return name === REPEAT_REFUSAL_NAME && key === REPEAT_REFUSAL_KEY;
109
+ }
110
+ export function isRepeatFoldRefusal(error) {
111
+ if (typeof error !== 'object' || error === null)
112
+ return false;
113
+ const candidate = error;
114
+ return isRepeatFoldRefusalIdentity(candidate.name, candidate.key);
115
+ }
116
+ // Mirrors `InvalidTransitionError`'s message format for an anonymous component with a numeric
117
+ // value. It is duplicated rather than imported because `validate.ts` is JS-side-only (it reaches
118
+ // the subset registry and the value-type parsers) and this module crosses to the UI runtime. The
119
+ // duplication is held honest by the cross-backend floor, not by review.
120
+ // alloc-ok: lifecycle-edge — a refusal built at command time, never per frame.
121
+ export function repeatRefusalMessage(repeat, reason) {
122
+ return `<Motion.View>: invalid transition option 'repeat' = ${String(repeat)} — ${reason}`;
123
+ }
124
+ /**
125
+ * The fold's geometry, verbatim from the pin (`JSAnimation.initAnimation:169-170`). The trailing
126
+ * delay of the LAST iteration is subtracted: a repeat delay separates plays, it does not extend
127
+ * the animation past its final one.
128
+ */
129
+ // alloc-ok: lifecycle-edge — geometry resolved once per animate command.
130
+ export function repeatFoldGeometry(config) {
131
+ const resolvedDurationMs = config.iterationDurationMs + config.repeatDelayMs;
132
+ return {
133
+ resolvedDurationMs,
134
+ totalDurationMs: resolvedDurationMs * (config.repeat + 1) - config.repeatDelayMs,
135
+ };
136
+ }
137
+ function clamp01(value) {
138
+ return value > 1 ? 1 : value < 0 ? 0 : value;
139
+ }
140
+ // DECLARATION ORDER IS LOAD-BEARING in this module: it ships FILE-tagged, so the workletizer
141
+ // emits every function as a non-hoisted const and a worklet may only reference worklets declared
142
+ // ABOVE it. `buildRepeatedGenerator` calls this, so this comes first. Pinned by
143
+ // check-worklet-forward-refs.test.ts.
144
+ /**
145
+ * Wrap `base` in the repeat fold (REQ-TIMING-003). The returned generator is a `Generator` like any
146
+ * other — the driver steps it without knowing it repeats.
147
+ *
148
+ * Two laws diverge deliberately from a naive port of the pin's `tick`:
149
+ *
150
+ * - A REVERSED leg negates the reported velocity. Our `GeneratorSample` carries the ANALYTIC
151
+ * velocity that the C1 retarget seam and `liveFor` read (REQ-SPRING-009 / REQ-DRIVER-025),
152
+ * whereas the pin's user-visible velocity is a finite-diff of the value — correctly signed by
153
+ * construction even though its generator's internal velocity on a reversed leg is not.
154
+ * Reproducing the pin's internal sign would seed a mid-reverse retarget in the wrong direction.
155
+ * - `done` follows the fold's `totalDuration`, never the base trajectory's own settle. The base
156
+ * settles once per iteration; only the last one ends the animation, and an endless repeat is
157
+ * never done at all (packet L5 — such an element intentionally never quiesces).
158
+ */
159
+ // alloc-ok: lifecycle-edge — construction resolves geometry and builds the ONE reused sample
160
+ // record; sample() below mutates it and allocates nothing (REQ-DRIVER-021).
161
+ export function repeatGenerator(base, config) {
162
+ const { repeat, leg, repeatDelayMs, iterationDurationMs } = config;
163
+ const { resolvedDurationMs, totalDurationMs } = repeatFoldGeometry({
164
+ repeat,
165
+ repeatDelayMs,
166
+ iterationDurationMs,
167
+ });
168
+ const mirrored = leg.type === 'mirror' ? leg.mirrored : null;
169
+ const isReverse = leg.type === 'reverse';
170
+ // The pin's terminal keyframe rule (motion-dom keyframes/get-final.ts), reduced to the only case
171
+ // where it disagrees with where this fold naturally lands:
172
+ //
173
+ // useFirstKeyframe = speed < 0 || (repeat && repeatType !== 'loop' && repeat % 2 === 1)
174
+ //
175
+ // `repeat % 2 === 1` selects exactly the ODD INTEGERS, and `repeatType !== 'loop'` puts BOTH
176
+ // `reverse` and `mirror` in that branch. An integer fold already ENDS on the pin's keyframe by
177
+ // construction — a whole-number progress is corrected to `iterationProgress` 1; a `reverse` leg
178
+ // reflects that to 0 and a `mirror` leg samples the mirrored trajectory at ITS end, which is the
179
+ // base origin, so both odd cases report the pin's FIRST keyframe while a `loop` leg and every
180
+ // EVEN case hold at 1 and report the pin's LAST. So the integer path needs no snap and must not
181
+ // get one. (Measured on the pin, `[0,100]` and `[0,100,30]`, repeats 1-4 × all three types: odd
182
+ // reverse and odd mirror both land on FIRST, everything else on LAST.) A FRACTIONAL repeat is
183
+ // the gap: at forward speed it satisfies no branch of the rule, so the pin commits the LAST keyframe while this
184
+ // fold is mid-play. `speed < 0` has no native producer (drivers accumulate non-negative elapsed
185
+ // only), so that half of the rule is stated and not implemented.
186
+ const snapsAtTerminal = Number.isFinite(repeat) && repeat > 0 && !Number.isInteger(repeat);
187
+ // The pin's snap is NOT part of the fold: `JSAnimation.tick` computes the fold, then applies
188
+ // `getFinalKeyframe` under its `isAnimationFinished` gate
189
+ // (`holdTime === null && (state === 'finished' || (state === 'running' && done))`), and only then
190
+ // calls `finish()` — which never touches the value. A FRESH, non-autoplaying instance never
191
+ // satisfies that gate: it is `paused`, so `holdTime` is non-null and neither disjunct holds, and
192
+ // its `sample()` returns the raw fold at EVERY time. An already-FINISHED instance takes the
193
+ // override at ANY sampled time, mid-play included; a RUNNING one only at or past `totalDuration`,
194
+ // because that disjunct also requires `done`. Measured on `repeat: 1.5 reverse`, whose raw fold is
195
+ // 80 mid-play and 50 at the terminal — paused 80/50, running 80/100, finished 100/100.
196
+ //
197
+ // `gen-repeat-goldens.mjs` builds a FRESH `autoplay: false` instance per sample, so the golden
198
+ // records both layers: `samples` are raw-fold seeks (50 at the terminal) and `terminalValue` comes
199
+ // from playing to the end (100). Keeping `sample` seek-faithful and exposing the commit separately
200
+ // reproduces that split instead of collapsing it; putting the snap inside `sample` broke the
201
+ // pinned `tween-reverse-fractional` samples, which is the golden reporting the wrong layer (H2 F1).
202
+ //
203
+ // The value is the FORWARD base at its end, never the leg the partial play was on: the pin
204
+ // commits `resolvedKeyframes[length - 1]` whatever direction it was travelling, so a `reverse` or
205
+ // `mirror` fold descending toward the origin jumps UP to the target. Core's generators are pure
206
+ // functions of elapsed, so sampling one here is safe and allocation-free at the frame path.
207
+ const finalValue = snapsAtTerminal ? base.sample(resolvedDurationMs).value : undefined;
208
+ // The delay's share of one resolved iteration — the reverse leg's re-bias factor, hoisted so the
209
+ // frame path does no division beyond the progress one.
210
+ const delayFraction = repeatDelayMs / resolvedDurationMs;
211
+ const out = { value: 0, velocity: 0, done: false };
212
+ return {
213
+ finalValue,
214
+ sample(rawElapsedMs) {
215
+ // The pin clamps before folding (`JSAnimation.tick`: `Math.max(timeWithoutDelay, 0)`), and so
216
+ // must this: `floor` of a negative progress is -1, an ODD iteration, so a `reverse` leg would
217
+ // reflect and report the far endpoint at a moment the animation has not begun. Today's
218
+ // drivers only ever accumulate non-negative deltas, but the pin's one producer of negative
219
+ // elapsed is `currentTime - delay` — property-lane `delay`, this packet's successor rung.
220
+ // `Math.max` rather than `rawElapsedMs > 0 ? rawElapsedMs : 0` — they agree on every number
221
+ // and disagree on NaN, which the comparison silently maps to 0 (reporting the trajectory's
222
+ // ORIGIN for a broken clock) while `Math.max` propagates it. A NaN clock is a programmer
223
+ // error, and it must stay loud rather than resolve to a plausible-looking value (review
224
+ // MINOR).
225
+ const elapsedMs = Math.max(rawElapsedMs, 0);
226
+ let elapsed = elapsedMs;
227
+ let frameGenerator = base;
228
+ let reversedLeg = false;
229
+ if (repeat > 0) {
230
+ // Progress across the WHOLE fold in iteration units: 2.5 is halfway through the third play.
231
+ const progress = Math.min(elapsedMs, totalDurationMs) / resolvedDurationMs;
232
+ let currentIteration = Math.floor(progress);
233
+ let iterationProgress = progress % 1;
234
+ // A whole-number progress at or past the first boundary is the END of the PREVIOUS
235
+ // iteration, not the start of the next — otherwise every boundary flickers a frame of the
236
+ // successor's origin.
237
+ if (iterationProgress === 0 && progress >= 1) {
238
+ iterationProgress = 1;
239
+ currentIteration--;
240
+ }
241
+ if (currentIteration > repeat + 1)
242
+ currentIteration = repeat + 1;
243
+ if (currentIteration % 2 !== 0) {
244
+ if (isReverse) {
245
+ iterationProgress = 1 - iterationProgress;
246
+ // Reflecting progress would put the dead time at the START of the reversed play; this
247
+ // subtraction moves it back to the END, so the rule stays uniform across repeat types:
248
+ // every play holds on the value it FINISHED on. A reversed leg therefore begins
249
+ // descending immediately from the iteration's end value and pauses at its start value.
250
+ if (repeatDelayMs > 0)
251
+ iterationProgress -= delayFraction;
252
+ reversedLeg = true;
253
+ }
254
+ else if (mirrored !== null) {
255
+ frameGenerator = mirrored;
256
+ }
257
+ }
258
+ elapsed = clamp01(iterationProgress) * resolvedDurationMs;
259
+ }
260
+ const sample = frameGenerator.sample(elapsed);
261
+ out.value = sample.value;
262
+ out.velocity = reversedLeg ? -sample.velocity : sample.velocity;
263
+ out.done = elapsedMs >= totalDurationMs;
264
+ return out;
265
+ },
266
+ };
267
+ }
268
+ /**
269
+ * Build the folded generator for a repeated transition — the ONE implementation every driver seam
270
+ * calls (both drivers' start AND retarget seams). Sharing this call does not by itself prevent
271
+ * skew: the fix-up review found both backends sharing it while both omitted it from retarget.
272
+ * Parity is held by the executing floor, `driverParity.differential.test.ts`. Each driver supplies
273
+ * only what it alone knows:
274
+ *
275
+ * - `base` / `leg` — its own already-CONSTRUCTED generators. The mirrored trajectory rides inside
276
+ * the `mirror` leg (origin/target swapped, seed velocity negated, keyframe list reversed), so a
277
+ * mirror fold without one stays unrepresentable.
278
+ * - `knownIterationDurationMs` — the transition's own resolved duration when it has one (a tween,
279
+ * a keyframe array, or a duration-resolved spring, mirroring the pin's `calculatedDuration`), or
280
+ * null for a physics/visualDuration spring, whose length is measured by the scan. A known ZERO
281
+ * is honored exactly: an authored `duration: 0` under a repeatDelay fold has genuinely
282
+ * zero-length plays (transition-default-selection F5) — scanning the base instead would invent
283
+ * a 50ms iteration the pin does not have.
284
+ *
285
+ * Generators are passed as VALUES, never as a factory callback: synchronously invoking a captured
286
+ * function parameter on the UI runtime is the wrong-runtime crash class, and `check:worklet-closures`
287
+ * fails closed on it. Both drivers therefore build their own legs eagerly and hand them over.
288
+ *
289
+ * Returns the folded generator, or the loud refusal for a fold the pin cannot execute (L3). The
290
+ * caller routes that refusal through its severity boundary — this never throws, because it runs on
291
+ * the UI runtime where a throw is a bare `std::terminate`.
292
+ */
293
+ // alloc-ok: lifecycle-edge — one fold construction per animate command, never per frame.
294
+ export function buildRepeatedGenerator(repeat, leg, repeatDelayMs, base, knownIterationDurationMs) {
295
+ const iterationDurationMs = knownIterationDurationMs ?? calcIterationDurationMs(base);
296
+ const refusal = repeatIterationRefusal(iterationDurationMs, repeat, repeatDelayMs);
297
+ if (refusal !== null)
298
+ return refusal;
299
+ return repeatGenerator(base, { repeat, leg, repeatDelayMs, iterationDurationMs });
300
+ }