@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,191 @@
1
+ "use strict";
2
+ // @frame-path — executes inside the UI-runtime frame step; allocation-gated (REQ-DRIVER-021).
3
+ // The multi-keyframe timing generator (REQ-API-033, R8). Interpolates THROUGH a keyframe array over
4
+ // `duration`: even `defaultOffset` unless `times`, one `easeInOut` per SEGMENT unless an `ease`
5
+ // array, `keyframes[0]` seeds — or the element's CURRENT value when the first keyframe is `null`
6
+ // (R8-F2). Semantics pinned to motion@12.42.2's generators/keyframes.ts; the cubic-bezier easing is
7
+ // timing.ts's exact BezierEasing port (never a second vocabulary). The reused sample record and the
8
+ // bounded segment scan allocate nothing per frame.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.keyframesGenerator = keyframesGenerator;
11
+ const validate_1 = require("./component/validate.cjs");
12
+ const timing_1 = require("./timing.cjs");
13
+ const keyframeTiming_1 = require("./driver/keyframeTiming.cjs");
14
+ const keyframeTimingConfig_1 = require("./driver/keyframeTimingConfig.cjs");
15
+ const boundedArray_1 = require("./component/boundedArray.cjs");
16
+ const DEFAULT_DURATION_MS = 300; // REQ-TIMING-002 (0.3s), motion's keyframes default
17
+ const VELOCITY_SAMPLE_MS = 5;
18
+ const clamp01 = (v) => Math.max(0, Math.min(1, v));
19
+ const mix = (from, to, p) => from + (to - from) * p;
20
+ function captureArray(value, invalid, nested = false) {
21
+ const capture = nested ? (0, boundedArray_1.captureBoundedCubicBezier)(value) : (0, boundedArray_1.captureBoundedArray)(value);
22
+ if (capture.kind === 'not-array')
23
+ throw invalid((0, boundedArray_1.capturedArrayDescription)(capture), 'must be a real array');
24
+ if (capture.kind === 'invalid-length') {
25
+ throw invalid((0, boundedArray_1.capturedArrayDescription)(capture), nested
26
+ ? 'must be a four-point cubic-bezier tuple (REQ-API-033)'
27
+ : `must have a safe array length between 0 and ${boundedArray_1.MAX_CAPTURED_ARRAY_LENGTH} (REQ-API-033)`);
28
+ }
29
+ const length = capture.values.length;
30
+ const captured = [];
31
+ captured.length = length;
32
+ for (let index = 0; index < length; index++) {
33
+ if (!Object.hasOwn(capture.values, index))
34
+ continue;
35
+ const member = capture.values[index];
36
+ captured[index] =
37
+ !nested && Array.isArray(member) ? captureArray(member, invalid, true) : member;
38
+ }
39
+ return Object.freeze(captured);
40
+ }
41
+ function captureKeyframes(keyframes) {
42
+ const captured = captureArray(keyframes, (value, reason) => new validate_1.InvalidTargetError(undefined, 'keyframes', value, `${reason} (REQ-API-033)`));
43
+ if (captured.length < 2) {
44
+ throw new validate_1.InvalidTargetError(undefined, 'keyframes', captured, 'a keyframe array must have at least two keyframes (REQ-API-033)');
45
+ }
46
+ for (let index = 0; index < captured.length; index++) {
47
+ if (!Object.hasOwn(captured, index)) {
48
+ throw new validate_1.InvalidTargetError(undefined, 'keyframes', captured, `keyframe array is missing index ${index}; sparse keyframe arrays are invalid (REQ-API-033)`);
49
+ }
50
+ const value = captured[index];
51
+ if (value === null && index === 0)
52
+ continue;
53
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
54
+ throw new validate_1.InvalidTargetError(undefined, 'keyframes', captured, `keyframe at index ${index} must be a finite number${index === 0 ? ' or null' : ''} (REQ-API-033)`);
55
+ }
56
+ }
57
+ return captured;
58
+ }
59
+ function captureConfig(config) {
60
+ if (config === undefined)
61
+ return undefined;
62
+ // This exported generator is a raw public boundary, not merely a downstream helper. Prove the
63
+ // outer record BEFORE reading a field so null/array/prototype-bearing values become the typed
64
+ // transition refusal instead of a host TypeError (R8 review r9). Getter faults deliberately
65
+ // remain unguarded: caller faults retain their identity rather than being laundered as schema
66
+ // errors.
67
+ if (typeof config !== 'object' || config === null || Array.isArray(config)) {
68
+ throw new validate_1.InvalidTransitionError(undefined, 'config', config, 'keyframesGenerator config must be a plain object (REQ-API-033)');
69
+ }
70
+ const prototype = Object.getPrototypeOf(config);
71
+ if (prototype !== Object.prototype && prototype !== null) {
72
+ throw new validate_1.InvalidTransitionError(undefined, 'config', config, 'keyframesGenerator config must not carry a prototype (REQ-API-033)');
73
+ }
74
+ for (const key of Reflect.ownKeys(config)) {
75
+ if (key !== 'duration' && key !== 'times' && key !== 'ease') {
76
+ throw new validate_1.InvalidTransitionError(undefined, typeof key === 'string' ? key : String(key), config, 'keyframesGenerator config permits only own duration, times, and ease fields (REQ-API-033)');
77
+ }
78
+ }
79
+ const duration = Object.hasOwn(config, 'duration') ? config.duration : undefined;
80
+ const times = Object.hasOwn(config, 'times') ? config.times : undefined;
81
+ const ease = Object.hasOwn(config, 'ease') ? config.ease : undefined;
82
+ if (duration !== undefined &&
83
+ (typeof duration !== 'number' || !Number.isFinite(duration) || duration < 0)) {
84
+ throw new validate_1.InvalidTransitionError(undefined, 'duration', duration, 'keyframesGenerator duration must be a finite non-negative number of milliseconds (REQ-API-033)');
85
+ }
86
+ if (times !== undefined && !Array.isArray(times)) {
87
+ throw new validate_1.InvalidTransitionError(undefined, 'times', times, 'times must be a real array of offsets (REQ-API-033 law d)');
88
+ }
89
+ return Object.freeze({
90
+ ...(duration === undefined ? {} : { duration }),
91
+ ...(times === undefined
92
+ ? {}
93
+ : {
94
+ times: captureArray(times, (value, reason) => new validate_1.InvalidTransitionError(undefined, 'times', value, `${reason}; times must be real offsets (REQ-API-033 law d)`)),
95
+ }),
96
+ ...(ease === undefined
97
+ ? {}
98
+ : {
99
+ ease: Array.isArray(ease)
100
+ ? captureArray(ease, (value, reason) => new validate_1.InvalidTransitionError(undefined, 'ease', value, `${reason}; ease must be a real easing list (REQ-API-033 law d)`))
101
+ : ease,
102
+ }),
103
+ });
104
+ }
105
+ /**
106
+ * Build a multi-keyframe tween `GeneratorFactory` (REQ-API-033). The array length is ≥2 and the
107
+ * `times`/`ease` shapes are enforced at the validation boundary (validate.ts); this generator
108
+ * assumes a valid config and is pure per frame.
109
+ */
110
+ // alloc-ok: lifecycle-edge — the factory resolves offsets + per-segment easers once per animate
111
+ // command; per-frame work is sample() below, which mutates the reused record and allocates nothing.
112
+ function keyframesGenerator(keyframes, config) {
113
+ const capturedKeyframes = captureKeyframes(keyframes);
114
+ const capturedConfig = captureConfig(config);
115
+ const n = capturedKeyframes.length;
116
+ const refusal = (0, keyframeTiming_1.keyframeConfigRefusal)(capturedKeyframes, capturedConfig);
117
+ if (refusal !== null)
118
+ throw refusal;
119
+ const duration = capturedConfig?.duration ?? DEFAULT_DURATION_MS;
120
+ // Motion uses authored offsets only when their count matches the keyframes; every other shape-valid
121
+ // count (including zero) falls back to defaultOffset. Resolve through the shared UI-safe seam.
122
+ const times = capturedKeyframes.map((_value, index) => (0, keyframeTimingConfig_1.keyframeOffsetAt)(capturedConfig?.times, index, n) * duration);
123
+ // A list maps available easings by segment; Motion's interpolate uses linear (`noop`) for a missing
124
+ // member and ignores extras. An absent list still receives the generator's easeInOut default.
125
+ const segmentEasers = [];
126
+ for (let i = 0; i < n - 1; i++) {
127
+ segmentEasers.push((0, timing_1.resolveEasing)((0, keyframeTimingConfig_1.keyframeSegmentEase)(capturedConfig?.ease, i)));
128
+ }
129
+ return (seed) => {
130
+ // null-first (R8-F2): the first keyframe reads the element's current value.
131
+ // alloc-ok: lifecycle-edge — one values/segments build per generator construction.
132
+ const values = capturedKeyframes.map((value) => (value === null ? seed.from : value));
133
+ // Precompute well-typed segments once. The ≥2-length + `times`/`ease` shape laws are enforced at
134
+ // the validation boundary (validate.ts); the undefined-guard keeps this generator TOTAL and
135
+ // type-safe under noUncheckedIndexedAccess (it is never hit for valid input) — no per-frame
136
+ // array indexing, no non-null assertions.
137
+ const segments = [];
138
+ for (let i = 0; i < values.length - 1; i++) {
139
+ const from = values[i];
140
+ const to = values[i + 1];
141
+ const start = times[i];
142
+ const end = times[i + 1];
143
+ const ease = segmentEasers[i];
144
+ if (from === undefined ||
145
+ to === undefined ||
146
+ start === undefined ||
147
+ end === undefined ||
148
+ ease === undefined)
149
+ continue;
150
+ segments.push({ start, end, from, to, ease });
151
+ }
152
+ const firstSeg = segments[0];
153
+ const lastSeg = segments[segments.length - 1];
154
+ const first = firstSeg ? firstSeg.from : seed.from;
155
+ const last = lastSeg ? lastSeg.to : first;
156
+ const firstTime = firstSeg ? firstSeg.start : 0;
157
+ const lastTime = lastSeg ? lastSeg.end : 0;
158
+ const value = (t) => {
159
+ if (t <= firstTime)
160
+ return first;
161
+ if (t >= lastTime)
162
+ return last;
163
+ // bounded segment scan — no iterator alloc, no indexed access after the guard-narrow.
164
+ for (let i = 0; i < segments.length; i++) {
165
+ const seg = segments[i];
166
+ if (seg === undefined)
167
+ break;
168
+ if (t <= seg.end) {
169
+ const span = seg.end - seg.start;
170
+ const localP = span > 0 ? (t - seg.start) / span : 1;
171
+ return mix(seg.from, seg.to, seg.ease(clamp01(localP)));
172
+ }
173
+ }
174
+ return last;
175
+ };
176
+ const out = { value: first, velocity: 0, done: false };
177
+ return {
178
+ sample(elapsedMs) {
179
+ const current = value(elapsedMs);
180
+ const prevT = Math.max(elapsedMs - VELOCITY_SAMPLE_MS, 0);
181
+ const dt = elapsedMs - prevT;
182
+ const velocity = dt ? ((current - value(prevT)) / dt) * 1000 : 0; // px/s (REQ-TIMING-001)
183
+ const done = elapsedMs >= duration;
184
+ out.value = done ? last : current;
185
+ out.velocity = velocity;
186
+ out.done = done;
187
+ return out;
188
+ },
189
+ };
190
+ };
191
+ }
@@ -0,0 +1,13 @@
1
+ import { type Easing } from "./timing.cjs";
2
+ import type { GeneratorFactory } from "./types.cjs";
3
+ export interface KeyframesConfig {
4
+ duration?: number;
5
+ times?: readonly number[];
6
+ ease?: Easing | readonly Easing[];
7
+ }
8
+ /**
9
+ * Build a multi-keyframe tween `GeneratorFactory` (REQ-API-033). The array length is ≥2 and the
10
+ * `times`/`ease` shapes are enforced at the validation boundary (validate.ts); this generator
11
+ * assumes a valid config and is pure per frame.
12
+ */
13
+ export declare function keyframesGenerator(keyframes: readonly (number | null)[], config?: KeyframesConfig): GeneratorFactory;
@@ -0,0 +1,13 @@
1
+ import { type Easing } from "./timing.js";
2
+ import type { GeneratorFactory } from "./types.js";
3
+ export interface KeyframesConfig {
4
+ duration?: number;
5
+ times?: readonly number[];
6
+ ease?: Easing | readonly Easing[];
7
+ }
8
+ /**
9
+ * Build a multi-keyframe tween `GeneratorFactory` (REQ-API-033). The array length is ≥2 and the
10
+ * `times`/`ease` shapes are enforced at the validation boundary (validate.ts); this generator
11
+ * assumes a valid config and is pure per frame.
12
+ */
13
+ export declare function keyframesGenerator(keyframes: readonly (number | null)[], config?: KeyframesConfig): GeneratorFactory;
@@ -0,0 +1,188 @@
1
+ // @frame-path — executes inside the UI-runtime frame step; allocation-gated (REQ-DRIVER-021).
2
+ // The multi-keyframe timing generator (REQ-API-033, R8). Interpolates THROUGH a keyframe array over
3
+ // `duration`: even `defaultOffset` unless `times`, one `easeInOut` per SEGMENT unless an `ease`
4
+ // array, `keyframes[0]` seeds — or the element's CURRENT value when the first keyframe is `null`
5
+ // (R8-F2). Semantics pinned to motion@12.42.2's generators/keyframes.ts; the cubic-bezier easing is
6
+ // timing.ts's exact BezierEasing port (never a second vocabulary). The reused sample record and the
7
+ // bounded segment scan allocate nothing per frame.
8
+ import { InvalidTargetError, InvalidTransitionError } from "./component/validate.js";
9
+ import { resolveEasing } from "./timing.js";
10
+ import { keyframeConfigRefusal } from "./driver/keyframeTiming.js";
11
+ import { keyframeOffsetAt, keyframeSegmentEase } from "./driver/keyframeTimingConfig.js";
12
+ import { captureBoundedArray, captureBoundedCubicBezier, capturedArrayDescription, MAX_CAPTURED_ARRAY_LENGTH, } from "./component/boundedArray.js";
13
+ const DEFAULT_DURATION_MS = 300; // REQ-TIMING-002 (0.3s), motion's keyframes default
14
+ const VELOCITY_SAMPLE_MS = 5;
15
+ const clamp01 = (v) => Math.max(0, Math.min(1, v));
16
+ const mix = (from, to, p) => from + (to - from) * p;
17
+ function captureArray(value, invalid, nested = false) {
18
+ const capture = nested ? captureBoundedCubicBezier(value) : captureBoundedArray(value);
19
+ if (capture.kind === 'not-array')
20
+ throw invalid(capturedArrayDescription(capture), 'must be a real array');
21
+ if (capture.kind === 'invalid-length') {
22
+ throw invalid(capturedArrayDescription(capture), nested
23
+ ? 'must be a four-point cubic-bezier tuple (REQ-API-033)'
24
+ : `must have a safe array length between 0 and ${MAX_CAPTURED_ARRAY_LENGTH} (REQ-API-033)`);
25
+ }
26
+ const length = capture.values.length;
27
+ const captured = [];
28
+ captured.length = length;
29
+ for (let index = 0; index < length; index++) {
30
+ if (!Object.hasOwn(capture.values, index))
31
+ continue;
32
+ const member = capture.values[index];
33
+ captured[index] =
34
+ !nested && Array.isArray(member) ? captureArray(member, invalid, true) : member;
35
+ }
36
+ return Object.freeze(captured);
37
+ }
38
+ function captureKeyframes(keyframes) {
39
+ const captured = captureArray(keyframes, (value, reason) => new InvalidTargetError(undefined, 'keyframes', value, `${reason} (REQ-API-033)`));
40
+ if (captured.length < 2) {
41
+ throw new InvalidTargetError(undefined, 'keyframes', captured, 'a keyframe array must have at least two keyframes (REQ-API-033)');
42
+ }
43
+ for (let index = 0; index < captured.length; index++) {
44
+ if (!Object.hasOwn(captured, index)) {
45
+ throw new InvalidTargetError(undefined, 'keyframes', captured, `keyframe array is missing index ${index}; sparse keyframe arrays are invalid (REQ-API-033)`);
46
+ }
47
+ const value = captured[index];
48
+ if (value === null && index === 0)
49
+ continue;
50
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
51
+ throw new InvalidTargetError(undefined, 'keyframes', captured, `keyframe at index ${index} must be a finite number${index === 0 ? ' or null' : ''} (REQ-API-033)`);
52
+ }
53
+ }
54
+ return captured;
55
+ }
56
+ function captureConfig(config) {
57
+ if (config === undefined)
58
+ return undefined;
59
+ // This exported generator is a raw public boundary, not merely a downstream helper. Prove the
60
+ // outer record BEFORE reading a field so null/array/prototype-bearing values become the typed
61
+ // transition refusal instead of a host TypeError (R8 review r9). Getter faults deliberately
62
+ // remain unguarded: caller faults retain their identity rather than being laundered as schema
63
+ // errors.
64
+ if (typeof config !== 'object' || config === null || Array.isArray(config)) {
65
+ throw new InvalidTransitionError(undefined, 'config', config, 'keyframesGenerator config must be a plain object (REQ-API-033)');
66
+ }
67
+ const prototype = Object.getPrototypeOf(config);
68
+ if (prototype !== Object.prototype && prototype !== null) {
69
+ throw new InvalidTransitionError(undefined, 'config', config, 'keyframesGenerator config must not carry a prototype (REQ-API-033)');
70
+ }
71
+ for (const key of Reflect.ownKeys(config)) {
72
+ if (key !== 'duration' && key !== 'times' && key !== 'ease') {
73
+ throw new InvalidTransitionError(undefined, typeof key === 'string' ? key : String(key), config, 'keyframesGenerator config permits only own duration, times, and ease fields (REQ-API-033)');
74
+ }
75
+ }
76
+ const duration = Object.hasOwn(config, 'duration') ? config.duration : undefined;
77
+ const times = Object.hasOwn(config, 'times') ? config.times : undefined;
78
+ const ease = Object.hasOwn(config, 'ease') ? config.ease : undefined;
79
+ if (duration !== undefined &&
80
+ (typeof duration !== 'number' || !Number.isFinite(duration) || duration < 0)) {
81
+ throw new InvalidTransitionError(undefined, 'duration', duration, 'keyframesGenerator duration must be a finite non-negative number of milliseconds (REQ-API-033)');
82
+ }
83
+ if (times !== undefined && !Array.isArray(times)) {
84
+ throw new InvalidTransitionError(undefined, 'times', times, 'times must be a real array of offsets (REQ-API-033 law d)');
85
+ }
86
+ return Object.freeze({
87
+ ...(duration === undefined ? {} : { duration }),
88
+ ...(times === undefined
89
+ ? {}
90
+ : {
91
+ times: captureArray(times, (value, reason) => new InvalidTransitionError(undefined, 'times', value, `${reason}; times must be real offsets (REQ-API-033 law d)`)),
92
+ }),
93
+ ...(ease === undefined
94
+ ? {}
95
+ : {
96
+ ease: Array.isArray(ease)
97
+ ? captureArray(ease, (value, reason) => new InvalidTransitionError(undefined, 'ease', value, `${reason}; ease must be a real easing list (REQ-API-033 law d)`))
98
+ : ease,
99
+ }),
100
+ });
101
+ }
102
+ /**
103
+ * Build a multi-keyframe tween `GeneratorFactory` (REQ-API-033). The array length is ≥2 and the
104
+ * `times`/`ease` shapes are enforced at the validation boundary (validate.ts); this generator
105
+ * assumes a valid config and is pure per frame.
106
+ */
107
+ // alloc-ok: lifecycle-edge — the factory resolves offsets + per-segment easers once per animate
108
+ // command; per-frame work is sample() below, which mutates the reused record and allocates nothing.
109
+ export function keyframesGenerator(keyframes, config) {
110
+ const capturedKeyframes = captureKeyframes(keyframes);
111
+ const capturedConfig = captureConfig(config);
112
+ const n = capturedKeyframes.length;
113
+ const refusal = keyframeConfigRefusal(capturedKeyframes, capturedConfig);
114
+ if (refusal !== null)
115
+ throw refusal;
116
+ const duration = capturedConfig?.duration ?? DEFAULT_DURATION_MS;
117
+ // Motion uses authored offsets only when their count matches the keyframes; every other shape-valid
118
+ // count (including zero) falls back to defaultOffset. Resolve through the shared UI-safe seam.
119
+ const times = capturedKeyframes.map((_value, index) => keyframeOffsetAt(capturedConfig?.times, index, n) * duration);
120
+ // A list maps available easings by segment; Motion's interpolate uses linear (`noop`) for a missing
121
+ // member and ignores extras. An absent list still receives the generator's easeInOut default.
122
+ const segmentEasers = [];
123
+ for (let i = 0; i < n - 1; i++) {
124
+ segmentEasers.push(resolveEasing(keyframeSegmentEase(capturedConfig?.ease, i)));
125
+ }
126
+ return (seed) => {
127
+ // null-first (R8-F2): the first keyframe reads the element's current value.
128
+ // alloc-ok: lifecycle-edge — one values/segments build per generator construction.
129
+ const values = capturedKeyframes.map((value) => (value === null ? seed.from : value));
130
+ // Precompute well-typed segments once. The ≥2-length + `times`/`ease` shape laws are enforced at
131
+ // the validation boundary (validate.ts); the undefined-guard keeps this generator TOTAL and
132
+ // type-safe under noUncheckedIndexedAccess (it is never hit for valid input) — no per-frame
133
+ // array indexing, no non-null assertions.
134
+ const segments = [];
135
+ for (let i = 0; i < values.length - 1; i++) {
136
+ const from = values[i];
137
+ const to = values[i + 1];
138
+ const start = times[i];
139
+ const end = times[i + 1];
140
+ const ease = segmentEasers[i];
141
+ if (from === undefined ||
142
+ to === undefined ||
143
+ start === undefined ||
144
+ end === undefined ||
145
+ ease === undefined)
146
+ continue;
147
+ segments.push({ start, end, from, to, ease });
148
+ }
149
+ const firstSeg = segments[0];
150
+ const lastSeg = segments[segments.length - 1];
151
+ const first = firstSeg ? firstSeg.from : seed.from;
152
+ const last = lastSeg ? lastSeg.to : first;
153
+ const firstTime = firstSeg ? firstSeg.start : 0;
154
+ const lastTime = lastSeg ? lastSeg.end : 0;
155
+ const value = (t) => {
156
+ if (t <= firstTime)
157
+ return first;
158
+ if (t >= lastTime)
159
+ return last;
160
+ // bounded segment scan — no iterator alloc, no indexed access after the guard-narrow.
161
+ for (let i = 0; i < segments.length; i++) {
162
+ const seg = segments[i];
163
+ if (seg === undefined)
164
+ break;
165
+ if (t <= seg.end) {
166
+ const span = seg.end - seg.start;
167
+ const localP = span > 0 ? (t - seg.start) / span : 1;
168
+ return mix(seg.from, seg.to, seg.ease(clamp01(localP)));
169
+ }
170
+ }
171
+ return last;
172
+ };
173
+ const out = { value: first, velocity: 0, done: false };
174
+ return {
175
+ sample(elapsedMs) {
176
+ const current = value(elapsedMs);
177
+ const prevT = Math.max(elapsedMs - VELOCITY_SAMPLE_MS, 0);
178
+ const dt = elapsedMs - prevT;
179
+ const velocity = dt ? ((current - value(prevT)) / dt) * 1000 : 0; // px/s (REQ-TIMING-001)
180
+ const done = elapsedMs >= duration;
181
+ out.value = done ? last : current;
182
+ out.velocity = velocity;
183
+ out.done = done;
184
+ return out;
185
+ },
186
+ };
187
+ };
188
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ // Commit detection for the L3 no-flash correction (specs/L3-BUILD-PACKET.md §Order 1): dedupe a
3
+ // per-frame stream of measured layout rects into COMMIT events. The UI-thread frame step feeds
4
+ // the committed layout it measures each frame; the detector answers "did the host commit a NEW
5
+ // layout this frame?" so the caller can apply the FLIP inversion in the same frame — before
6
+ // paint. Host-agnostic and pure: the caller owns WHERE rects come from (the spike must feed
7
+ // transform-free layout values — a measure that reflected the correction transform would feed
8
+ // the projection back into measurement, which is forbidden by the standing law).
9
+ //
10
+ // Fail-closed lifecycle (REQ-LAYOUT-011 posture): a null read is 'unmeasured' — never an event,
11
+ // never a fabricated previous rect; the FIRST non-null rect is 'initial' (nothing to correct
12
+ // from); only a rect that DIFFERS from the last committed one is a 'commit' carrying
13
+ // {previous, next}. Equality is exact: two commits that produce identical layout values are the
14
+ // same layout — the detector never invents a zero-delta commit.
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.createCommitDetector = createCommitDetector;
17
+ function rectsEqual(a, b) {
18
+ 'worklet';
19
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
20
+ }
21
+ function isFiniteRect(rect) {
22
+ 'worklet';
23
+ return (Number.isFinite(rect.x) &&
24
+ Number.isFinite(rect.y) &&
25
+ Number.isFinite(rect.width) &&
26
+ Number.isFinite(rect.height));
27
+ }
28
+ function createCommitDetector(options) {
29
+ 'worklet';
30
+ let committed = null;
31
+ let refusalReported = false;
32
+ return {
33
+ feed(rect) {
34
+ 'worklet';
35
+ if (rect === null)
36
+ return { kind: 'unmeasured' };
37
+ if (!isFiniteRect(rect)) {
38
+ if (options?.onRefusal === undefined) {
39
+ // ui-throw-ok: JS-side public-API loud path (seal r8, F1) — reachable only when the
40
+ // detector was constructed WITHOUT a refusal reporter, which the UI-runtime wiring
41
+ // never does (it injects the deferred reporter, per the options contract above); a
42
+ // bare public construction is a JS consumer, where fail-loud beats a silent swallow.
43
+ throw new Error(`layout commit detector: refused a malformed rect (${JSON.stringify(rect)}) — ` +
44
+ 'non-finite fields never become a commit (L3 packet; M2.5 §semantics 6 posture). ' +
45
+ 'No refusal reporter installed — the bare public API fails loud (seal r8, F1).');
46
+ }
47
+ if (!refusalReported) {
48
+ refusalReported = true;
49
+ options.onRefusal(`layout commit detector: refused a malformed rect (${JSON.stringify(rect)}) — ` +
50
+ 'non-finite fields never become a commit (L3 packet; M2.5 §semantics 6 posture).');
51
+ }
52
+ // The refusal itself: no commit is recorded — the detector keeps the last valid state.
53
+ return committed === null ? { kind: 'unmeasured' } : { kind: 'unchanged', rect: committed };
54
+ }
55
+ if (committed === null) {
56
+ committed = rect;
57
+ return { kind: 'initial', rect };
58
+ }
59
+ if (rectsEqual(committed, rect))
60
+ return { kind: 'unchanged', rect };
61
+ const previous = committed;
62
+ committed = rect;
63
+ return { kind: 'commit', previous, next: rect };
64
+ },
65
+ committed: () => committed,
66
+ };
67
+ }
@@ -0,0 +1,21 @@
1
+ import type { Rect } from "./types.cjs";
2
+ export type CommitDetection = {
3
+ readonly kind: 'unmeasured';
4
+ } | {
5
+ readonly kind: 'initial';
6
+ readonly rect: Rect;
7
+ } | {
8
+ readonly kind: 'unchanged';
9
+ readonly rect: Rect;
10
+ } | {
11
+ readonly kind: 'commit';
12
+ readonly previous: Rect;
13
+ readonly next: Rect;
14
+ };
15
+ export interface CommitDetector {
16
+ feed(rect: Rect | null): CommitDetection;
17
+ committed(): Rect | null;
18
+ }
19
+ export declare function createCommitDetector(options?: {
20
+ readonly onRefusal?: (detail: string) => void;
21
+ }): CommitDetector;
@@ -0,0 +1,21 @@
1
+ import type { Rect } from "./types.js";
2
+ export type CommitDetection = {
3
+ readonly kind: 'unmeasured';
4
+ } | {
5
+ readonly kind: 'initial';
6
+ readonly rect: Rect;
7
+ } | {
8
+ readonly kind: 'unchanged';
9
+ readonly rect: Rect;
10
+ } | {
11
+ readonly kind: 'commit';
12
+ readonly previous: Rect;
13
+ readonly next: Rect;
14
+ };
15
+ export interface CommitDetector {
16
+ feed(rect: Rect | null): CommitDetection;
17
+ committed(): Rect | null;
18
+ }
19
+ export declare function createCommitDetector(options?: {
20
+ readonly onRefusal?: (detail: string) => void;
21
+ }): CommitDetector;
@@ -0,0 +1,64 @@
1
+ // Commit detection for the L3 no-flash correction (specs/L3-BUILD-PACKET.md §Order 1): dedupe a
2
+ // per-frame stream of measured layout rects into COMMIT events. The UI-thread frame step feeds
3
+ // the committed layout it measures each frame; the detector answers "did the host commit a NEW
4
+ // layout this frame?" so the caller can apply the FLIP inversion in the same frame — before
5
+ // paint. Host-agnostic and pure: the caller owns WHERE rects come from (the spike must feed
6
+ // transform-free layout values — a measure that reflected the correction transform would feed
7
+ // the projection back into measurement, which is forbidden by the standing law).
8
+ //
9
+ // Fail-closed lifecycle (REQ-LAYOUT-011 posture): a null read is 'unmeasured' — never an event,
10
+ // never a fabricated previous rect; the FIRST non-null rect is 'initial' (nothing to correct
11
+ // from); only a rect that DIFFERS from the last committed one is a 'commit' carrying
12
+ // {previous, next}. Equality is exact: two commits that produce identical layout values are the
13
+ // same layout — the detector never invents a zero-delta commit.
14
+ function rectsEqual(a, b) {
15
+ 'worklet';
16
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
17
+ }
18
+ function isFiniteRect(rect) {
19
+ 'worklet';
20
+ return (Number.isFinite(rect.x) &&
21
+ Number.isFinite(rect.y) &&
22
+ Number.isFinite(rect.width) &&
23
+ Number.isFinite(rect.height));
24
+ }
25
+ export function createCommitDetector(options) {
26
+ 'worklet';
27
+ let committed = null;
28
+ let refusalReported = false;
29
+ return {
30
+ feed(rect) {
31
+ 'worklet';
32
+ if (rect === null)
33
+ return { kind: 'unmeasured' };
34
+ if (!isFiniteRect(rect)) {
35
+ if (options?.onRefusal === undefined) {
36
+ // ui-throw-ok: JS-side public-API loud path (seal r8, F1) — reachable only when the
37
+ // detector was constructed WITHOUT a refusal reporter, which the UI-runtime wiring
38
+ // never does (it injects the deferred reporter, per the options contract above); a
39
+ // bare public construction is a JS consumer, where fail-loud beats a silent swallow.
40
+ throw new Error(`layout commit detector: refused a malformed rect (${JSON.stringify(rect)}) — ` +
41
+ 'non-finite fields never become a commit (L3 packet; M2.5 §semantics 6 posture). ' +
42
+ 'No refusal reporter installed — the bare public API fails loud (seal r8, F1).');
43
+ }
44
+ if (!refusalReported) {
45
+ refusalReported = true;
46
+ options.onRefusal(`layout commit detector: refused a malformed rect (${JSON.stringify(rect)}) — ` +
47
+ 'non-finite fields never become a commit (L3 packet; M2.5 §semantics 6 posture).');
48
+ }
49
+ // The refusal itself: no commit is recorded — the detector keeps the last valid state.
50
+ return committed === null ? { kind: 'unmeasured' } : { kind: 'unchanged', rect: committed };
51
+ }
52
+ if (committed === null) {
53
+ committed = rect;
54
+ return { kind: 'initial', rect };
55
+ }
56
+ if (rectsEqual(committed, rect))
57
+ return { kind: 'unchanged', rect };
58
+ const previous = committed;
59
+ committed = rect;
60
+ return { kind: 'commit', previous, next: rect };
61
+ },
62
+ committed: () => committed,
63
+ };
64
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ // SPEC-LAYOUT §2/§4a — parent-relative composition + distortion correction (REQ-LAYOUT-012/013). A nested
3
+ // child is positioned relative to the animating ancestor's projection frame, so it composes coherently and
4
+ // is never left behind at its pre-animation viewport origin. A child carrying layout identity under a scaled
5
+ // parent gets a counter-scale to keep its intrinsic size, and border-radius is corrected under non-unit
6
+ // scale. A property that cannot be corrected under a scaled layout fails loud (REQ-LAYOUT-013 → REQ-API-005/
7
+ // 006). Pure; consumes the frozen SUBSET registry (existence check) but owns no second property registry.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.composeParentRelative = composeParentRelative;
10
+ exports.counterScale = counterScale;
11
+ exports.correctRadius = correctRadius;
12
+ exports.assertCorrectable = assertCorrectable;
13
+ const subset_1 = require("../subset/index.cjs");
14
+ const projection_1 = require("./projection.cjs");
15
+ /**
16
+ * The viewport rect of a child at progress `p`, positioned parent-relative (REQ-LAYOUT-012). `childLocal` is
17
+ * the child's rect in the parent's LOCAL space (relative to the parent's top-left, in the after frame). The
18
+ * child rides the parent's projection: its origin and size scale with the parent's current visual frame, so
19
+ * its fractional position within the parent is invariant across progress and it is never stranded at its
20
+ * host-absolute origin while the parent is displaced.
21
+ */
22
+ function composeParentRelative(parentBefore, parentAfter, childLocal, progress) {
23
+ const tp = (0, projection_1.projectAtProgress)(parentBefore, parentAfter, progress);
24
+ const parentVisual = (0, projection_1.applyTransformToRect)(tp, parentAfter);
25
+ return {
26
+ x: parentVisual.x + childLocal.x * tp.scaleX,
27
+ y: parentVisual.y + childLocal.y * tp.scaleY,
28
+ width: childLocal.width * tp.scaleX,
29
+ height: childLocal.height * tp.scaleY,
30
+ };
31
+ }
32
+ function assertNonZeroScale(scaleX, scaleY) {
33
+ if (!Number.isFinite(scaleX) || !Number.isFinite(scaleY) || scaleX === 0 || scaleY === 0) {
34
+ throw new Error(`layout: cannot counter-correct under a zero/non-finite scale (scaleX=${scaleX}, scaleY=${scaleY}) — divide-by-zero guard (REQ-LAYOUT-011)`);
35
+ }
36
+ }
37
+ /**
38
+ * The counter-scale a child carrying layout identity applies to cancel its parent's scale, so it keeps its
39
+ * intrinsic size under a scaled layout (REQ-LAYOUT-013). `parent.scaleX × counterScale.scaleX == 1`.
40
+ */
41
+ function counterScale(parent) {
42
+ assertNonZeroScale(parent.scaleX, parent.scaleY);
43
+ return { translateX: 0, translateY: 0, scaleX: 1 / parent.scaleX, scaleY: 1 / parent.scaleY };
44
+ }
45
+ /**
46
+ * Correct a border-radius under a non-unit scale (REQ-LAYOUT-013): dividing by the axis scale so the applied
47
+ * scale reproduces the intended visual radius. Anisotropic scale yields distinct x/y radii.
48
+ */
49
+ function correctRadius(radius, scaleX, scaleY) {
50
+ assertNonZeroScale(scaleX, scaleY);
51
+ return { x: radius / scaleX, y: radius / scaleY };
52
+ }
53
+ // LAYOUT's proposed set of properties that are corrected under a scaled layout. This is the subsystem's
54
+ // CONTRIBUTION to the universal subset (REQ-LAYOUT-013), pending SUBSET ratification (§7) — NOT a second
55
+ // registry. Size is handled structurally by counterScale; borderRadius by correctRadius.
56
+ const LAYOUT_CORRECTABLE = new Set(['borderRadius']);
57
+ /**
58
+ * Assert a property can be corrected under a scaled layout, else fail loud (REQ-LAYOUT-013 → REQ-API-005/006)
59
+ * rather than rendering a distorted result. Consults the frozen registry only to sharpen the message
60
+ * (known style prop vs typo); the correctable set is LAYOUT's own contribution, pending ratification.
61
+ */
62
+ function assertCorrectable(property) {
63
+ if (LAYOUT_CORRECTABLE.has(property))
64
+ return;
65
+ const known = subset_1.UNIVERSAL_SUBSET.has(property);
66
+ throw new Error(`layout: '${property}' is ${known ? 'a known style property but is not' : 'not a known style property and is not'} correctable under a scaled layout at M1 — the correctable set is {borderRadius} (size via counter-scale), LAYOUT's proposed contribution to the universal subset (§7, pending SUBSET ratification). Fails loud rather than rendering distorted (REQ-LAYOUT-013).`);
67
+ }