@fairyhunter13/opentui-core 0.1.112 → 0.1.114

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 (591) hide show
  1. package/dev/keypress-debug-renderer.ts +148 -0
  2. package/dev/keypress-debug.ts +43 -0
  3. package/dev/print-env-vars.ts +32 -0
  4. package/dev/test-tmux-graphics-334.sh +68 -0
  5. package/dev/thai-debug-test.ts +68 -0
  6. package/docs/development.md +144 -0
  7. package/package.json +63 -51
  8. package/scripts/build.ts +400 -0
  9. package/scripts/publish.ts +60 -0
  10. package/src/3d/SpriteResourceManager.ts +286 -0
  11. package/src/3d/SpriteUtils.ts +70 -0
  12. package/src/3d/TextureUtils.ts +196 -0
  13. package/src/3d/ThreeRenderable.ts +197 -0
  14. package/src/3d/WGPURenderer.ts +294 -0
  15. package/src/3d/animation/ExplodingSpriteEffect.ts +513 -0
  16. package/src/3d/animation/PhysicsExplodingSpriteEffect.ts +429 -0
  17. package/src/3d/animation/SpriteAnimator.ts +633 -0
  18. package/src/3d/animation/SpriteParticleGenerator.ts +435 -0
  19. package/src/3d/canvas.ts +464 -0
  20. package/src/3d/index.ts +12 -0
  21. package/src/3d/physics/PlanckPhysicsAdapter.ts +72 -0
  22. package/src/3d/physics/RapierPhysicsAdapter.ts +66 -0
  23. package/src/3d/physics/physics-interface.ts +31 -0
  24. package/src/3d/shaders/supersampling.wgsl +201 -0
  25. package/src/3d.ts +3 -0
  26. package/src/NativeSpanFeed.ts +300 -0
  27. package/src/Renderable.ts +1704 -0
  28. package/src/__snapshots__/buffer.test.ts.snap +28 -0
  29. package/src/animation/Timeline.test.ts +2709 -0
  30. package/src/animation/Timeline.ts +598 -0
  31. package/src/ansi.ts +18 -0
  32. package/src/benchmark/attenuation-benchmark.ts +81 -0
  33. package/src/benchmark/colormatrix-benchmark.ts +128 -0
  34. package/src/benchmark/gain-benchmark.ts +80 -0
  35. package/src/benchmark/latest-all-bench-run.json +707 -0
  36. package/src/benchmark/latest-async-bench-run.json +336 -0
  37. package/src/benchmark/latest-default-bench-run.json +657 -0
  38. package/src/benchmark/latest-large-bench-run.json +707 -0
  39. package/src/benchmark/latest-quick-bench-run.json +207 -0
  40. package/src/benchmark/markdown-benchmark.ts +1796 -0
  41. package/src/benchmark/native-span-feed-async-benchmark.ts +355 -0
  42. package/src/benchmark/native-span-feed-benchmark.md +56 -0
  43. package/src/benchmark/native-span-feed-benchmark.ts +596 -0
  44. package/src/benchmark/native-span-feed-compare.ts +280 -0
  45. package/src/benchmark/renderer-benchmark.ts +754 -0
  46. package/src/benchmark/text-table-benchmark.ts +948 -0
  47. package/src/buffer.test.ts +291 -0
  48. package/src/buffer.ts +554 -0
  49. package/src/console.test.ts +612 -0
  50. package/src/console.ts +1254 -0
  51. package/src/edit-buffer.test.ts +1769 -0
  52. package/src/edit-buffer.ts +411 -0
  53. package/src/editor-view.test.ts +1032 -0
  54. package/src/editor-view.ts +284 -0
  55. package/src/examples/ascii-font-selection-demo.ts +245 -0
  56. package/src/examples/assets/Water_2_M_Normal.jpg +0 -0
  57. package/src/examples/assets/concrete.png +0 -0
  58. package/src/examples/assets/crate.png +0 -0
  59. package/src/examples/assets/crate_emissive.png +0 -0
  60. package/src/examples/assets/forrest_background.png +0 -0
  61. package/src/examples/assets/hast-example.json +1018 -0
  62. package/src/examples/assets/heart.png +0 -0
  63. package/src/examples/assets/main_char_heavy_attack.png +0 -0
  64. package/src/examples/assets/main_char_idle.png +0 -0
  65. package/src/examples/assets/main_char_jump_end.png +0 -0
  66. package/src/examples/assets/main_char_jump_landing.png +0 -0
  67. package/src/examples/assets/main_char_jump_start.png +0 -0
  68. package/src/examples/assets/main_char_run_loop.png +0 -0
  69. package/src/examples/assets/roughness_map.jpg +0 -0
  70. package/src/examples/build.ts +115 -0
  71. package/src/examples/code-demo.ts +924 -0
  72. package/src/examples/console-demo.ts +358 -0
  73. package/src/examples/core-plugin-slots-demo.ts +759 -0
  74. package/src/examples/diff-demo.ts +701 -0
  75. package/src/examples/draggable-three-demo.ts +259 -0
  76. package/src/examples/editor-demo.ts +322 -0
  77. package/src/examples/extmarks-demo.ts +196 -0
  78. package/src/examples/focus-restore-demo.ts +310 -0
  79. package/src/examples/fonts.ts +245 -0
  80. package/src/examples/fractal-shader-demo.ts +268 -0
  81. package/src/examples/framebuffer-demo.ts +674 -0
  82. package/src/examples/full-unicode-demo.ts +241 -0
  83. package/src/examples/golden-star-demo.ts +933 -0
  84. package/src/examples/grayscale-buffer-demo.ts +249 -0
  85. package/src/examples/hast-syntax-highlighting-demo.ts +129 -0
  86. package/src/examples/index.ts +926 -0
  87. package/src/examples/input-demo.ts +377 -0
  88. package/src/examples/input-select-layout-demo.ts +425 -0
  89. package/src/examples/install.sh +143 -0
  90. package/src/examples/keypress-debug-demo.ts +452 -0
  91. package/src/examples/lib/HexList.ts +122 -0
  92. package/src/examples/lib/PaletteGrid.ts +125 -0
  93. package/src/examples/lib/standalone-keys.ts +25 -0
  94. package/src/examples/lib/tab-controller.ts +243 -0
  95. package/src/examples/lights-phong-demo.ts +290 -0
  96. package/src/examples/link-demo.ts +220 -0
  97. package/src/examples/live-state-demo.ts +480 -0
  98. package/src/examples/markdown-demo.ts +725 -0
  99. package/src/examples/mouse-interaction-demo.ts +428 -0
  100. package/src/examples/nested-zindex-demo.ts +357 -0
  101. package/src/examples/opacity-example.ts +235 -0
  102. package/src/examples/opentui-demo.ts +1057 -0
  103. package/src/examples/physx-planck-2d-demo.ts +623 -0
  104. package/src/examples/physx-rapier-2d-demo.ts +655 -0
  105. package/src/examples/relative-positioning-demo.ts +323 -0
  106. package/src/examples/scroll-example.ts +214 -0
  107. package/src/examples/scrollbox-mouse-test.ts +112 -0
  108. package/src/examples/scrollbox-overlay-hit-test.ts +206 -0
  109. package/src/examples/select-demo.ts +237 -0
  110. package/src/examples/shader-cube-demo.ts +1015 -0
  111. package/src/examples/simple-layout-example.ts +591 -0
  112. package/src/examples/slider-demo.ts +617 -0
  113. package/src/examples/split-mode-demo.ts +453 -0
  114. package/src/examples/sprite-animation-demo.ts +443 -0
  115. package/src/examples/sprite-particle-generator-demo.ts +486 -0
  116. package/src/examples/static-sprite-demo.ts +193 -0
  117. package/src/examples/sticky-scroll-example.ts +308 -0
  118. package/src/examples/styled-text-demo.ts +282 -0
  119. package/src/examples/tab-select-demo.ts +219 -0
  120. package/src/examples/terminal-title.ts +29 -0
  121. package/src/examples/terminal.ts +305 -0
  122. package/src/examples/text-node-demo.ts +416 -0
  123. package/src/examples/text-selection-demo.ts +377 -0
  124. package/src/examples/text-table-demo.ts +503 -0
  125. package/src/examples/text-truncation-demo.ts +481 -0
  126. package/src/examples/text-wrap.ts +757 -0
  127. package/src/examples/texture-loading-demo.ts +259 -0
  128. package/src/examples/timeline-example.ts +670 -0
  129. package/src/examples/transparency-demo.ts +400 -0
  130. package/src/examples/vnode-composition-demo.ts +404 -0
  131. package/src/examples/wide-grapheme-overlay-demo.ts +280 -0
  132. package/src/index.ts +24 -0
  133. package/src/lib/KeyHandler.integration.test.ts +292 -0
  134. package/src/lib/KeyHandler.stopPropagation.test.ts +289 -0
  135. package/src/lib/KeyHandler.test.ts +662 -0
  136. package/src/lib/KeyHandler.ts +222 -0
  137. package/src/lib/RGBA.test.ts +984 -0
  138. package/src/lib/RGBA.ts +204 -0
  139. package/src/lib/ascii.font.ts +330 -0
  140. package/src/lib/border.test.ts +83 -0
  141. package/src/lib/border.ts +170 -0
  142. package/src/lib/bunfs.test.ts +27 -0
  143. package/src/lib/bunfs.ts +18 -0
  144. package/src/lib/clipboard.test.ts +41 -0
  145. package/src/lib/clipboard.ts +47 -0
  146. package/src/lib/clock.ts +35 -0
  147. package/src/lib/data-paths.test.ts +133 -0
  148. package/src/lib/data-paths.ts +109 -0
  149. package/src/lib/debounce.ts +106 -0
  150. package/src/lib/detect-links.test.ts +98 -0
  151. package/src/lib/detect-links.ts +56 -0
  152. package/src/lib/env.test.ts +228 -0
  153. package/src/lib/env.ts +209 -0
  154. package/src/lib/extmarks-history.ts +51 -0
  155. package/src/lib/extmarks-multiwidth.test.ts +322 -0
  156. package/src/lib/extmarks.test.ts +3457 -0
  157. package/src/lib/extmarks.ts +843 -0
  158. package/src/lib/fonts/block.json +405 -0
  159. package/src/lib/fonts/grid.json +265 -0
  160. package/src/lib/fonts/huge.json +741 -0
  161. package/src/lib/fonts/pallet.json +314 -0
  162. package/src/lib/fonts/shade.json +591 -0
  163. package/src/lib/fonts/slick.json +321 -0
  164. package/src/lib/fonts/tiny.json +69 -0
  165. package/src/lib/hast-styled-text.ts +59 -0
  166. package/src/lib/index.ts +21 -0
  167. package/src/lib/keymapping.test.ts +317 -0
  168. package/src/lib/keymapping.ts +115 -0
  169. package/src/lib/objects-in-viewport.test.ts +787 -0
  170. package/src/lib/objects-in-viewport.ts +153 -0
  171. package/src/lib/output.capture.ts +58 -0
  172. package/src/lib/parse.keypress-kitty.protocol.test.ts +340 -0
  173. package/src/lib/parse.keypress-kitty.test.ts +663 -0
  174. package/src/lib/parse.keypress-kitty.ts +439 -0
  175. package/src/lib/parse.keypress.test.ts +1849 -0
  176. package/src/lib/parse.keypress.ts +397 -0
  177. package/src/lib/parse.mouse.test.ts +552 -0
  178. package/src/lib/parse.mouse.ts +232 -0
  179. package/src/lib/paste.ts +16 -0
  180. package/src/lib/queue.ts +65 -0
  181. package/src/lib/renderable.validations.test.ts +87 -0
  182. package/src/lib/renderable.validations.ts +83 -0
  183. package/src/lib/scroll-acceleration.ts +98 -0
  184. package/src/lib/selection.ts +240 -0
  185. package/src/lib/singleton.ts +28 -0
  186. package/src/lib/stdin-parser.test.ts +2290 -0
  187. package/src/lib/stdin-parser.ts +1810 -0
  188. package/src/lib/styled-text.ts +178 -0
  189. package/src/lib/terminal-capability-detection.test.ts +202 -0
  190. package/src/lib/terminal-capability-detection.ts +79 -0
  191. package/src/lib/terminal-palette.test.ts +878 -0
  192. package/src/lib/terminal-palette.ts +383 -0
  193. package/src/lib/tree-sitter/assets/README.md +118 -0
  194. package/src/lib/tree-sitter/assets/update.ts +334 -0
  195. package/src/lib/tree-sitter/assets.d.ts +9 -0
  196. package/src/lib/tree-sitter/cache.test.ts +273 -0
  197. package/src/lib/tree-sitter/client.test.ts +1165 -0
  198. package/src/lib/tree-sitter/client.ts +607 -0
  199. package/src/lib/tree-sitter/default-parsers.ts +86 -0
  200. package/src/lib/tree-sitter/download-utils.ts +148 -0
  201. package/src/lib/tree-sitter/index.ts +28 -0
  202. package/src/lib/tree-sitter/parser.worker.ts +1042 -0
  203. package/src/lib/tree-sitter/parsers-config.ts +81 -0
  204. package/src/lib/tree-sitter/resolve-ft.test.ts +55 -0
  205. package/src/lib/tree-sitter/resolve-ft.ts +189 -0
  206. package/src/lib/tree-sitter/types.ts +82 -0
  207. package/src/lib/tree-sitter-styled-text.test.ts +1253 -0
  208. package/src/lib/tree-sitter-styled-text.ts +306 -0
  209. package/src/lib/validate-dir-name.ts +55 -0
  210. package/src/lib/yoga.options.test.ts +628 -0
  211. package/src/lib/yoga.options.ts +346 -0
  212. package/src/plugins/core-slot.ts +579 -0
  213. package/src/plugins/registry.ts +402 -0
  214. package/src/plugins/types.ts +46 -0
  215. package/src/post/effects.ts +930 -0
  216. package/src/post/filters.ts +489 -0
  217. package/src/post/matrices.ts +288 -0
  218. package/src/renderables/ASCIIFont.ts +219 -0
  219. package/src/renderables/Box.test.ts +205 -0
  220. package/src/renderables/Box.ts +326 -0
  221. package/src/renderables/Code.test.ts +2062 -0
  222. package/src/renderables/Code.ts +357 -0
  223. package/src/renderables/Diff.regression.test.ts +226 -0
  224. package/src/renderables/Diff.test.ts +3101 -0
  225. package/src/renderables/Diff.ts +1211 -0
  226. package/src/renderables/EditBufferRenderable.test.ts +288 -0
  227. package/src/renderables/EditBufferRenderable.ts +1166 -0
  228. package/src/renderables/FrameBuffer.ts +47 -0
  229. package/src/renderables/Input.test.ts +1228 -0
  230. package/src/renderables/Input.ts +247 -0
  231. package/src/renderables/LineNumberRenderable.ts +724 -0
  232. package/src/renderables/Markdown.ts +1393 -0
  233. package/src/renderables/ScrollBar.ts +422 -0
  234. package/src/renderables/ScrollBox.ts +883 -0
  235. package/src/renderables/Select.test.ts +1033 -0
  236. package/src/renderables/Select.ts +524 -0
  237. package/src/renderables/Slider.test.ts +456 -0
  238. package/src/renderables/Slider.ts +342 -0
  239. package/src/renderables/TabSelect.test.ts +197 -0
  240. package/src/renderables/TabSelect.ts +455 -0
  241. package/src/renderables/Text.selection-buffer.test.ts +123 -0
  242. package/src/renderables/Text.test.ts +2660 -0
  243. package/src/renderables/Text.ts +147 -0
  244. package/src/renderables/TextBufferRenderable.ts +518 -0
  245. package/src/renderables/TextNode.test.ts +1058 -0
  246. package/src/renderables/TextNode.ts +325 -0
  247. package/src/renderables/TextTable.test.ts +1421 -0
  248. package/src/renderables/TextTable.ts +1344 -0
  249. package/src/renderables/Textarea.ts +430 -0
  250. package/src/renderables/TimeToFirstDraw.ts +89 -0
  251. package/src/renderables/__snapshots__/Code.test.ts.snap +13 -0
  252. package/src/renderables/__snapshots__/Diff.test.ts.snap +785 -0
  253. package/src/renderables/__snapshots__/Text.test.ts.snap +421 -0
  254. package/src/renderables/__snapshots__/TextTable.test.ts.snap +215 -0
  255. package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +144 -0
  256. package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +816 -0
  257. package/src/renderables/__tests__/LineNumberRenderable.test.ts +1865 -0
  258. package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +85 -0
  259. package/src/renderables/__tests__/Markdown.code-colors.test.ts +242 -0
  260. package/src/renderables/__tests__/Markdown.test.ts +2518 -0
  261. package/src/renderables/__tests__/MultiRenderable.selection.test.ts +87 -0
  262. package/src/renderables/__tests__/Textarea.buffer.test.ts +682 -0
  263. package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +675 -0
  264. package/src/renderables/__tests__/Textarea.editing.test.ts +2041 -0
  265. package/src/renderables/__tests__/Textarea.error-handling.test.ts +35 -0
  266. package/src/renderables/__tests__/Textarea.events.test.ts +738 -0
  267. package/src/renderables/__tests__/Textarea.highlights.test.ts +590 -0
  268. package/src/renderables/__tests__/Textarea.keybinding.test.ts +3149 -0
  269. package/src/renderables/__tests__/Textarea.paste.test.ts +357 -0
  270. package/src/renderables/__tests__/Textarea.rendering.test.ts +1866 -0
  271. package/src/renderables/__tests__/Textarea.scroll.test.ts +733 -0
  272. package/src/renderables/__tests__/Textarea.selection.test.ts +1590 -0
  273. package/src/renderables/__tests__/Textarea.stress.test.ts +670 -0
  274. package/src/renderables/__tests__/Textarea.undo-redo.test.ts +383 -0
  275. package/src/renderables/__tests__/Textarea.visual-lines.test.ts +310 -0
  276. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +221 -0
  277. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +89 -0
  278. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +457 -0
  279. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +158 -0
  280. package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +387 -0
  281. package/src/renderables/__tests__/markdown-parser.test.ts +217 -0
  282. package/src/renderables/__tests__/renderable-test-utils.ts +60 -0
  283. package/src/renderables/composition/README.md +8 -0
  284. package/src/renderables/composition/VRenderable.ts +32 -0
  285. package/src/renderables/composition/constructs.ts +127 -0
  286. package/src/renderables/composition/vnode.ts +289 -0
  287. package/src/renderables/index.ts +23 -0
  288. package/src/renderables/markdown-parser.ts +66 -0
  289. package/src/renderer.ts +2681 -0
  290. package/src/runtime-plugin-support.ts +39 -0
  291. package/src/runtime-plugin.ts +615 -0
  292. package/src/syntax-style.test.ts +841 -0
  293. package/src/syntax-style.ts +257 -0
  294. package/src/testing/README.md +210 -0
  295. package/src/testing/capture-spans.test.ts +194 -0
  296. package/src/testing/integration.test.ts +276 -0
  297. package/src/testing/manual-clock.ts +117 -0
  298. package/src/testing/mock-keys.test.ts +1378 -0
  299. package/src/testing/mock-keys.ts +457 -0
  300. package/src/testing/mock-mouse.test.ts +218 -0
  301. package/src/testing/mock-mouse.ts +247 -0
  302. package/src/testing/mock-tree-sitter-client.ts +73 -0
  303. package/src/testing/spy.ts +13 -0
  304. package/src/testing/test-recorder.test.ts +415 -0
  305. package/src/testing/test-recorder.ts +145 -0
  306. package/src/testing/test-renderer.ts +132 -0
  307. package/src/testing.ts +7 -0
  308. package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +481 -0
  309. package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +19 -0
  310. package/src/tests/__snapshots__/scrollbox.test.ts.snap +29 -0
  311. package/src/tests/absolute-positioning.snapshot.test.ts +638 -0
  312. package/src/tests/allocator-stats.test.ts +38 -0
  313. package/src/tests/destroy-during-render.test.ts +200 -0
  314. package/src/tests/destroy-on-exit.fixture.ts +36 -0
  315. package/src/tests/destroy-on-exit.test.ts +41 -0
  316. package/src/tests/hover-cursor.test.ts +98 -0
  317. package/src/tests/native-span-feed-async.test.ts +173 -0
  318. package/src/tests/native-span-feed-close.test.ts +120 -0
  319. package/src/tests/native-span-feed-coverage.test.ts +227 -0
  320. package/src/tests/native-span-feed-edge-cases.test.ts +352 -0
  321. package/src/tests/native-span-feed-use-after-free.test.ts +45 -0
  322. package/src/tests/opacity.test.ts +123 -0
  323. package/src/tests/renderable.snapshot.test.ts +524 -0
  324. package/src/tests/renderable.test.ts +1281 -0
  325. package/src/tests/renderer.clock.test.ts +158 -0
  326. package/src/tests/renderer.console-startup.test.ts +185 -0
  327. package/src/tests/renderer.control.test.ts +425 -0
  328. package/src/tests/renderer.core-slot-binding.test.ts +952 -0
  329. package/src/tests/renderer.cursor.test.ts +26 -0
  330. package/src/tests/renderer.destroy-during-render.test.ts +147 -0
  331. package/src/tests/renderer.focus-restore.test.ts +257 -0
  332. package/src/tests/renderer.focus.test.ts +294 -0
  333. package/src/tests/renderer.idle.test.ts +219 -0
  334. package/src/tests/renderer.input.test.ts +2237 -0
  335. package/src/tests/renderer.kitty-flags.test.ts +195 -0
  336. package/src/tests/renderer.mouse.test.ts +1274 -0
  337. package/src/tests/renderer.palette.test.ts +629 -0
  338. package/src/tests/renderer.selection.test.ts +49 -0
  339. package/src/tests/renderer.slot-registry.test.ts +684 -0
  340. package/src/tests/renderer.useMouse.test.ts +47 -0
  341. package/src/tests/runtime-plugin-node-modules-cycle.fixture.ts +76 -0
  342. package/src/tests/runtime-plugin-node-modules-mjs.fixture.ts +43 -0
  343. package/src/tests/runtime-plugin-node-modules-no-bare-rewrite.fixture.ts +67 -0
  344. package/src/tests/runtime-plugin-node-modules-package-type-cache.fixture.ts +72 -0
  345. package/src/tests/runtime-plugin-node-modules-runtime-specifier.fixture.ts +44 -0
  346. package/src/tests/runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts +85 -0
  347. package/src/tests/runtime-plugin-path-alias.fixture.ts +43 -0
  348. package/src/tests/runtime-plugin-resolve-roots.fixture.ts +65 -0
  349. package/src/tests/runtime-plugin-support.fixture.ts +11 -0
  350. package/src/tests/runtime-plugin-support.test.ts +19 -0
  351. package/src/tests/runtime-plugin-windows-file-url.fixture.ts +30 -0
  352. package/src/tests/runtime-plugin.fixture.ts +40 -0
  353. package/src/tests/runtime-plugin.test.ts +354 -0
  354. package/src/tests/scrollbox-culling-bug.test.ts +114 -0
  355. package/src/tests/scrollbox-hitgrid-resize.test.ts +136 -0
  356. package/src/tests/scrollbox-hitgrid.test.ts +909 -0
  357. package/src/tests/scrollbox.test.ts +1530 -0
  358. package/src/tests/wrap-resize-perf.test.ts +276 -0
  359. package/src/tests/yoga-setters.test.ts +921 -0
  360. package/src/text-buffer-view.test.ts +705 -0
  361. package/src/text-buffer-view.ts +189 -0
  362. package/src/text-buffer.test.ts +347 -0
  363. package/src/text-buffer.ts +250 -0
  364. package/src/types.ts +161 -0
  365. package/src/utils.ts +88 -0
  366. package/src/zig/ansi.zig +268 -0
  367. package/src/zig/bench/README.md +50 -0
  368. package/src/zig/bench/buffer-draw-text-buffer_bench.zig +887 -0
  369. package/src/zig/bench/edit-buffer_bench.zig +476 -0
  370. package/src/zig/bench/native-span-feed_bench.zig +100 -0
  371. package/src/zig/bench/rope-markers_bench.zig +713 -0
  372. package/src/zig/bench/rope_bench.zig +514 -0
  373. package/src/zig/bench/styled-text_bench.zig +470 -0
  374. package/src/zig/bench/text-buffer-coords_bench.zig +362 -0
  375. package/src/zig/bench/text-buffer-view_bench.zig +459 -0
  376. package/src/zig/bench/text-chunk-graphemes_bench.zig +273 -0
  377. package/src/zig/bench/utf8_bench.zig +799 -0
  378. package/src/zig/bench-utils.zig +431 -0
  379. package/src/zig/bench.zig +217 -0
  380. package/src/zig/buffer-methods.zig +211 -0
  381. package/src/zig/buffer.zig +2281 -0
  382. package/src/zig/build.zig +289 -0
  383. package/src/zig/build.zig.zon +16 -0
  384. package/src/zig/edit-buffer.zig +825 -0
  385. package/src/zig/editor-view.zig +802 -0
  386. package/src/zig/event-bus.zig +13 -0
  387. package/src/zig/event-emitter.zig +65 -0
  388. package/src/zig/file-logger.zig +92 -0
  389. package/src/zig/grapheme.zig +599 -0
  390. package/src/zig/lib.zig +1854 -0
  391. package/src/zig/link.zig +333 -0
  392. package/src/zig/logger.zig +43 -0
  393. package/src/zig/mem-registry.zig +125 -0
  394. package/src/zig/native-span-feed-bench-lib.zig +7 -0
  395. package/src/zig/native-span-feed.zig +708 -0
  396. package/src/zig/renderer.zig +1393 -0
  397. package/src/zig/rope.zig +1220 -0
  398. package/src/zig/syntax-style.zig +161 -0
  399. package/src/zig/terminal.zig +987 -0
  400. package/src/zig/test.zig +72 -0
  401. package/src/zig/tests/README.md +18 -0
  402. package/src/zig/tests/buffer-methods_test.zig +1109 -0
  403. package/src/zig/tests/buffer_test.zig +2557 -0
  404. package/src/zig/tests/edit-buffer-history_test.zig +271 -0
  405. package/src/zig/tests/edit-buffer_test.zig +1689 -0
  406. package/src/zig/tests/editor-view_test.zig +3299 -0
  407. package/src/zig/tests/event-emitter_test.zig +249 -0
  408. package/src/zig/tests/grapheme_test.zig +1304 -0
  409. package/src/zig/tests/link_test.zig +190 -0
  410. package/src/zig/tests/mem-registry_test.zig +473 -0
  411. package/src/zig/tests/memory_leak_regression_test.zig +159 -0
  412. package/src/zig/tests/native-span-feed_test.zig +1264 -0
  413. package/src/zig/tests/renderer_test.zig +1017 -0
  414. package/src/zig/tests/rope-nested_test.zig +712 -0
  415. package/src/zig/tests/rope_fuzz_test.zig +238 -0
  416. package/src/zig/tests/rope_test.zig +2362 -0
  417. package/src/zig/tests/segment-merge.test.zig +148 -0
  418. package/src/zig/tests/syntax-style_test.zig +557 -0
  419. package/src/zig/tests/terminal_test.zig +754 -0
  420. package/src/zig/tests/text-buffer-drawing_test.zig +3237 -0
  421. package/src/zig/tests/text-buffer-highlights_test.zig +666 -0
  422. package/src/zig/tests/text-buffer-iterators_test.zig +776 -0
  423. package/src/zig/tests/text-buffer-segment_test.zig +320 -0
  424. package/src/zig/tests/text-buffer-selection_test.zig +1035 -0
  425. package/src/zig/tests/text-buffer-selection_viewport_test.zig +358 -0
  426. package/src/zig/tests/text-buffer-view_test.zig +3649 -0
  427. package/src/zig/tests/text-buffer_test.zig +2191 -0
  428. package/src/zig/tests/unicode-width-map.zon +3909 -0
  429. package/src/zig/tests/utf8_no_zwj_test.zig +260 -0
  430. package/src/zig/tests/utf8_test.zig +4057 -0
  431. package/src/zig/tests/utf8_wcwidth_cursor_test.zig +267 -0
  432. package/src/zig/tests/utf8_wcwidth_test.zig +357 -0
  433. package/src/zig/tests/word-wrap-editing_test.zig +498 -0
  434. package/src/zig/tests/wrap-cache-perf_test.zig +113 -0
  435. package/src/zig/text-buffer-iterators.zig +499 -0
  436. package/src/zig/text-buffer-segment.zig +404 -0
  437. package/src/zig/text-buffer-view.zig +1371 -0
  438. package/src/zig/text-buffer.zig +1180 -0
  439. package/src/zig/utf8.zig +1948 -0
  440. package/src/zig/utils.zig +9 -0
  441. package/src/zig-structs.ts +261 -0
  442. package/src/zig.ts +3884 -0
  443. package/tsconfig.build.json +24 -0
  444. package/tsconfig.json +27 -0
  445. package/3d/SpriteResourceManager.d.ts +0 -74
  446. package/3d/SpriteUtils.d.ts +0 -13
  447. package/3d/TextureUtils.d.ts +0 -24
  448. package/3d/ThreeRenderable.d.ts +0 -40
  449. package/3d/WGPURenderer.d.ts +0 -61
  450. package/3d/animation/ExplodingSpriteEffect.d.ts +0 -71
  451. package/3d/animation/PhysicsExplodingSpriteEffect.d.ts +0 -76
  452. package/3d/animation/SpriteAnimator.d.ts +0 -124
  453. package/3d/animation/SpriteParticleGenerator.d.ts +0 -62
  454. package/3d/canvas.d.ts +0 -44
  455. package/3d/index.d.ts +0 -12
  456. package/3d/physics/PlanckPhysicsAdapter.d.ts +0 -19
  457. package/3d/physics/RapierPhysicsAdapter.d.ts +0 -19
  458. package/3d/physics/physics-interface.d.ts +0 -27
  459. package/3d.d.ts +0 -2
  460. package/3d.js +0 -34041
  461. package/3d.js.map +0 -155
  462. package/LICENSE +0 -21
  463. package/NativeSpanFeed.d.ts +0 -41
  464. package/Renderable.d.ts +0 -334
  465. package/animation/Timeline.d.ts +0 -126
  466. package/ansi.d.ts +0 -13
  467. package/buffer.d.ts +0 -111
  468. package/console.d.ts +0 -144
  469. package/edit-buffer.d.ts +0 -98
  470. package/editor-view.d.ts +0 -73
  471. package/index-8fks7yv1.js +0 -411
  472. package/index-8fks7yv1.js.map +0 -10
  473. package/index-egy5e2rs.js +0 -12267
  474. package/index-egy5e2rs.js.map +0 -42
  475. package/index-tse8gzh0.js +0 -20614
  476. package/index-tse8gzh0.js.map +0 -67
  477. package/index.d.ts +0 -23
  478. package/index.js +0 -478
  479. package/index.js.map +0 -9
  480. package/lib/KeyHandler.d.ts +0 -61
  481. package/lib/RGBA.d.ts +0 -25
  482. package/lib/ascii.font.d.ts +0 -508
  483. package/lib/border.d.ts +0 -51
  484. package/lib/bunfs.d.ts +0 -7
  485. package/lib/clipboard.d.ts +0 -17
  486. package/lib/clock.d.ts +0 -15
  487. package/lib/data-paths.d.ts +0 -26
  488. package/lib/debounce.d.ts +0 -42
  489. package/lib/detect-links.d.ts +0 -6
  490. package/lib/env.d.ts +0 -42
  491. package/lib/extmarks-history.d.ts +0 -17
  492. package/lib/extmarks.d.ts +0 -89
  493. package/lib/hast-styled-text.d.ts +0 -17
  494. package/lib/index.d.ts +0 -21
  495. package/lib/keymapping.d.ts +0 -25
  496. package/lib/objects-in-viewport.d.ts +0 -24
  497. package/lib/output.capture.d.ts +0 -24
  498. package/lib/parse.keypress-kitty.d.ts +0 -2
  499. package/lib/parse.keypress.d.ts +0 -26
  500. package/lib/parse.mouse.d.ts +0 -30
  501. package/lib/paste.d.ts +0 -7
  502. package/lib/queue.d.ts +0 -15
  503. package/lib/renderable.validations.d.ts +0 -12
  504. package/lib/scroll-acceleration.d.ts +0 -43
  505. package/lib/selection.d.ts +0 -63
  506. package/lib/singleton.d.ts +0 -7
  507. package/lib/stdin-parser.d.ts +0 -87
  508. package/lib/styled-text.d.ts +0 -63
  509. package/lib/terminal-capability-detection.d.ts +0 -30
  510. package/lib/terminal-palette.d.ts +0 -50
  511. package/lib/tree-sitter/assets/update.d.ts +0 -11
  512. package/lib/tree-sitter/client.d.ts +0 -47
  513. package/lib/tree-sitter/default-parsers.d.ts +0 -2
  514. package/lib/tree-sitter/download-utils.d.ts +0 -21
  515. package/lib/tree-sitter/index.d.ts +0 -8
  516. package/lib/tree-sitter/parser.worker.d.ts +0 -1
  517. package/lib/tree-sitter/parsers-config.d.ts +0 -53
  518. package/lib/tree-sitter/resolve-ft.d.ts +0 -5
  519. package/lib/tree-sitter/types.d.ts +0 -82
  520. package/lib/tree-sitter-styled-text.d.ts +0 -14
  521. package/lib/validate-dir-name.d.ts +0 -1
  522. package/lib/yoga.options.d.ts +0 -32
  523. package/parser.worker.js +0 -899
  524. package/parser.worker.js.map +0 -12
  525. package/plugins/core-slot.d.ts +0 -72
  526. package/plugins/registry.d.ts +0 -42
  527. package/plugins/types.d.ts +0 -34
  528. package/post/effects.d.ts +0 -147
  529. package/post/filters.d.ts +0 -65
  530. package/post/matrices.d.ts +0 -20
  531. package/renderables/ASCIIFont.d.ts +0 -52
  532. package/renderables/Box.d.ts +0 -81
  533. package/renderables/Code.d.ts +0 -78
  534. package/renderables/Diff.d.ts +0 -142
  535. package/renderables/EditBufferRenderable.d.ts +0 -237
  536. package/renderables/FrameBuffer.d.ts +0 -16
  537. package/renderables/Input.d.ts +0 -67
  538. package/renderables/LineNumberRenderable.d.ts +0 -78
  539. package/renderables/Markdown.d.ts +0 -185
  540. package/renderables/ScrollBar.d.ts +0 -77
  541. package/renderables/ScrollBox.d.ts +0 -124
  542. package/renderables/Select.d.ts +0 -115
  543. package/renderables/Slider.d.ts +0 -47
  544. package/renderables/TabSelect.d.ts +0 -96
  545. package/renderables/Text.d.ts +0 -36
  546. package/renderables/TextBufferRenderable.d.ts +0 -105
  547. package/renderables/TextNode.d.ts +0 -91
  548. package/renderables/TextTable.d.ts +0 -140
  549. package/renderables/Textarea.d.ts +0 -63
  550. package/renderables/TimeToFirstDraw.d.ts +0 -24
  551. package/renderables/__tests__/renderable-test-utils.d.ts +0 -12
  552. package/renderables/composition/VRenderable.d.ts +0 -16
  553. package/renderables/composition/constructs.d.ts +0 -35
  554. package/renderables/composition/vnode.d.ts +0 -46
  555. package/renderables/index.d.ts +0 -23
  556. package/renderables/markdown-parser.d.ts +0 -10
  557. package/renderer.d.ts +0 -419
  558. package/runtime-plugin-support.d.ts +0 -3
  559. package/runtime-plugin-support.js +0 -29
  560. package/runtime-plugin-support.js.map +0 -10
  561. package/runtime-plugin.d.ts +0 -16
  562. package/runtime-plugin.js +0 -16
  563. package/runtime-plugin.js.map +0 -9
  564. package/syntax-style.d.ts +0 -54
  565. package/testing/manual-clock.d.ts +0 -17
  566. package/testing/mock-keys.d.ts +0 -81
  567. package/testing/mock-mouse.d.ts +0 -38
  568. package/testing/mock-tree-sitter-client.d.ts +0 -23
  569. package/testing/spy.d.ts +0 -7
  570. package/testing/test-recorder.d.ts +0 -61
  571. package/testing/test-renderer.d.ts +0 -23
  572. package/testing.d.ts +0 -6
  573. package/testing.js +0 -697
  574. package/testing.js.map +0 -15
  575. package/text-buffer-view.d.ts +0 -42
  576. package/text-buffer.d.ts +0 -67
  577. package/types.d.ts +0 -139
  578. package/utils.d.ts +0 -14
  579. package/zig-structs.d.ts +0 -155
  580. package/zig.d.ts +0 -353
  581. /package/{assets → src/lib/tree-sitter/assets}/javascript/highlights.scm +0 -0
  582. /package/{assets → src/lib/tree-sitter/assets}/javascript/tree-sitter-javascript.wasm +0 -0
  583. /package/{assets → src/lib/tree-sitter/assets}/markdown/highlights.scm +0 -0
  584. /package/{assets → src/lib/tree-sitter/assets}/markdown/injections.scm +0 -0
  585. /package/{assets → src/lib/tree-sitter/assets}/markdown/tree-sitter-markdown.wasm +0 -0
  586. /package/{assets → src/lib/tree-sitter/assets}/markdown_inline/highlights.scm +0 -0
  587. /package/{assets → src/lib/tree-sitter/assets}/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
  588. /package/{assets → src/lib/tree-sitter/assets}/typescript/highlights.scm +0 -0
  589. /package/{assets → src/lib/tree-sitter/assets}/typescript/tree-sitter-typescript.wasm +0 -0
  590. /package/{assets → src/lib/tree-sitter/assets}/zig/highlights.scm +0 -0
  591. /package/{assets → src/lib/tree-sitter/assets}/zig/tree-sitter-zig.wasm +0 -0
@@ -0,0 +1,2709 @@
1
+ import { expect, describe, it, beforeEach, afterEach } from "bun:test"
2
+ import { createTimeline, Timeline, type JSAnimation, engine, type EasingFunctions } from "./Timeline.js"
3
+
4
+ describe("Timeline", () => {
5
+ let timeline: Timeline
6
+ let target: { x: number; y: number; value: number }
7
+ let updateCallbacks: JSAnimation[]
8
+
9
+ beforeEach(() => {
10
+ target = { x: 0, y: 0, value: 0 }
11
+ updateCallbacks = []
12
+ })
13
+
14
+ afterEach(() => {
15
+ engine.clear()
16
+ })
17
+
18
+ describe("Basic Animation", () => {
19
+ it("should animate a single property", () => {
20
+ timeline = createTimeline({ duration: 1000, autoplay: false })
21
+
22
+ timeline.add(target, {
23
+ x: 100,
24
+ duration: 1000,
25
+ onUpdate: (anim: JSAnimation) => updateCallbacks.push(anim),
26
+ })
27
+
28
+ timeline.play()
29
+
30
+ engine.update(0)
31
+ expect(target.x).toBe(0)
32
+
33
+ engine.update(500)
34
+ expect(target.x).toBe(50)
35
+
36
+ engine.update(500)
37
+ expect(target.x).toBe(100)
38
+ expect(updateCallbacks.length).toBeGreaterThan(0)
39
+ })
40
+
41
+ it("should animate multiple properties", () => {
42
+ timeline = createTimeline({ duration: 1000, autoplay: false })
43
+
44
+ timeline.add(target, {
45
+ x: 100,
46
+ y: 200,
47
+ duration: 1000,
48
+ })
49
+
50
+ timeline.play()
51
+ engine.update(500)
52
+
53
+ expect(target.x).toBe(50)
54
+ expect(target.y).toBe(100)
55
+ })
56
+
57
+ it("should handle easing functions", () => {
58
+ timeline = createTimeline({ duration: 1000, autoplay: false })
59
+
60
+ timeline.add(target, {
61
+ x: 100,
62
+ duration: 1000,
63
+ ease: "linear",
64
+ })
65
+
66
+ timeline.play()
67
+ engine.update(500)
68
+
69
+ expect(target.x).toBe(50)
70
+ })
71
+ })
72
+
73
+ describe("Timeline Control", () => {
74
+ beforeEach(() => {
75
+ timeline = createTimeline({ duration: 1000, autoplay: false })
76
+ timeline.add(target, { x: 100, duration: 1000 })
77
+ })
78
+
79
+ it("should start paused when autoplay is false", () => {
80
+ engine.update(500)
81
+ expect(target.x).toBe(0)
82
+ })
83
+
84
+ it("should animate when played", () => {
85
+ timeline.play()
86
+ engine.update(500)
87
+ expect(target.x).toBe(50)
88
+ })
89
+
90
+ it("should pause animation", () => {
91
+ timeline.play()
92
+ engine.update(250)
93
+ expect(target.x).toBe(25)
94
+
95
+ timeline.pause()
96
+ engine.update(250)
97
+ expect(target.x).toBe(25)
98
+ })
99
+
100
+ it("should restart animation", () => {
101
+ timeline.play()
102
+ engine.update(500)
103
+ expect(target.x).toBe(50)
104
+
105
+ timeline.restart()
106
+ engine.update(250)
107
+ expect(target.x).toBe(25)
108
+ })
109
+
110
+ it("should play again when calling play() on a finished non-looping timeline", () => {
111
+ timeline.play()
112
+
113
+ engine.update(1000)
114
+ expect(target.x).toBe(100)
115
+ expect(timeline.isPlaying).toBe(false)
116
+
117
+ timeline.play()
118
+ expect(timeline.isPlaying).toBe(true)
119
+
120
+ engine.update(500)
121
+ expect(target.x).toBe(50)
122
+
123
+ engine.update(500)
124
+ expect(target.x).toBe(100)
125
+ expect(timeline.isPlaying).toBe(false)
126
+ })
127
+
128
+ it("should call onPause callback when timeline is paused", () => {
129
+ let pauseCallCount = 0
130
+ timeline = createTimeline({
131
+ duration: 1000,
132
+ autoplay: false,
133
+ onPause: () => pauseCallCount++,
134
+ })
135
+ timeline.add(target, { x: 100, duration: 1000 })
136
+
137
+ timeline.play()
138
+ engine.update(500)
139
+ expect(target.x).toBe(50)
140
+ expect(pauseCallCount).toBe(0)
141
+
142
+ timeline.pause()
143
+ expect(pauseCallCount).toBe(1)
144
+ expect(timeline.isPlaying).toBe(false)
145
+
146
+ timeline.pause()
147
+ expect(pauseCallCount).toBe(2)
148
+
149
+ timeline.play()
150
+ timeline.pause()
151
+ expect(pauseCallCount).toBe(3)
152
+ })
153
+
154
+ it("should not call onPause callback when timeline is not initialized with one", () => {
155
+ timeline = createTimeline({ duration: 1000, autoplay: false })
156
+ timeline.add(target, { x: 100, duration: 1000 })
157
+
158
+ timeline.play()
159
+ timeline.pause()
160
+
161
+ expect(timeline.isPlaying).toBe(false)
162
+ })
163
+
164
+ it("should not call onPause callback when timeline completes naturally", () => {
165
+ let pauseCallCount = 0
166
+ let completeCallCount = 0
167
+ timeline = createTimeline({
168
+ duration: 1000,
169
+ autoplay: false,
170
+ onPause: () => pauseCallCount++,
171
+ onComplete: () => completeCallCount++,
172
+ })
173
+ timeline.add(target, { x: 100, duration: 500 })
174
+
175
+ timeline.play()
176
+ engine.update(1000)
177
+
178
+ expect(timeline.isPlaying).toBe(false)
179
+ expect(pauseCallCount).toBe(0)
180
+ expect(completeCallCount).toBe(1)
181
+ })
182
+ })
183
+
184
+ describe("Looping", () => {
185
+ it("should loop timeline when loop is true", () => {
186
+ timeline = createTimeline({ duration: 1000, loop: true, autoplay: false })
187
+ timeline.add(target, { x: 100, duration: 1000 })
188
+
189
+ timeline.play()
190
+
191
+ engine.update(1000)
192
+ expect(target.x).toBe(100)
193
+
194
+ engine.update(500)
195
+ expect(target.x).toBe(50)
196
+ })
197
+
198
+ it("should not loop when loop is false", () => {
199
+ timeline = createTimeline({ duration: 1000, loop: false, autoplay: false })
200
+ timeline.add(target, { x: 100, duration: 1000 })
201
+
202
+ timeline.play()
203
+
204
+ engine.update(1000)
205
+ expect(target.x).toBe(100)
206
+ expect(timeline.isPlaying).toBe(false)
207
+
208
+ engine.update(500)
209
+ expect(target.x).toBe(100)
210
+ })
211
+ })
212
+
213
+ describe("Individual Animation Loops", () => {
214
+ it("should loop individual animation specified number of times", () => {
215
+ timeline = createTimeline({ duration: 5000, autoplay: false })
216
+
217
+ let completionCount = 0
218
+ timeline.add(target, {
219
+ x: 100,
220
+ duration: 1000,
221
+ loop: 3,
222
+ onComplete: () => completionCount++,
223
+ })
224
+
225
+ timeline.play()
226
+
227
+ engine.update(1000)
228
+ expect(target.x).toBe(100)
229
+ expect(completionCount).toBe(0)
230
+
231
+ engine.update(1000)
232
+ expect(target.x).toBe(100)
233
+ expect(completionCount).toBe(0)
234
+
235
+ engine.update(1000)
236
+ expect(target.x).toBe(100)
237
+ expect(completionCount).toBe(1)
238
+
239
+ engine.update(1000)
240
+ expect(target.x).toBe(100)
241
+ expect(completionCount).toBe(1)
242
+ })
243
+
244
+ it("should handle loop delay", () => {
245
+ timeline = createTimeline({ duration: 5000, autoplay: false })
246
+
247
+ timeline.add(target, {
248
+ x: 100,
249
+ duration: 1000,
250
+ loop: 2,
251
+ loopDelay: 500,
252
+ })
253
+
254
+ timeline.play()
255
+
256
+ engine.update(1000)
257
+ expect(target.x).toBe(100)
258
+
259
+ engine.update(250)
260
+ expect(target.x).toBe(100)
261
+
262
+ engine.update(250)
263
+ engine.update(500)
264
+ expect(target.x).toBe(50)
265
+ })
266
+ })
267
+
268
+ describe("Alternating Animations", () => {
269
+ it("should alternate direction with each loop", () => {
270
+ timeline = createTimeline({ duration: 5000, autoplay: false })
271
+
272
+ const values: number[] = []
273
+ timeline.add(target, {
274
+ x: 100,
275
+ duration: 1000,
276
+ loop: 3,
277
+ alternate: true,
278
+ onUpdate: (anim: JSAnimation) => {
279
+ values.push(anim.targets[0].x)
280
+ },
281
+ })
282
+
283
+ timeline.play()
284
+
285
+ engine.update(500)
286
+ expect(target.x).toBe(50)
287
+ engine.update(500)
288
+ expect(target.x).toBe(100)
289
+
290
+ engine.update(500)
291
+ expect(target.x).toBe(50)
292
+ engine.update(500)
293
+ expect(target.x).toBe(0)
294
+
295
+ engine.update(500)
296
+ expect(target.x).toBe(50)
297
+ engine.update(500)
298
+ expect(target.x).toBe(100)
299
+ })
300
+
301
+ it("should handle alternating with loop delay", () => {
302
+ timeline = createTimeline({ duration: 5000, autoplay: false })
303
+
304
+ timeline.add(target, {
305
+ x: 100,
306
+ duration: 1000,
307
+ loop: 2,
308
+ alternate: true,
309
+ loopDelay: 500,
310
+ })
311
+
312
+ timeline.play()
313
+
314
+ engine.update(1000)
315
+ expect(target.x).toBe(100)
316
+
317
+ engine.update(500)
318
+ expect(target.x).toBe(100)
319
+
320
+ engine.update(500)
321
+ expect(target.x).toBe(50)
322
+ engine.update(500)
323
+ expect(target.x).toBe(0)
324
+ })
325
+
326
+ it("should handle alternating animations with looping parent timeline", () => {
327
+ timeline = createTimeline({ duration: 3000, loop: true, autoplay: false })
328
+
329
+ const animationValues: { time: number; value: number; loop: number }[] = []
330
+ let mainTimelineLoops = 0
331
+
332
+ timeline.add(
333
+ target,
334
+ {
335
+ x: 100,
336
+ duration: 1000,
337
+ loop: 2,
338
+ alternate: true,
339
+ onUpdate: (anim: JSAnimation) => {
340
+ animationValues.push({
341
+ time: timeline.currentTime,
342
+ value: anim.targets[0].x,
343
+ loop: mainTimelineLoops,
344
+ })
345
+ },
346
+ },
347
+ 500,
348
+ )
349
+
350
+ timeline.play()
351
+
352
+ engine.update(500)
353
+ const firstLoopStartValue = target.x
354
+ engine.update(500)
355
+ engine.update(500)
356
+ engine.update(500)
357
+ engine.update(500)
358
+ engine.update(500)
359
+
360
+ mainTimelineLoops++
361
+
362
+ const secondLoopTime = timeline.currentTime
363
+ engine.update(500)
364
+ const secondLoopStartValue = target.x
365
+
366
+ expect(secondLoopTime).toBe(0)
367
+ expect(secondLoopStartValue).toBe(firstLoopStartValue)
368
+ })
369
+ })
370
+
371
+ describe("Timeline Sync", () => {
372
+ it("should sync sub-timelines to main timeline", () => {
373
+ const mainTimeline = createTimeline({ duration: 3000, autoplay: false })
374
+ const subTimeline = createTimeline({ duration: 1000, autoplay: false })
375
+
376
+ const subTarget = { value: 0 }
377
+ subTimeline.add(subTarget, { value: 100, duration: 1000 })
378
+
379
+ mainTimeline.sync(subTimeline, 1000)
380
+ mainTimeline.play()
381
+
382
+ engine.update(500)
383
+ expect(subTarget.value).toBe(0)
384
+
385
+ engine.update(500)
386
+ expect(subTarget.value).toBe(0)
387
+
388
+ engine.update(500)
389
+ expect(subTarget.value).toBe(50)
390
+
391
+ engine.update(500)
392
+ expect(subTarget.value).toBe(100)
393
+
394
+ engine.update(500)
395
+ expect(subTarget.value).toBe(100)
396
+ })
397
+
398
+ it("should restart completed sub-timelines when main timeline loops", () => {
399
+ const mainTimeline = createTimeline({ duration: 1000, loop: true, autoplay: false })
400
+ const subTimeline = createTimeline({ duration: 300, autoplay: false })
401
+
402
+ const subTarget = { value: 0 }
403
+ let subCompleteCount = 0
404
+
405
+ subTimeline.add(subTarget, {
406
+ value: 100,
407
+ duration: 300,
408
+ onComplete: () => subCompleteCount++,
409
+ })
410
+
411
+ mainTimeline.sync(subTimeline, 200)
412
+ mainTimeline.play()
413
+
414
+ engine.update(200)
415
+ expect(subTarget.value).toBe(0)
416
+
417
+ engine.update(150)
418
+ expect(subTarget.value).toBe(50)
419
+
420
+ engine.update(150)
421
+ expect(subTarget.value).toBe(100)
422
+ expect(subCompleteCount).toBe(1)
423
+ expect(subTimeline.isPlaying).toBe(false)
424
+
425
+ engine.update(500)
426
+
427
+ expect(mainTimeline.currentTime).toBe(0)
428
+ expect(subTarget.value).toBe(100)
429
+ expect(subTimeline.isPlaying).toBe(false)
430
+
431
+ engine.update(200)
432
+ expect(subTimeline.isPlaying).toBe(true)
433
+
434
+ engine.update(150)
435
+ expect(subTarget.value).toBe(50)
436
+
437
+ engine.update(150)
438
+ expect(subTarget.value).toBe(100)
439
+ expect(subCompleteCount).toBe(2)
440
+ })
441
+
442
+ it("should preserve initial values for looping sub-timeline when main timeline does not loop", () => {
443
+ const mainTimeline = createTimeline({ duration: 5000, loop: false, autoplay: false })
444
+ const subTimeline = createTimeline({ duration: 1000, loop: true, autoplay: false })
445
+
446
+ const subTarget = { x: 10, y: 20 }
447
+ const capturedStates: Array<{ x: number; y: number; time: number; loop: number }> = []
448
+ let subLoopCount = 0
449
+
450
+ subTarget.x = 50
451
+ subTarget.y = 80
452
+
453
+ subTimeline.add(subTarget, {
454
+ x: 200,
455
+ y: 300,
456
+ duration: 1000,
457
+ onUpdate: (anim: JSAnimation) => {
458
+ capturedStates.push({
459
+ x: anim.targets[0].x,
460
+ y: anim.targets[0].y,
461
+ time: mainTimeline.currentTime,
462
+ loop: subLoopCount,
463
+ })
464
+ },
465
+ onComplete: () => {
466
+ subLoopCount++
467
+ },
468
+ })
469
+
470
+ mainTimeline.sync(subTimeline, 1500)
471
+ mainTimeline.play()
472
+
473
+ engine.update(1000)
474
+ expect(subTarget.x).toBe(50)
475
+ expect(subTarget.y).toBe(80)
476
+ expect(capturedStates).toHaveLength(0)
477
+
478
+ engine.update(750)
479
+ expect(capturedStates.length).toBeGreaterThan(0)
480
+
481
+ const firstLoopMidpoint = capturedStates.find((state) => state.loop === 0)
482
+ expect(firstLoopMidpoint).toBeDefined()
483
+ expect(firstLoopMidpoint!.x).toBeGreaterThan(50)
484
+ expect(firstLoopMidpoint!.x).toBeLessThan(200)
485
+ expect(firstLoopMidpoint!.y).toBeGreaterThan(80)
486
+ expect(firstLoopMidpoint!.y).toBeLessThan(300)
487
+
488
+ engine.update(750)
489
+ expect(subTarget.x).toBe(200)
490
+ expect(subTarget.y).toBe(300)
491
+ expect(subLoopCount).toBe(1)
492
+
493
+ engine.update(500)
494
+
495
+ const secondLoopMidpoint = capturedStates.find((state) => state.loop === 1 && state.time >= 2500)
496
+ expect(secondLoopMidpoint).toBeDefined()
497
+
498
+ expect(secondLoopMidpoint!.x).toBeGreaterThan(50)
499
+ expect(secondLoopMidpoint!.x).toBeLessThan(200)
500
+ expect(secondLoopMidpoint!.y).toBeGreaterThan(80)
501
+ expect(secondLoopMidpoint!.y).toBeLessThan(300)
502
+
503
+ engine.update(500)
504
+ expect(subTarget.x).toBe(200)
505
+ expect(subTarget.y).toBe(300)
506
+ expect(subLoopCount).toBe(2)
507
+
508
+ engine.update(500)
509
+
510
+ const thirdLoopMidpoint = capturedStates.find((state) => state.loop === 2 && state.time >= 3500)
511
+ expect(thirdLoopMidpoint).toBeDefined()
512
+
513
+ expect(thirdLoopMidpoint!.x).toBeGreaterThan(50)
514
+ expect(thirdLoopMidpoint!.x).toBeLessThan(200)
515
+ expect(thirdLoopMidpoint!.y).toBeGreaterThan(80)
516
+ expect(thirdLoopMidpoint!.y).toBeLessThan(300)
517
+
518
+ engine.update(1000)
519
+ expect(mainTimeline.isPlaying).toBe(false)
520
+ expect(subLoopCount).toBeGreaterThanOrEqual(2)
521
+ })
522
+
523
+ it("should pause sub-timelines when main timeline is paused", () => {
524
+ const mainTimeline = createTimeline({ duration: 3000, autoplay: false })
525
+ const subTimeline = createTimeline({ duration: 1000, autoplay: false })
526
+
527
+ const mainTarget = { x: 0 }
528
+ const subTarget = { value: 0 }
529
+
530
+ mainTimeline.add(mainTarget, { x: 100, duration: 2000 })
531
+ subTimeline.add(subTarget, { value: 50, duration: 800 })
532
+
533
+ mainTimeline.sync(subTimeline, 500)
534
+ mainTimeline.play()
535
+
536
+ engine.update(250)
537
+ expect(mainTarget.x).toBe(12.5)
538
+ expect(subTarget.value).toBe(0)
539
+ expect(mainTimeline.isPlaying).toBe(true)
540
+ expect(subTimeline.isPlaying).toBe(false)
541
+
542
+ engine.update(500)
543
+ expect(mainTarget.x).toBe(37.5)
544
+ expect(subTarget.value).toBe(15.625)
545
+ expect(mainTimeline.isPlaying).toBe(true)
546
+ expect(subTimeline.isPlaying).toBe(true)
547
+
548
+ mainTimeline.pause()
549
+ expect(mainTimeline.isPlaying).toBe(false)
550
+ expect(subTimeline.isPlaying).toBe(false)
551
+
552
+ engine.update(400)
553
+ expect(mainTarget.x).toBe(37.5)
554
+ expect(subTarget.value).toBe(15.625)
555
+ expect(subTimeline.isPlaying).toBe(false)
556
+
557
+ mainTimeline.play()
558
+ expect(mainTimeline.isPlaying).toBe(true)
559
+ expect(subTimeline.isPlaying).toBe(true)
560
+
561
+ engine.update(200)
562
+ expect(mainTarget.x).toBe(47.5)
563
+ expect(subTarget.value).toBe(28.125)
564
+ expect(subTimeline.isPlaying).toBe(true)
565
+ })
566
+ })
567
+
568
+ describe("Callbacks", () => {
569
+ it("should execute call callbacks at specified times", () => {
570
+ timeline = createTimeline({ duration: 2000, autoplay: false })
571
+
572
+ const callTimes: number[] = []
573
+ timeline.call(() => callTimes.push(0), 0)
574
+ timeline.call(() => callTimes.push(1000), 1000)
575
+ timeline.call(() => callTimes.push(1500), 1500)
576
+
577
+ timeline.play()
578
+
579
+ engine.update(500)
580
+ expect(callTimes).toEqual([0])
581
+
582
+ engine.update(500)
583
+ expect(callTimes).toEqual([0, 1000])
584
+
585
+ engine.update(500)
586
+ expect(callTimes).toEqual([0, 1000, 1500])
587
+ })
588
+
589
+ it("should support string startTime parameters", () => {
590
+ timeline = createTimeline({ duration: 1000, autoplay: false })
591
+
592
+ const callTimes: string[] = []
593
+ timeline.call(() => callTimes.push("start"), "start")
594
+ timeline.add(target, { x: 100, duration: 1000 }, "start")
595
+
596
+ timeline.play()
597
+ engine.update(500)
598
+
599
+ expect(callTimes).toEqual(["start"])
600
+ expect(target.x).toBe(50)
601
+ })
602
+
603
+ it("should trigger onStart callback correctly", () => {
604
+ timeline = createTimeline({ duration: 1000, autoplay: false })
605
+ let started = false
606
+ timeline.add(
607
+ target,
608
+ {
609
+ x: 100,
610
+ duration: 500,
611
+ onStart: () => {
612
+ started = true
613
+ },
614
+ },
615
+ 200,
616
+ )
617
+
618
+ timeline.play()
619
+ expect(started).toBe(false)
620
+
621
+ engine.update(100)
622
+ expect(started).toBe(false)
623
+ expect(target.x).toBe(0)
624
+
625
+ engine.update(150)
626
+ expect(started).toBe(true)
627
+ expect(target.x).toBe(10)
628
+ })
629
+
630
+ it("should trigger onLoop callback correctly for individual animation loops", () => {
631
+ timeline = createTimeline({ duration: 5000, autoplay: false })
632
+ let loopCount = 0
633
+ let completeCount = 0
634
+ timeline.add(target, {
635
+ x: 100,
636
+ duration: 500,
637
+ loop: 3,
638
+ loopDelay: 100,
639
+ onLoop: () => {
640
+ loopCount++
641
+ },
642
+ onComplete: () => {
643
+ completeCount++
644
+ },
645
+ })
646
+
647
+ timeline.play()
648
+
649
+ engine.update(500)
650
+ expect(target.x).toBe(100)
651
+ expect(loopCount).toBe(0)
652
+ engine.update(100)
653
+ expect(loopCount).toBe(1)
654
+
655
+ engine.update(500)
656
+ expect(target.x).toBe(100)
657
+ expect(loopCount).toBe(1)
658
+ engine.update(100)
659
+ expect(loopCount).toBe(2)
660
+
661
+ engine.update(500)
662
+ expect(target.x).toBe(100)
663
+ expect(loopCount).toBe(2)
664
+ expect(completeCount).toBe(1)
665
+ })
666
+ })
667
+
668
+ describe("Complex Looping Scenarios", () => {
669
+ it("should correctly reset and re-run finite-looped animation when parent timeline loops", () => {
670
+ timeline = createTimeline({ duration: 2000, loop: true, autoplay: false })
671
+
672
+ let animLoopCount = 0
673
+ let animCompleteCount = 0
674
+ let animStartCount = 0
675
+
676
+ timeline.add(
677
+ target,
678
+ {
679
+ x: 100,
680
+ duration: 500,
681
+ loop: 2,
682
+ loopDelay: 100,
683
+ onStart: () => animStartCount++,
684
+ onLoop: () => animLoopCount++,
685
+ onComplete: () => animCompleteCount++,
686
+ },
687
+ 500,
688
+ )
689
+
690
+ timeline.play()
691
+
692
+ engine.update(500)
693
+ expect(animStartCount).toBe(1)
694
+ engine.update(500)
695
+ expect(target.x).toBe(100)
696
+ expect(animLoopCount).toBe(0)
697
+ engine.update(100)
698
+ expect(animLoopCount).toBe(1)
699
+
700
+ engine.update(500)
701
+ expect(target.x).toBe(100)
702
+ expect(animLoopCount).toBe(1)
703
+ expect(animCompleteCount).toBe(1)
704
+ engine.update(100)
705
+ expect(animLoopCount).toBe(1)
706
+ expect(animCompleteCount).toBe(1)
707
+
708
+ engine.update(300)
709
+ expect(target.x).toBe(100)
710
+ expect(animCompleteCount).toBe(1)
711
+
712
+ expect(timeline.currentTime).toBe(0)
713
+
714
+ engine.update(500)
715
+ expect(animStartCount).toBe(2)
716
+ expect(target.x).toBe(0)
717
+
718
+ engine.update(500)
719
+ expect(target.x).toBe(100)
720
+ expect(animLoopCount).toBe(1)
721
+ engine.update(100)
722
+ expect(animLoopCount).toBe(2)
723
+
724
+ engine.update(500)
725
+ expect(target.x).toBe(100)
726
+ expect(animLoopCount).toBe(2)
727
+ expect(animCompleteCount).toBe(2)
728
+ })
729
+ })
730
+
731
+ describe("Timing Precision", () => {
732
+ describe("Animation Start Time Overshoot", () => {
733
+ it("should account for overshoot when animation starts late", () => {
734
+ timeline = createTimeline({ duration: 2000, autoplay: false })
735
+
736
+ timeline.add(
737
+ target,
738
+ {
739
+ x: 100,
740
+ duration: 1000,
741
+ ease: "linear",
742
+ },
743
+ 50,
744
+ )
745
+
746
+ timeline.play()
747
+
748
+ engine.update(66)
749
+ expect(target.x).toBeCloseTo(1.6, 1)
750
+ })
751
+
752
+ it("should handle multiple animations with different start time overshoots", () => {
753
+ timeline = createTimeline({ duration: 3000, autoplay: false })
754
+
755
+ const target1 = { x: 0 }
756
+ const target2 = { y: 0 }
757
+
758
+ timeline.add(target1, { x: 100, duration: 1000, ease: "linear" }, 30)
759
+ timeline.add(target2, { y: 200, duration: 1000, ease: "linear" }, 80)
760
+
761
+ timeline.play()
762
+ engine.update(100)
763
+
764
+ expect(target1.x).toBeCloseTo(7, 1)
765
+ expect(target2.y).toBeCloseTo(4, 1)
766
+ })
767
+
768
+ it("should handle zero duration animations with overshoot", () => {
769
+ timeline = createTimeline({ duration: 1000, autoplay: false })
770
+
771
+ timeline.add(target, { x: 100, duration: 0 }, 50)
772
+
773
+ timeline.play()
774
+ engine.update(66)
775
+
776
+ expect(target.x).toBe(100)
777
+ })
778
+ })
779
+
780
+ describe("Loop Delay Precision", () => {
781
+ it("should account for overshoot in loop delays", () => {
782
+ timeline = createTimeline({ duration: 5000, autoplay: false })
783
+
784
+ const values: number[] = []
785
+ timeline.add(target, {
786
+ x: 100,
787
+ duration: 1000,
788
+ loop: 3,
789
+ loopDelay: 500,
790
+ ease: "linear",
791
+ onUpdate: (anim: JSAnimation) => values.push(anim.targets[0].x),
792
+ })
793
+
794
+ timeline.play()
795
+
796
+ engine.update(1000)
797
+ expect(target.x).toBe(100)
798
+
799
+ engine.update(516)
800
+ expect(target.x).toBeCloseTo(1.6, 1)
801
+ })
802
+
803
+ it("should handle multiple loop delay overshoots", () => {
804
+ timeline = createTimeline({ duration: 10000, autoplay: false })
805
+
806
+ timeline.add(target, {
807
+ x: 100,
808
+ duration: 1000,
809
+ loop: 4,
810
+ loopDelay: 300,
811
+ ease: "linear",
812
+ })
813
+
814
+ timeline.play()
815
+
816
+ engine.update(1000)
817
+ expect(target.x).toBe(100)
818
+
819
+ engine.update(333)
820
+ expect(target.x).toBeCloseTo(3.3, 1)
821
+
822
+ engine.update(967)
823
+ expect(target.x).toBe(100)
824
+
825
+ engine.update(350)
826
+ expect(target.x).toBeCloseTo(5, 1)
827
+ })
828
+
829
+ it("should handle alternating animations with loop delay overshoot", () => {
830
+ timeline = createTimeline({ duration: 8000, autoplay: false })
831
+
832
+ timeline.add(target, {
833
+ x: 100,
834
+ duration: 1000,
835
+ loop: 3,
836
+ alternate: true,
837
+ loopDelay: 400,
838
+ ease: "linear",
839
+ })
840
+
841
+ timeline.play()
842
+
843
+ engine.update(1000)
844
+ expect(target.x).toBe(100)
845
+
846
+ engine.update(450)
847
+ expect(target.x).toBe(95)
848
+
849
+ engine.update(950)
850
+ expect(target.x).toBe(0)
851
+
852
+ engine.update(425)
853
+ expect(target.x).toBe(2.5)
854
+ })
855
+ })
856
+
857
+ describe("Synced Timeline Precision", () => {
858
+ it("should account for overshoot when starting synced timelines", () => {
859
+ const mainTimeline = createTimeline({ duration: 3000, autoplay: false })
860
+ const subTimeline = createTimeline({ duration: 1000, autoplay: false })
861
+
862
+ const subTarget = { value: 0 }
863
+ subTimeline.add(subTarget, { value: 100, duration: 1000, ease: "linear" })
864
+
865
+ mainTimeline.sync(subTimeline, 500)
866
+ mainTimeline.play()
867
+
868
+ engine.update(533)
869
+ expect(subTarget.value).toBeCloseTo(3.3, 1)
870
+ })
871
+
872
+ it("should handle multiple synced timelines with different overshoot amounts", () => {
873
+ const mainTimeline = createTimeline({ duration: 5000, autoplay: false })
874
+ const subTimeline1 = createTimeline({ duration: 1000, autoplay: false })
875
+ const subTimeline2 = createTimeline({ duration: 1500, autoplay: false })
876
+
877
+ const subTarget1 = { value: 0 }
878
+ const subTarget2 = { value: 0 }
879
+
880
+ subTimeline1.add(subTarget1, { value: 100, duration: 1000, ease: "linear" })
881
+ subTimeline2.add(subTarget2, { value: 200, duration: 1500, ease: "linear" })
882
+
883
+ mainTimeline.sync(subTimeline1, 300)
884
+ mainTimeline.sync(subTimeline2, 800)
885
+ mainTimeline.play()
886
+
887
+ engine.update(850)
888
+
889
+ expect(subTarget1.value).toBeCloseTo(55, 1)
890
+ expect(subTarget2.value).toBeCloseTo(6.67, 1)
891
+ })
892
+ })
893
+
894
+ describe("Complex Precision Scenarios", () => {
895
+ it("should handle alternating animation with main timeline loop and overshoot", () => {
896
+ timeline = createTimeline({ duration: 3000, loop: true, autoplay: false })
897
+
898
+ timeline.add(
899
+ target,
900
+ {
901
+ x: 100,
902
+ duration: 800,
903
+ loop: 2,
904
+ alternate: true,
905
+ loopDelay: 200,
906
+ ease: "linear",
907
+ },
908
+ 500,
909
+ )
910
+
911
+ timeline.play()
912
+
913
+ engine.update(3100)
914
+
915
+ expect(target.x).toBe(0)
916
+
917
+ engine.update(450)
918
+ expect(target.x).toBe(6.25)
919
+
920
+ engine.update(750 + 250)
921
+ expect(target.x).toBe(93.75)
922
+ })
923
+
924
+ it("should maintain precision across multiple frame updates at 30fps", () => {
925
+ timeline = createTimeline({ duration: 2000, autoplay: false })
926
+
927
+ const frameTime = 33.33
928
+ const values: number[] = []
929
+
930
+ timeline.add(
931
+ target,
932
+ {
933
+ x: 100,
934
+ duration: 1000,
935
+ ease: "linear",
936
+ onUpdate: (anim: JSAnimation) => values.push(anim.targets[0].x),
937
+ },
938
+ 50,
939
+ )
940
+
941
+ timeline.play()
942
+
943
+ engine.update(frameTime)
944
+ expect(target.x).toBe(0)
945
+
946
+ engine.update(frameTime)
947
+ expect(target.x).toBeCloseTo(1.67, 1)
948
+
949
+ engine.update(frameTime)
950
+ expect(target.x).toBeCloseTo(5, 1)
951
+
952
+ for (let i = 0; i < 29; i++) {
953
+ engine.update(frameTime)
954
+ }
955
+
956
+ expect(target.x).toBeCloseTo(100, 0)
957
+ })
958
+ })
959
+ })
960
+
961
+ describe("Edge Cases", () => {
962
+ it("should handle zero duration", () => {
963
+ timeline = createTimeline({ duration: 1000, autoplay: false })
964
+ timeline.add(target, { x: 100, duration: 0 })
965
+
966
+ timeline.play()
967
+ engine.update(1)
968
+
969
+ expect(target.x).toBe(100)
970
+ })
971
+
972
+ it("should handle negative deltaTime gracefully", () => {
973
+ timeline = createTimeline({ duration: 1000, autoplay: false })
974
+ timeline.add(target, { x: 100, duration: 1000 })
975
+
976
+ timeline.play()
977
+ engine.update(-100)
978
+
979
+ expect(target.x).toBe(0)
980
+ })
981
+
982
+ it("should handle very large deltaTime", () => {
983
+ timeline = createTimeline({ duration: 1000, autoplay: false })
984
+ timeline.add(target, { x: 100, duration: 1000 })
985
+
986
+ timeline.play()
987
+ engine.update(10000)
988
+
989
+ expect(target.x).toBe(100)
990
+ })
991
+ })
992
+
993
+ describe("New Easing Function Tests", () => {
994
+ const testCases: { name: EasingFunctions; midValue: number }[] = [
995
+ { name: "inCirc", midValue: 0.13397459621556135 },
996
+ { name: "outCirc", midValue: 0.8660254037844386 },
997
+ { name: "inOutCirc", midValue: 0.5 },
998
+ { name: "inBack", midValue: -0.0876975 },
999
+ { name: "outBack", midValue: 1.0876975 },
1000
+ { name: "inOutBack", midValue: 0.5 },
1001
+ ]
1002
+
1003
+ testCases.forEach((tc) => {
1004
+ it(`should animate correctly with ${tc.name} easing`, () => {
1005
+ timeline = createTimeline({ duration: 1000, autoplay: false })
1006
+ timeline.add(target, { x: 100, duration: 1000, ease: tc.name })
1007
+ timeline.play()
1008
+
1009
+ engine.update(0)
1010
+ expect(target.x).toBeCloseTo(0, 5)
1011
+
1012
+ engine.update(500)
1013
+ if (tc.name === "inBack") {
1014
+ expect(target.x).toBeCloseTo(100 * tc.midValue, 5)
1015
+ } else if (tc.name === "outBack") {
1016
+ expect(target.x).toBeCloseTo(100 * tc.midValue, 5)
1017
+ } else if (tc.name === "inOutCirc" || tc.name === "inOutBack") {
1018
+ expect(target.x).toBeCloseTo(50, 5)
1019
+ } else {
1020
+ expect(target.x).toBeCloseTo(100 * tc.midValue, 5)
1021
+ }
1022
+
1023
+ engine.update(500)
1024
+ expect(target.x).toBeCloseTo(100, 5)
1025
+ })
1026
+ })
1027
+ })
1028
+
1029
+ describe("DeltaTime in onUpdate Callbacks", () => {
1030
+ it("should provide correct deltaTime to onUpdate callbacks", () => {
1031
+ timeline = createTimeline({ duration: 1000, autoplay: false })
1032
+
1033
+ const deltaTimesReceived: number[] = []
1034
+ timeline.add(target, {
1035
+ x: 100,
1036
+ duration: 1000,
1037
+ onUpdate: (anim: JSAnimation) => {
1038
+ deltaTimesReceived.push(anim.deltaTime)
1039
+ },
1040
+ })
1041
+
1042
+ timeline.play()
1043
+
1044
+ engine.update(16)
1045
+ expect(deltaTimesReceived[0]).toBe(16)
1046
+
1047
+ engine.update(33)
1048
+ expect(deltaTimesReceived[1]).toBe(33)
1049
+
1050
+ engine.update(50)
1051
+ expect(deltaTimesReceived[2]).toBe(50)
1052
+ })
1053
+
1054
+ it("should support throttling patterns like the vignette example", () => {
1055
+ timeline = createTimeline({ duration: 2000, autoplay: false })
1056
+
1057
+ let vignetteTime = 0
1058
+ let vignetteUpdateCount = 0
1059
+ const vignetteStrengthValues: number[] = []
1060
+
1061
+ timeline.add(target, {
1062
+ strength: 1.0,
1063
+ duration: 1000,
1064
+ onUpdate: (values: JSAnimation) => {
1065
+ vignetteTime += values.deltaTime
1066
+ if (vignetteTime > 66) {
1067
+ vignetteStrengthValues.push(values.targets[0].strength)
1068
+ vignetteUpdateCount++
1069
+ vignetteTime = 0
1070
+ }
1071
+ },
1072
+ })
1073
+
1074
+ timeline.play()
1075
+
1076
+ for (let i = 0; i < 10; i++) {
1077
+ engine.update(16.67)
1078
+ }
1079
+
1080
+ expect(vignetteUpdateCount).toBeGreaterThan(0)
1081
+ expect(vignetteUpdateCount).toBeLessThan(10)
1082
+ expect(vignetteStrengthValues.length).toBe(vignetteUpdateCount)
1083
+ })
1084
+
1085
+ it("should provide deltaTime across multiple animation loops", () => {
1086
+ timeline = createTimeline({ duration: 5000, autoplay: false })
1087
+
1088
+ const deltaTimesReceived: number[] = []
1089
+ timeline.add(target, {
1090
+ x: 100,
1091
+ duration: 500,
1092
+ loop: 3,
1093
+ loopDelay: 100,
1094
+ onUpdate: (anim: JSAnimation) => {
1095
+ deltaTimesReceived.push(anim.deltaTime)
1096
+ },
1097
+ })
1098
+
1099
+ timeline.play()
1100
+
1101
+ engine.update(25)
1102
+ engine.update(30)
1103
+ engine.update(445)
1104
+ engine.update(35)
1105
+ engine.update(65)
1106
+ engine.update(40)
1107
+
1108
+ expect(deltaTimesReceived).toEqual([25, 30, 445, 35, 65, 40])
1109
+ })
1110
+
1111
+ it("should provide deltaTime to synced sub-timeline animations", () => {
1112
+ const mainTimeline = createTimeline({ duration: 2000, autoplay: false })
1113
+ const subTimeline = createTimeline({ duration: 500, autoplay: false })
1114
+
1115
+ const mainDeltaTimes: number[] = []
1116
+ const subDeltaTimes: number[] = []
1117
+
1118
+ const subTarget = { value: 0 }
1119
+
1120
+ mainTimeline.add(target, {
1121
+ x: 100,
1122
+ duration: 1000,
1123
+ onUpdate: (anim: JSAnimation) => {
1124
+ mainDeltaTimes.push(anim.deltaTime)
1125
+ },
1126
+ })
1127
+
1128
+ subTimeline.add(subTarget, {
1129
+ value: 50,
1130
+ duration: 500,
1131
+ onUpdate: (anim: JSAnimation) => {
1132
+ subDeltaTimes.push(anim.deltaTime)
1133
+ },
1134
+ })
1135
+
1136
+ mainTimeline.sync(subTimeline, 300)
1137
+ mainTimeline.play()
1138
+
1139
+ engine.update(200)
1140
+ expect(mainDeltaTimes).toEqual([200])
1141
+ expect(subDeltaTimes).toEqual([])
1142
+
1143
+ engine.update(150)
1144
+ expect(mainDeltaTimes).toEqual([200, 150])
1145
+ expect(subDeltaTimes).toEqual([50])
1146
+
1147
+ engine.update(100)
1148
+ expect(mainDeltaTimes).toEqual([200, 150, 100])
1149
+ expect(subDeltaTimes).toEqual([50, 100])
1150
+ })
1151
+
1152
+ it("should handle deltaTime correctly when animation starts mid-frame", () => {
1153
+ timeline = createTimeline({ duration: 1000, autoplay: false })
1154
+
1155
+ const deltaTimesReceived: number[] = []
1156
+ timeline.add(
1157
+ target,
1158
+ {
1159
+ x: 100,
1160
+ duration: 500,
1161
+ onUpdate: (anim: JSAnimation) => {
1162
+ deltaTimesReceived.push(anim.deltaTime)
1163
+ },
1164
+ },
1165
+ 250,
1166
+ )
1167
+
1168
+ timeline.play()
1169
+
1170
+ engine.update(200)
1171
+ expect(deltaTimesReceived).toEqual([])
1172
+
1173
+ engine.update(100)
1174
+ expect(deltaTimesReceived).toEqual([100])
1175
+
1176
+ engine.update(150)
1177
+ expect(deltaTimesReceived).toEqual([100, 150])
1178
+ })
1179
+
1180
+ it("should provide correct deltaTime for zero duration animations", () => {
1181
+ timeline = createTimeline({ duration: 1000, autoplay: false })
1182
+
1183
+ const deltaTimesReceived: number[] = []
1184
+ timeline.add(target, {
1185
+ x: 100,
1186
+ duration: 0,
1187
+ onUpdate: (anim: JSAnimation) => {
1188
+ deltaTimesReceived.push(anim.deltaTime)
1189
+ },
1190
+ })
1191
+
1192
+ timeline.play()
1193
+
1194
+ engine.update(50)
1195
+ expect(deltaTimesReceived).toEqual([50])
1196
+ expect(target.x).toBe(100)
1197
+
1198
+ engine.update(25)
1199
+ expect(deltaTimesReceived).toEqual([50])
1200
+ })
1201
+
1202
+ it("should provide consistent deltaTime during alternating animations", () => {
1203
+ timeline = createTimeline({ duration: 3000, autoplay: false })
1204
+
1205
+ const deltaTimesReceived: number[] = []
1206
+ const progressValues: number[] = []
1207
+
1208
+ timeline.add(target, {
1209
+ x: 100,
1210
+ duration: 500,
1211
+ loop: 2,
1212
+ alternate: true,
1213
+ onUpdate: (anim: JSAnimation) => {
1214
+ deltaTimesReceived.push(anim.deltaTime)
1215
+ progressValues.push(anim.progress)
1216
+ },
1217
+ })
1218
+
1219
+ timeline.play()
1220
+
1221
+ engine.update(250)
1222
+ engine.update(250)
1223
+
1224
+ engine.update(125)
1225
+ engine.update(375)
1226
+
1227
+ expect(deltaTimesReceived).toEqual([250, 250, 125, 375])
1228
+
1229
+ expect(progressValues[0]).toBe(0.5)
1230
+ expect(progressValues[1]).toBe(1)
1231
+ expect(progressValues[2]).toBe(0.25)
1232
+ expect(progressValues[3]).toBe(1)
1233
+ })
1234
+ })
1235
+
1236
+ describe("onUpdate Callback Frequency and Correctness", () => {
1237
+ it("should provide correct progress values in onUpdate callbacks", () => {
1238
+ timeline = createTimeline({ duration: 1000, autoplay: false })
1239
+
1240
+ const progressValues: number[] = []
1241
+ const targetValues: number[] = []
1242
+
1243
+ timeline.add(target, {
1244
+ x: 100,
1245
+ duration: 1000,
1246
+ ease: "linear",
1247
+ onUpdate: (anim: JSAnimation) => {
1248
+ progressValues.push(anim.progress)
1249
+ targetValues.push(anim.targets[0].x)
1250
+ },
1251
+ })
1252
+
1253
+ timeline.play()
1254
+
1255
+ engine.update(0)
1256
+ engine.update(250)
1257
+ engine.update(250)
1258
+ engine.update(250)
1259
+ engine.update(250)
1260
+
1261
+ expect(progressValues).toEqual([0, 0.25, 0.5, 0.75, 1])
1262
+ expect(targetValues).toEqual([0, 25, 50, 75, 100])
1263
+ })
1264
+
1265
+ it("should call onUpdate for each animation in a looping scenario without duplicates", () => {
1266
+ timeline = createTimeline({ duration: 3000, autoplay: false })
1267
+
1268
+ let updateCount = 0
1269
+ const progressHistory: number[] = []
1270
+
1271
+ timeline.add(target, {
1272
+ x: 100,
1273
+ duration: 500,
1274
+ loop: 3,
1275
+ onUpdate: (anim: JSAnimation) => {
1276
+ updateCount++
1277
+ progressHistory.push(anim.progress)
1278
+ },
1279
+ })
1280
+
1281
+ timeline.play()
1282
+
1283
+ engine.update(250)
1284
+ engine.update(250)
1285
+
1286
+ engine.update(250)
1287
+ engine.update(250)
1288
+
1289
+ engine.update(250)
1290
+ engine.update(250)
1291
+
1292
+ expect(updateCount).toBe(6)
1293
+ expect(progressHistory).toEqual([0.5, 1, 0.5, 1, 0.5, 1])
1294
+ })
1295
+
1296
+ it("should call onUpdate correctly for alternating animations", () => {
1297
+ timeline = createTimeline({ duration: 3000, autoplay: false })
1298
+
1299
+ let updateCount = 0
1300
+ const targetValueHistory: number[] = []
1301
+ const progressHistory: number[] = []
1302
+
1303
+ timeline.add(target, {
1304
+ x: 100,
1305
+ duration: 500,
1306
+ loop: 3,
1307
+ alternate: true,
1308
+ onUpdate: (anim: JSAnimation) => {
1309
+ updateCount++
1310
+ targetValueHistory.push(anim.targets[0].x)
1311
+ progressHistory.push(anim.progress)
1312
+ },
1313
+ })
1314
+
1315
+ timeline.play()
1316
+
1317
+ engine.update(250)
1318
+ engine.update(250)
1319
+
1320
+ engine.update(250)
1321
+ engine.update(250)
1322
+
1323
+ engine.update(250)
1324
+ engine.update(250)
1325
+
1326
+ expect(updateCount).toBe(6)
1327
+ expect(targetValueHistory).toEqual([50, 100, 50, 0, 50, 100])
1328
+ expect(progressHistory).toEqual([0.5, 1, 0.5, 1, 0.5, 1])
1329
+ })
1330
+
1331
+ it("should provide correct deltaTime and timing information in onUpdate", () => {
1332
+ timeline = createTimeline({ duration: 2000, autoplay: false })
1333
+
1334
+ const deltaTimeHistory: number[] = []
1335
+ const currentTimeHistory: number[] = []
1336
+
1337
+ timeline.add(
1338
+ target,
1339
+ {
1340
+ x: 100,
1341
+ duration: 1000,
1342
+ onUpdate: (anim: JSAnimation) => {
1343
+ deltaTimeHistory.push(anim.deltaTime)
1344
+ currentTimeHistory.push(anim.currentTime)
1345
+ },
1346
+ },
1347
+ 300,
1348
+ )
1349
+
1350
+ timeline.play()
1351
+
1352
+ engine.update(200)
1353
+ expect(deltaTimeHistory).toEqual([])
1354
+
1355
+ engine.update(150)
1356
+ engine.update(200)
1357
+ engine.update(300)
1358
+ engine.update(450)
1359
+
1360
+ expect(deltaTimeHistory).toEqual([150, 200, 300, 450])
1361
+ expect(currentTimeHistory).toEqual([350, 550, 850, 1300])
1362
+ })
1363
+
1364
+ it("should not call onUpdate multiple times for zero duration animations", () => {
1365
+ timeline = createTimeline({ duration: 1000, autoplay: false })
1366
+
1367
+ let updateCount = 0
1368
+ const receivedValues: JSAnimation[] = []
1369
+
1370
+ timeline.add(target, {
1371
+ x: 100,
1372
+ duration: 0,
1373
+ onUpdate: (anim: JSAnimation) => {
1374
+ updateCount++
1375
+ receivedValues.push(anim)
1376
+ },
1377
+ })
1378
+
1379
+ timeline.play()
1380
+
1381
+ engine.update(50)
1382
+ engine.update(100)
1383
+ engine.update(200)
1384
+
1385
+ expect(updateCount).toBe(1)
1386
+ expect(receivedValues[0].progress).toBe(1)
1387
+ expect(receivedValues[0].targets[0].x).toBe(100)
1388
+ })
1389
+
1390
+ it("should not call onUpdate after animation completes", () => {
1391
+ timeline = createTimeline({ duration: 2000, autoplay: false })
1392
+
1393
+ let updateCallCount = 0
1394
+ let completeCallCount = 0
1395
+ const updateTimes: number[] = []
1396
+
1397
+ timeline.add(target, {
1398
+ x: 100,
1399
+ duration: 500,
1400
+ onUpdate: (anim: JSAnimation) => {
1401
+ updateCallCount++
1402
+ updateTimes.push(timeline.currentTime)
1403
+ },
1404
+ onComplete: () => {
1405
+ completeCallCount++
1406
+ },
1407
+ })
1408
+
1409
+ timeline.play()
1410
+
1411
+ engine.update(250)
1412
+ expect(updateCallCount).toBe(1)
1413
+ expect(completeCallCount).toBe(0)
1414
+ expect(target.x).toBe(50)
1415
+
1416
+ engine.update(250)
1417
+ expect(updateCallCount).toBe(2)
1418
+ expect(completeCallCount).toBe(1)
1419
+ expect(target.x).toBe(100)
1420
+
1421
+ engine.update(300)
1422
+ engine.update(400)
1423
+ engine.update(500)
1424
+
1425
+ expect(updateCallCount).toBe(2)
1426
+ expect(completeCallCount).toBe(1)
1427
+ expect(target.x).toBe(100)
1428
+
1429
+ expect(updateTimes).toEqual([250, 500])
1430
+ })
1431
+
1432
+ it("should call onUpdate for multiple targets on same animation correctly", () => {
1433
+ timeline = createTimeline({ duration: 1000, autoplay: false })
1434
+
1435
+ const target1 = { x: 0, y: 0 }
1436
+ const target2 = { x: 0, y: 0 }
1437
+ let updateCount = 0
1438
+ const allTargetsHistory: Array<{ x: number; y: number }[]> = []
1439
+
1440
+ timeline.add([target1, target2], {
1441
+ x: 100,
1442
+ y: 200,
1443
+ duration: 1000,
1444
+ onUpdate: (anim: JSAnimation) => {
1445
+ updateCount++
1446
+ allTargetsHistory.push(anim.targets.map((target) => ({ x: target.x, y: target.y })))
1447
+ },
1448
+ })
1449
+
1450
+ timeline.play()
1451
+
1452
+ engine.update(500)
1453
+ engine.update(500)
1454
+
1455
+ expect(updateCount).toBe(2)
1456
+
1457
+ expect(allTargetsHistory[0]).toEqual([
1458
+ { x: 50, y: 100 },
1459
+ { x: 50, y: 100 },
1460
+ ])
1461
+
1462
+ expect(allTargetsHistory[1]).toEqual([
1463
+ { x: 100, y: 200 },
1464
+ { x: 100, y: 200 },
1465
+ ])
1466
+ })
1467
+ })
1468
+
1469
+ describe("Target Value Persistence Bug", () => {
1470
+ it("should not reset target values to initial values when animation hasnt started", () => {
1471
+ timeline = createTimeline({ duration: 1000, autoplay: false })
1472
+
1473
+ const testTarget = { x: 50, strength: 1.5 }
1474
+
1475
+ testTarget.x = 75
1476
+ testTarget.strength = 2.0
1477
+
1478
+ timeline.add(
1479
+ testTarget,
1480
+ {
1481
+ x: 100,
1482
+ duration: 300,
1483
+ },
1484
+ 500,
1485
+ )
1486
+
1487
+ timeline.play()
1488
+
1489
+ engine.update(100)
1490
+
1491
+ expect(testTarget.x).toBe(75)
1492
+ expect(testTarget.strength).toBe(2.0)
1493
+
1494
+ engine.update(200)
1495
+ expect(testTarget.x).toBe(75)
1496
+ expect(testTarget.strength).toBe(2.0)
1497
+
1498
+ engine.update(300)
1499
+ expect(testTarget.x).toBeCloseTo(83.33, 2)
1500
+ expect(testTarget.strength).toBe(2.0)
1501
+ })
1502
+
1503
+ it("should not reset target values to initial values after onUpdate", () => {
1504
+ timeline = createTimeline({ duration: 1000, autoplay: false })
1505
+
1506
+ const testTarget = { x: 0, y: 50 }
1507
+ let onUpdateCallCount = 0
1508
+ let capturedValues: Array<{ x: number; y: number }> = []
1509
+
1510
+ timeline.add(testTarget, {
1511
+ x: 100,
1512
+ duration: 500,
1513
+ onUpdate: (anim: JSAnimation) => {
1514
+ onUpdateCallCount++
1515
+ capturedValues.push({ x: testTarget.x, y: testTarget.y })
1516
+ },
1517
+ })
1518
+
1519
+ timeline.play()
1520
+
1521
+ engine.update(250)
1522
+ expect(onUpdateCallCount).toBe(1)
1523
+ expect(testTarget.x).toBe(50)
1524
+ expect(testTarget.y).toBe(50)
1525
+
1526
+ engine.update(250)
1527
+ expect(onUpdateCallCount).toBe(2)
1528
+ expect(testTarget.x).toBe(100)
1529
+ expect(testTarget.y).toBe(50)
1530
+
1531
+ engine.update(100)
1532
+ engine.update(100)
1533
+ engine.update(100)
1534
+
1535
+ expect(testTarget.x).toBe(100)
1536
+ expect(testTarget.y).toBe(50)
1537
+ expect(onUpdateCallCount).toBe(2)
1538
+
1539
+ expect(capturedValues[0]).toEqual({ x: 50, y: 50 })
1540
+ expect(capturedValues[1]).toEqual({ x: 100, y: 50 })
1541
+ })
1542
+
1543
+ it("should preserve final values across timeline loops", () => {
1544
+ timeline = createTimeline({ duration: 1000, loop: true, autoplay: false })
1545
+
1546
+ const testTarget = { value: 0 }
1547
+ let updateCallCount = 0
1548
+
1549
+ timeline.add(testTarget, {
1550
+ value: 100,
1551
+ duration: 600,
1552
+ onUpdate: () => updateCallCount++,
1553
+ })
1554
+
1555
+ timeline.play()
1556
+
1557
+ engine.update(600)
1558
+ expect(testTarget.value).toBe(100)
1559
+ expect(updateCallCount).toBe(1)
1560
+
1561
+ engine.update(400)
1562
+
1563
+ expect(testTarget.value).toBe(100)
1564
+ expect(updateCallCount).toBe(1)
1565
+
1566
+ engine.update(300)
1567
+ expect(testTarget.value).toBe(50)
1568
+ expect(updateCallCount).toBe(2)
1569
+ })
1570
+
1571
+ it("should preserve original initial values across timeline loops", () => {
1572
+ timeline = createTimeline({ duration: 1000, loop: true, autoplay: false })
1573
+
1574
+ const testTarget = { value: 0 }
1575
+ let updateCallCount = 0
1576
+
1577
+ timeline.add(testTarget, {
1578
+ value: 100,
1579
+ duration: 600,
1580
+ onUpdate: () => updateCallCount++,
1581
+ })
1582
+
1583
+ timeline.play()
1584
+
1585
+ engine.update(600)
1586
+ expect(testTarget.value).toBe(100)
1587
+ expect(updateCallCount).toBe(1)
1588
+
1589
+ engine.update(400)
1590
+
1591
+ expect(testTarget.value).toBe(100)
1592
+ expect(updateCallCount).toBe(1)
1593
+
1594
+ engine.update(300)
1595
+ expect(testTarget.value).toBe(50)
1596
+ expect(updateCallCount).toBe(2)
1597
+ })
1598
+ })
1599
+
1600
+ describe("Multiple Animations on Same Object", () => {
1601
+ it("should handle multiple animations on the same object", () => {
1602
+ timeline = createTimeline({ duration: 5000, autoplay: false })
1603
+
1604
+ const testTarget = { x: 0 }
1605
+
1606
+ timeline.add(
1607
+ testTarget,
1608
+ {
1609
+ x: 100,
1610
+ duration: 100,
1611
+ },
1612
+ 0,
1613
+ )
1614
+
1615
+ timeline.add(
1616
+ testTarget,
1617
+ {
1618
+ x: 50,
1619
+ duration: 100,
1620
+ },
1621
+ 200,
1622
+ )
1623
+
1624
+ timeline.play()
1625
+
1626
+ expect(testTarget.x).toBe(0)
1627
+
1628
+ engine.update(50)
1629
+ expect(testTarget.x).toBe(50)
1630
+
1631
+ engine.update(50)
1632
+ expect(testTarget.x).toBe(100)
1633
+
1634
+ engine.update(50)
1635
+ expect(testTarget.x).toBe(100)
1636
+
1637
+ engine.update(100)
1638
+ expect(testTarget.x).toBe(75)
1639
+
1640
+ engine.update(50)
1641
+ expect(testTarget.x).toBe(50)
1642
+ })
1643
+
1644
+ it("should handle multiple sequential animations on the same object", () => {
1645
+ timeline = createTimeline({ duration: 5000, autoplay: false })
1646
+
1647
+ const testTarget = { x: 0, y: 0, z: 0 }
1648
+ const animationStates: Array<{ time: number; x: number; y: number; z: number }> = []
1649
+
1650
+ timeline.add(
1651
+ testTarget,
1652
+ {
1653
+ x: 100,
1654
+ duration: 1000,
1655
+ onUpdate: () =>
1656
+ animationStates.push({ time: timeline.currentTime, x: testTarget.x, y: testTarget.y, z: testTarget.z }),
1657
+ },
1658
+ 0,
1659
+ )
1660
+
1661
+ timeline.add(
1662
+ testTarget,
1663
+ {
1664
+ y: 50,
1665
+ duration: 500,
1666
+ onUpdate: () =>
1667
+ animationStates.push({ time: timeline.currentTime, x: testTarget.x, y: testTarget.y, z: testTarget.z }),
1668
+ },
1669
+ 1500,
1670
+ )
1671
+
1672
+ timeline.add(
1673
+ testTarget,
1674
+ {
1675
+ z: 200,
1676
+ duration: 1000,
1677
+ onUpdate: () =>
1678
+ animationStates.push({ time: timeline.currentTime, x: testTarget.x, y: testTarget.y, z: testTarget.z }),
1679
+ },
1680
+ 3000,
1681
+ )
1682
+
1683
+ timeline.play()
1684
+
1685
+ engine.update(0)
1686
+ expect(testTarget.x).toBe(0)
1687
+ expect(testTarget.y).toBe(0)
1688
+ expect(testTarget.z).toBe(0)
1689
+
1690
+ engine.update(500)
1691
+ expect(testTarget.x).toBe(50)
1692
+ expect(testTarget.y).toBe(0)
1693
+ expect(testTarget.z).toBe(0)
1694
+
1695
+ engine.update(500)
1696
+ expect(testTarget.x).toBe(100)
1697
+ expect(testTarget.y).toBe(0)
1698
+ expect(testTarget.z).toBe(0)
1699
+
1700
+ engine.update(250)
1701
+ expect(testTarget.x).toBe(100)
1702
+ expect(testTarget.y).toBe(0)
1703
+ expect(testTarget.z).toBe(0)
1704
+
1705
+ engine.update(250)
1706
+ expect(testTarget.x).toBe(100)
1707
+ expect(testTarget.y).toBe(0)
1708
+ expect(testTarget.z).toBe(0)
1709
+
1710
+ engine.update(250)
1711
+ expect(testTarget.x).toBe(100)
1712
+ expect(testTarget.y).toBe(25)
1713
+ expect(testTarget.z).toBe(0)
1714
+
1715
+ engine.update(250)
1716
+ engine.update(500)
1717
+ expect(testTarget.x).toBe(100)
1718
+ expect(testTarget.y).toBe(50)
1719
+ expect(testTarget.z).toBe(0)
1720
+
1721
+ engine.update(500)
1722
+ expect(testTarget.x).toBe(100)
1723
+ expect(testTarget.y).toBe(50)
1724
+ expect(testTarget.z).toBe(0)
1725
+
1726
+ engine.update(500)
1727
+ expect(testTarget.x).toBe(100)
1728
+ expect(testTarget.y).toBe(50)
1729
+ expect(testTarget.z).toBe(100)
1730
+
1731
+ engine.update(500)
1732
+ expect(testTarget.x).toBe(100)
1733
+ expect(testTarget.y).toBe(50)
1734
+ expect(testTarget.z).toBe(200)
1735
+
1736
+ engine.update(1000)
1737
+ expect(testTarget.x).toBe(100)
1738
+ expect(testTarget.y).toBe(50)
1739
+ expect(testTarget.z).toBe(200)
1740
+
1741
+ expect(animationStates.length).toBeGreaterThan(0)
1742
+
1743
+ engine.update(1000)
1744
+ expect(testTarget.x).toBe(100)
1745
+ expect(testTarget.y).toBe(50)
1746
+ expect(testTarget.z).toBe(200)
1747
+ })
1748
+
1749
+ it("should handle overlapping animations on different properties", () => {
1750
+ timeline = createTimeline({ duration: 3000, autoplay: false })
1751
+
1752
+ const testTarget = { x: 0, y: 0, scale: 1 }
1753
+
1754
+ timeline.add(
1755
+ testTarget,
1756
+ {
1757
+ x: 100,
1758
+ duration: 1000,
1759
+ },
1760
+ 0,
1761
+ )
1762
+
1763
+ timeline.add(
1764
+ testTarget,
1765
+ {
1766
+ y: 50,
1767
+ duration: 1000,
1768
+ },
1769
+ 500,
1770
+ )
1771
+
1772
+ timeline.add(
1773
+ testTarget,
1774
+ {
1775
+ scale: 2,
1776
+ duration: 1000,
1777
+ },
1778
+ 800,
1779
+ )
1780
+
1781
+ timeline.play()
1782
+
1783
+ engine.update(600)
1784
+ expect(testTarget.x).toBe(60)
1785
+ expect(testTarget.y).toBe(5)
1786
+ expect(testTarget.scale).toBe(1)
1787
+
1788
+ engine.update(400)
1789
+ expect(testTarget.x).toBe(100)
1790
+ expect(testTarget.y).toBe(25)
1791
+ expect(testTarget.scale).toBe(1.2)
1792
+
1793
+ engine.update(600)
1794
+ expect(testTarget.x).toBe(100)
1795
+ expect(testTarget.y).toBe(50)
1796
+ expect(testTarget.scale).toBe(1.8)
1797
+
1798
+ engine.update(400)
1799
+ expect(testTarget.x).toBe(100)
1800
+ expect(testTarget.y).toBe(50)
1801
+ expect(testTarget.scale).toBe(2)
1802
+ })
1803
+
1804
+ it("should handle multiple animations with different easing functions", () => {
1805
+ timeline = createTimeline({ duration: 3000, autoplay: false })
1806
+
1807
+ const testTarget = { a: 0, b: 0, c: 0 }
1808
+
1809
+ timeline.add(
1810
+ testTarget,
1811
+ {
1812
+ a: 100,
1813
+ duration: 1000,
1814
+ ease: "linear",
1815
+ },
1816
+ 0,
1817
+ )
1818
+
1819
+ timeline.add(
1820
+ testTarget,
1821
+ {
1822
+ b: 100,
1823
+ duration: 1000,
1824
+ ease: "inQuad",
1825
+ },
1826
+ 500,
1827
+ )
1828
+
1829
+ timeline.add(
1830
+ testTarget,
1831
+ {
1832
+ c: 100,
1833
+ duration: 1000,
1834
+ ease: "inExpo",
1835
+ },
1836
+ 1000,
1837
+ )
1838
+
1839
+ timeline.play()
1840
+
1841
+ engine.update(500)
1842
+ expect(testTarget.a).toBe(50)
1843
+ expect(testTarget.b).toBe(0)
1844
+ expect(testTarget.c).toBe(0)
1845
+
1846
+ engine.update(500)
1847
+ expect(testTarget.a).toBe(100)
1848
+ expect(testTarget.b).toBe(25)
1849
+ expect(testTarget.c).toBe(0)
1850
+
1851
+ engine.update(500)
1852
+ expect(testTarget.a).toBe(100)
1853
+ expect(testTarget.b).toBe(100)
1854
+ expect(testTarget.c).toBeGreaterThan(0)
1855
+ expect(testTarget.c).toBeLessThan(50)
1856
+
1857
+ engine.update(500)
1858
+ expect(testTarget.a).toBe(100)
1859
+ expect(testTarget.b).toBe(100)
1860
+ expect(testTarget.c).toBe(100)
1861
+ })
1862
+ })
1863
+
1864
+ describe("JSAnimation targets Array Handling", () => {
1865
+ it("should provide single target as targets[0] in onUpdate callback", () => {
1866
+ timeline = createTimeline({ duration: 2000, autoplay: false })
1867
+
1868
+ const brightnessEffect = { brightness: 0.5 }
1869
+ const capturedTargets: any[][] = []
1870
+ const capturedValues: number[] = []
1871
+
1872
+ timeline.add(brightnessEffect, {
1873
+ brightness: 1.0,
1874
+ ease: "linear",
1875
+ duration: 1000,
1876
+ onUpdate: (values: JSAnimation) => {
1877
+ capturedTargets.push([...values.targets])
1878
+ capturedValues.push(values.targets[0].brightness)
1879
+ },
1880
+ })
1881
+
1882
+ timeline.play()
1883
+
1884
+ engine.update(250)
1885
+ expect(capturedValues[0]).toBe(0.625)
1886
+ expect(capturedTargets[0]).toHaveLength(1)
1887
+ expect(capturedTargets[0][0].brightness).toBe(0.625)
1888
+
1889
+ engine.update(250)
1890
+ expect(capturedValues[1]).toBe(0.75)
1891
+ expect(capturedTargets[1][0].brightness).toBe(0.75)
1892
+
1893
+ engine.update(500)
1894
+ expect(capturedValues[2]).toBe(1.0)
1895
+ expect(capturedTargets[2][0].brightness).toBe(1.0)
1896
+
1897
+ expect(brightnessEffect.brightness).toBe(1.0)
1898
+ })
1899
+
1900
+ it("should provide multiple targets correctly in targets array", () => {
1901
+ timeline = createTimeline({ duration: 1000, autoplay: false })
1902
+
1903
+ const effect1 = { intensity: 0.0 }
1904
+ const effect2 = { intensity: 0.0 }
1905
+ const capturedTargets: any[][] = []
1906
+
1907
+ timeline.add([effect1, effect2], {
1908
+ intensity: 2.0,
1909
+ ease: "linear",
1910
+ duration: 500,
1911
+ onUpdate: (values: JSAnimation) => {
1912
+ capturedTargets.push([...values.targets])
1913
+ },
1914
+ })
1915
+
1916
+ timeline.play()
1917
+
1918
+ engine.update(250)
1919
+ expect(capturedTargets[0]).toHaveLength(2)
1920
+ expect(capturedTargets[0][0].intensity).toBe(1.0)
1921
+ expect(capturedTargets[0][1].intensity).toBe(1.0)
1922
+
1923
+ engine.update(250)
1924
+ expect(capturedTargets[1]).toHaveLength(2)
1925
+ expect(capturedTargets[1][0].intensity).toBe(2.0)
1926
+ expect(capturedTargets[1][1].intensity).toBe(2.0)
1927
+
1928
+ expect(effect1.intensity).toBe(2.0)
1929
+ expect(effect2.intensity).toBe(2.0)
1930
+ })
1931
+
1932
+ it("should provide targets with complex object properties", () => {
1933
+ timeline = createTimeline({ duration: 1000, autoplay: false })
1934
+
1935
+ const postProcessEffect = {
1936
+ brightness: 0.8,
1937
+ contrast: 1.0,
1938
+ saturation: 0.9,
1939
+ vignette: { strength: 0.2 },
1940
+ }
1941
+
1942
+ const loggedValues: Array<{ brightness: number; contrast: number; saturation: number }> = []
1943
+
1944
+ timeline.add(postProcessEffect, {
1945
+ brightness: 1.2,
1946
+ contrast: 1.5,
1947
+ saturation: 1.1,
1948
+ ease: "outExpo",
1949
+ duration: 500,
1950
+ onUpdate: (values: JSAnimation) => {
1951
+ const target = values.targets[0]
1952
+ loggedValues.push({
1953
+ brightness: target.brightness,
1954
+ contrast: target.contrast,
1955
+ saturation: target.saturation,
1956
+ })
1957
+ },
1958
+ })
1959
+
1960
+ timeline.play()
1961
+
1962
+ engine.update(100)
1963
+ engine.update(200)
1964
+ engine.update(200)
1965
+
1966
+ expect(loggedValues).toHaveLength(3)
1967
+
1968
+ expect(loggedValues[0].brightness).toBeGreaterThan(1.0)
1969
+ expect(loggedValues[0].contrast).toBeGreaterThan(1.2)
1970
+ expect(loggedValues[0].saturation).toBeGreaterThan(1.0)
1971
+
1972
+ expect(loggedValues[2].brightness).toBe(1.2)
1973
+ expect(loggedValues[2].contrast).toBe(1.5)
1974
+ expect(loggedValues[2].saturation).toBe(1.1)
1975
+
1976
+ expect(postProcessEffect.vignette.strength).toBe(0.2)
1977
+
1978
+ expect(postProcessEffect.brightness).toBe(1.2)
1979
+ expect(postProcessEffect.contrast).toBe(1.5)
1980
+ expect(postProcessEffect.saturation).toBe(1.1)
1981
+ })
1982
+
1983
+ it("should maintain targets array consistency with different animation properties", () => {
1984
+ timeline = createTimeline({ duration: 2000, autoplay: false })
1985
+
1986
+ const multiPropTarget = { x: 0, y: 0, z: 0, scale: 1, rotation: 0 }
1987
+ const allCapturedStates: any[] = []
1988
+
1989
+ timeline.add(multiPropTarget, {
1990
+ x: 100,
1991
+ scale: 2,
1992
+ rotation: 360,
1993
+ ease: "linear",
1994
+ duration: 1000,
1995
+ onUpdate: (values: JSAnimation) => {
1996
+ allCapturedStates.push({ ...values.targets[0] })
1997
+ },
1998
+ })
1999
+
2000
+ timeline.play()
2001
+
2002
+ engine.update(500)
2003
+ engine.update(500)
2004
+
2005
+ expect(allCapturedStates).toHaveLength(2)
2006
+
2007
+ expect(allCapturedStates[0].x).toBe(50)
2008
+ expect(allCapturedStates[0].scale).toBe(1.5)
2009
+ expect(allCapturedStates[0].rotation).toBe(180)
2010
+ expect(allCapturedStates[0].y).toBe(0)
2011
+ expect(allCapturedStates[0].z).toBe(0)
2012
+
2013
+ expect(allCapturedStates[1].x).toBe(100)
2014
+ expect(allCapturedStates[1].scale).toBe(2)
2015
+ expect(allCapturedStates[1].rotation).toBe(360)
2016
+ expect(allCapturedStates[1].y).toBe(0)
2017
+ expect(allCapturedStates[1].z).toBe(0)
2018
+
2019
+ expect(multiPropTarget.x).toBe(100)
2020
+ expect(multiPropTarget.scale).toBe(2)
2021
+ expect(multiPropTarget.rotation).toBe(360)
2022
+ expect(multiPropTarget.y).toBe(0)
2023
+ expect(multiPropTarget.z).toBe(0)
2024
+ })
2025
+
2026
+ it("should handle class instances with getter/setter properties", () => {
2027
+ timeline = createTimeline({ duration: 1000, autoplay: false })
2028
+
2029
+ class TestEffect {
2030
+ private _brightness: number = 1.0
2031
+ private _contrast: number = 1.0
2032
+
2033
+ get brightness(): number {
2034
+ return this._brightness
2035
+ }
2036
+
2037
+ set brightness(value: number) {
2038
+ this._brightness = value
2039
+ }
2040
+
2041
+ get contrast(): number {
2042
+ return this._contrast
2043
+ }
2044
+
2045
+ set contrast(value: number) {
2046
+ this._contrast = value
2047
+ }
2048
+ }
2049
+
2050
+ const effectInstance = new TestEffect()
2051
+ const capturedValues: Array<{ brightness: number; contrast: number }> = []
2052
+
2053
+ timeline.add(effectInstance, {
2054
+ brightness: 2.0,
2055
+ contrast: 1.5,
2056
+ ease: "linear",
2057
+ duration: 500,
2058
+ onUpdate: (values: JSAnimation) => {
2059
+ const target = values.targets[0]
2060
+ capturedValues.push({
2061
+ brightness: target.brightness,
2062
+ contrast: target.contrast,
2063
+ })
2064
+ },
2065
+ })
2066
+
2067
+ timeline.play()
2068
+
2069
+ engine.update(250)
2070
+ engine.update(250)
2071
+
2072
+ expect(capturedValues).toHaveLength(2)
2073
+
2074
+ expect(capturedValues[0].brightness).toBe(1.5)
2075
+ expect(capturedValues[0].contrast).toBe(1.25)
2076
+
2077
+ expect(capturedValues[1].brightness).toBe(2.0)
2078
+ expect(capturedValues[1].contrast).toBe(1.5)
2079
+
2080
+ expect(effectInstance.brightness).toBe(2.0)
2081
+ expect(effectInstance.contrast).toBe(1.5)
2082
+ })
2083
+ })
2084
+
2085
+ describe("Scene00 Reproduction Bug", () => {
2086
+ it("should execute callbacks at position 0 again when timeline loops", () => {
2087
+ timeline = createTimeline({ duration: 1000, loop: true, autoplay: false })
2088
+
2089
+ let callbackExecutionCount = 0
2090
+ const resetValue = { x: 0 }
2091
+
2092
+ timeline.call(() => {
2093
+ callbackExecutionCount++
2094
+ resetValue.x = 0
2095
+ }, 0)
2096
+
2097
+ timeline.add(
2098
+ resetValue,
2099
+ {
2100
+ x: 100,
2101
+ duration: 500,
2102
+ },
2103
+ 200,
2104
+ )
2105
+
2106
+ timeline.play()
2107
+
2108
+ engine.update(0)
2109
+ expect(callbackExecutionCount).toBe(1)
2110
+ expect(resetValue.x).toBe(0)
2111
+
2112
+ engine.update(200)
2113
+ expect(resetValue.x).toBe(0)
2114
+
2115
+ engine.update(250)
2116
+ expect(resetValue.x).toBe(50)
2117
+
2118
+ engine.update(575)
2119
+ expect(timeline.currentTime).toBe(25)
2120
+
2121
+ expect(callbackExecutionCount).toBe(2)
2122
+ expect(resetValue.x).toBe(0)
2123
+
2124
+ engine.update(175)
2125
+ expect(resetValue.x).toBe(0)
2126
+
2127
+ engine.update(250)
2128
+ expect(resetValue.x).toBe(50)
2129
+ })
2130
+ })
2131
+
2132
+ it("should execute callbacks at position 0 again when timeline loops", () => {
2133
+ timeline = createTimeline({ duration: 1000, loop: true, autoplay: false })
2134
+
2135
+ let callbackExecutionCount = 0
2136
+ const resetValue = { x: 0 }
2137
+
2138
+ timeline.call(() => {
2139
+ callbackExecutionCount++
2140
+ resetValue.x = 0
2141
+ }, 0)
2142
+
2143
+ timeline.add(
2144
+ resetValue,
2145
+ {
2146
+ x: 100,
2147
+ duration: 500,
2148
+ },
2149
+ 200,
2150
+ )
2151
+
2152
+ timeline.play()
2153
+
2154
+ engine.update(0)
2155
+ expect(callbackExecutionCount).toBe(1)
2156
+ expect(resetValue.x).toBe(0)
2157
+
2158
+ engine.update(200)
2159
+ expect(resetValue.x).toBe(0)
2160
+
2161
+ engine.update(250)
2162
+ expect(resetValue.x).toBe(50)
2163
+
2164
+ engine.update(575)
2165
+ expect(timeline.currentTime).toBe(25)
2166
+
2167
+ expect(callbackExecutionCount).toBe(2)
2168
+ expect(resetValue.x).toBe(0)
2169
+
2170
+ engine.update(175)
2171
+ expect(resetValue.x).toBe(0)
2172
+
2173
+ engine.update(250)
2174
+ expect(resetValue.x).toBe(50)
2175
+ })
2176
+
2177
+ it("should execute callbacks at position 0 again when nested sub-timeline loops", () => {
2178
+ const mainTimeline = createTimeline({ duration: 3000, loop: false, autoplay: false })
2179
+ const subTimeline = createTimeline({ duration: 1000, loop: true, autoplay: false })
2180
+
2181
+ let callbackExecutionCount = 0
2182
+ const resetValue = { x: 0 }
2183
+
2184
+ subTimeline.call(() => {
2185
+ callbackExecutionCount++
2186
+ resetValue.x = 0
2187
+ }, 0)
2188
+
2189
+ subTimeline.add(
2190
+ resetValue,
2191
+ {
2192
+ x: 100,
2193
+ duration: 500,
2194
+ },
2195
+ 200,
2196
+ )
2197
+
2198
+ mainTimeline.sync(subTimeline, 500)
2199
+ mainTimeline.play()
2200
+
2201
+ engine.update(400)
2202
+ expect(callbackExecutionCount).toBe(0)
2203
+ expect(resetValue.x).toBe(0)
2204
+
2205
+ engine.update(100)
2206
+ expect(callbackExecutionCount).toBe(1)
2207
+ expect(resetValue.x).toBe(0)
2208
+
2209
+ engine.update(200)
2210
+ expect(resetValue.x).toBe(0)
2211
+
2212
+ engine.update(250)
2213
+ expect(resetValue.x).toBe(50)
2214
+
2215
+ engine.update(550)
2216
+ engine.update(25)
2217
+
2218
+ expect(callbackExecutionCount).toBe(2)
2219
+ expect(resetValue.x).toBe(0)
2220
+
2221
+ engine.update(200)
2222
+ expect(resetValue.x).toBe(5)
2223
+
2224
+ engine.update(225)
2225
+ expect(resetValue.x).toBe(50)
2226
+ })
2227
+
2228
+ it("should restart animations at position 0 again when nested sub-timeline loops", () => {
2229
+ const mainTimeline = createTimeline({ duration: 3000, loop: false, autoplay: false })
2230
+ const subTimeline = createTimeline({ duration: 1000, loop: true, autoplay: false })
2231
+
2232
+ const animationTarget = { value: 0 }
2233
+ let animationStartCount = 0
2234
+
2235
+ subTimeline.add(
2236
+ animationTarget,
2237
+ {
2238
+ value: 100,
2239
+ duration: 500,
2240
+ onStart: () => animationStartCount++,
2241
+ },
2242
+ 0,
2243
+ )
2244
+
2245
+ mainTimeline.sync(subTimeline, 500)
2246
+ mainTimeline.play()
2247
+
2248
+ engine.update(400)
2249
+ expect(animationStartCount).toBe(0)
2250
+ expect(animationTarget.value).toBe(0)
2251
+
2252
+ engine.update(100)
2253
+ expect(animationStartCount).toBe(1)
2254
+ expect(animationTarget.value).toBe(0)
2255
+
2256
+ engine.update(250)
2257
+ expect(animationTarget.value).toBe(50)
2258
+
2259
+ engine.update(250)
2260
+ expect(animationTarget.value).toBe(100)
2261
+
2262
+ engine.update(500)
2263
+ engine.update(25)
2264
+
2265
+ expect(animationStartCount).toBe(2)
2266
+ expect(animationTarget.value).toBe(5)
2267
+
2268
+ engine.update(225)
2269
+ expect(animationTarget.value).toBe(50)
2270
+
2271
+ engine.update(250)
2272
+ expect(animationTarget.value).toBe(100)
2273
+ })
2274
+
2275
+ describe("Timeline onComplete Callback", () => {
2276
+ it("should call onComplete when timeline finishes (non-looping)", () => {
2277
+ let completeCallCount = 0
2278
+ timeline = createTimeline({
2279
+ duration: 1000,
2280
+ loop: false,
2281
+ autoplay: false,
2282
+ onComplete: () => completeCallCount++,
2283
+ })
2284
+
2285
+ timeline.add(target, { x: 100, duration: 500 })
2286
+ timeline.play()
2287
+
2288
+ engine.update(500)
2289
+ expect(completeCallCount).toBe(0)
2290
+ expect(timeline.isPlaying).toBe(true)
2291
+
2292
+ engine.update(500)
2293
+ expect(completeCallCount).toBe(1)
2294
+ expect(timeline.isPlaying).toBe(false)
2295
+
2296
+ engine.update(1000)
2297
+ expect(completeCallCount).toBe(1)
2298
+ })
2299
+
2300
+ it("should not call onComplete for looping timelines", () => {
2301
+ let completeCallCount = 0
2302
+ timeline = createTimeline({
2303
+ duration: 1000,
2304
+ loop: true,
2305
+ autoplay: false,
2306
+ onComplete: () => completeCallCount++,
2307
+ })
2308
+
2309
+ timeline.add(target, { x: 100, duration: 500 })
2310
+ timeline.play()
2311
+
2312
+ engine.update(1000)
2313
+ expect(completeCallCount).toBe(0)
2314
+ expect(timeline.isPlaying).toBe(true)
2315
+
2316
+ engine.update(1000)
2317
+ expect(completeCallCount).toBe(0)
2318
+ expect(timeline.isPlaying).toBe(true)
2319
+
2320
+ engine.update(2000)
2321
+ expect(completeCallCount).toBe(0)
2322
+ expect(timeline.isPlaying).toBe(true)
2323
+ })
2324
+
2325
+ it("should call onComplete again when timeline is restarted and completes", () => {
2326
+ let completeCallCount = 0
2327
+ timeline = createTimeline({
2328
+ duration: 1000,
2329
+ loop: false,
2330
+ autoplay: false,
2331
+ onComplete: () => completeCallCount++,
2332
+ })
2333
+
2334
+ timeline.add(target, { x: 100, duration: 800 })
2335
+ timeline.play()
2336
+
2337
+ engine.update(1000)
2338
+ expect(completeCallCount).toBe(1)
2339
+ expect(timeline.isPlaying).toBe(false)
2340
+
2341
+ timeline.restart()
2342
+ expect(timeline.isPlaying).toBe(true)
2343
+
2344
+ engine.update(1000)
2345
+ expect(completeCallCount).toBe(2)
2346
+ expect(timeline.isPlaying).toBe(false)
2347
+ })
2348
+
2349
+ it("should not call onComplete when timeline is paused before completion", () => {
2350
+ let completeCallCount = 0
2351
+ timeline = createTimeline({
2352
+ duration: 1000,
2353
+ loop: false,
2354
+ autoplay: false,
2355
+ onComplete: () => completeCallCount++,
2356
+ })
2357
+
2358
+ timeline.add(target, { x: 100, duration: 800 })
2359
+ timeline.play()
2360
+
2361
+ engine.update(500)
2362
+ expect(completeCallCount).toBe(0)
2363
+ expect(timeline.isPlaying).toBe(true)
2364
+
2365
+ timeline.pause()
2366
+ engine.update(1000)
2367
+ expect(completeCallCount).toBe(0)
2368
+ expect(timeline.isPlaying).toBe(false)
2369
+ })
2370
+
2371
+ it("should call onComplete when playing again after pause reaches completion", () => {
2372
+ let completeCallCount = 0
2373
+ timeline = createTimeline({
2374
+ duration: 1000,
2375
+ loop: false,
2376
+ autoplay: false,
2377
+ onComplete: () => completeCallCount++,
2378
+ })
2379
+
2380
+ timeline.add(target, { x: 100, duration: 800 })
2381
+ timeline.play()
2382
+
2383
+ engine.update(500)
2384
+ timeline.pause()
2385
+ engine.update(1000)
2386
+ expect(completeCallCount).toBe(0)
2387
+
2388
+ timeline.play()
2389
+ engine.update(500)
2390
+ expect(completeCallCount).toBe(1)
2391
+ expect(timeline.isPlaying).toBe(false)
2392
+ })
2393
+
2394
+ it("should call onComplete with correct timing when timeline has overshoot", () => {
2395
+ let completeCallCount = 0
2396
+ let completionTime = 0
2397
+ timeline = createTimeline({
2398
+ duration: 1000,
2399
+ loop: false,
2400
+ autoplay: false,
2401
+ onComplete: () => {
2402
+ completeCallCount++
2403
+ completionTime = timeline.currentTime
2404
+ },
2405
+ })
2406
+
2407
+ timeline.add(target, { x: 100, duration: 800 })
2408
+ timeline.play()
2409
+
2410
+ engine.update(1200)
2411
+ expect(completeCallCount).toBe(1)
2412
+ // expect(completionTime).toBe(0);
2413
+ expect(timeline.isPlaying).toBe(false)
2414
+ })
2415
+
2416
+ it("should work correctly with synced sub-timelines", () => {
2417
+ let mainCompleteCount = 0
2418
+ let subCompleteCount = 0
2419
+
2420
+ const mainTimeline = createTimeline({
2421
+ duration: 2000,
2422
+ loop: false,
2423
+ autoplay: false,
2424
+ onComplete: () => mainCompleteCount++,
2425
+ })
2426
+
2427
+ const subTimeline = createTimeline({
2428
+ duration: 1000,
2429
+ loop: false,
2430
+ autoplay: false,
2431
+ onComplete: () => subCompleteCount++,
2432
+ })
2433
+
2434
+ const subTarget = { value: 0 }
2435
+ subTimeline.add(subTarget, { value: 100, duration: 800 })
2436
+ mainTimeline.add(target, { x: 50, duration: 1500 })
2437
+
2438
+ mainTimeline.sync(subTimeline, 500)
2439
+ mainTimeline.play()
2440
+
2441
+ engine.update(1300)
2442
+ expect(subCompleteCount).toBe(0)
2443
+ expect(mainCompleteCount).toBe(0)
2444
+ expect(mainTimeline.isPlaying).toBe(true)
2445
+
2446
+ engine.update(700)
2447
+ expect(subCompleteCount).toBe(1)
2448
+ expect(mainCompleteCount).toBe(1)
2449
+ expect(mainTimeline.isPlaying).toBe(false)
2450
+ })
2451
+
2452
+ it("should handle onComplete with timeline that has only callbacks", () => {
2453
+ let completeCallCount = 0
2454
+ let callbackExecuted = false
2455
+
2456
+ timeline = createTimeline({
2457
+ duration: 500,
2458
+ loop: false,
2459
+ autoplay: false,
2460
+ onComplete: () => completeCallCount++,
2461
+ })
2462
+
2463
+ timeline.call(() => {
2464
+ callbackExecuted = true
2465
+ }, 200)
2466
+ timeline.play()
2467
+
2468
+ engine.update(300)
2469
+ expect(callbackExecuted).toBe(true)
2470
+ expect(completeCallCount).toBe(0)
2471
+ expect(timeline.isPlaying).toBe(true)
2472
+
2473
+ engine.update(200)
2474
+ expect(completeCallCount).toBe(1)
2475
+ expect(timeline.isPlaying).toBe(false)
2476
+ })
2477
+
2478
+ it("should handle onComplete when timeline duration is shorter than animations", () => {
2479
+ let completeCallCount = 0
2480
+ timeline = createTimeline({
2481
+ duration: 800,
2482
+ loop: false,
2483
+ autoplay: false,
2484
+ onComplete: () => completeCallCount++,
2485
+ })
2486
+
2487
+ timeline.add(target, { x: 100, duration: 1000 })
2488
+ timeline.play()
2489
+
2490
+ engine.update(800)
2491
+ expect(completeCallCount).toBe(1)
2492
+ expect(timeline.isPlaying).toBe(false)
2493
+ expect(target.x).toBe(80)
2494
+ })
2495
+
2496
+ it("should not call onComplete multiple times on same completion", () => {
2497
+ let completeCallCount = 0
2498
+ timeline = createTimeline({
2499
+ duration: 500,
2500
+ loop: false,
2501
+ autoplay: false,
2502
+ onComplete: () => completeCallCount++,
2503
+ })
2504
+
2505
+ timeline.add(target, { x: 100, duration: 300 })
2506
+ timeline.play()
2507
+
2508
+ engine.update(500)
2509
+ expect(completeCallCount).toBe(1)
2510
+
2511
+ engine.update(100)
2512
+ engine.update(200)
2513
+ engine.update(500)
2514
+ expect(completeCallCount).toBe(1)
2515
+ })
2516
+ })
2517
+
2518
+ describe("Once Method", () => {
2519
+ it("should execute once animation immediately", () => {
2520
+ timeline = createTimeline({ duration: 2000, autoplay: false })
2521
+
2522
+ timeline.play()
2523
+ engine.update(500)
2524
+
2525
+ expect(target.x).toBe(0)
2526
+ expect(timeline.items).toHaveLength(0)
2527
+
2528
+ timeline.once(target, { x: 100, duration: 500 })
2529
+
2530
+ expect(timeline.items).toHaveLength(1)
2531
+ expect(target.x).toBe(0)
2532
+
2533
+ engine.update(250)
2534
+ expect(target.x).toBe(50)
2535
+ expect(timeline.items).toHaveLength(1)
2536
+
2537
+ engine.update(250)
2538
+ expect(target.x).toBe(100)
2539
+ expect(timeline.items).toHaveLength(0)
2540
+ })
2541
+
2542
+ it("should remove once animation after completion", () => {
2543
+ timeline = createTimeline({ duration: 2000, autoplay: false })
2544
+
2545
+ timeline.add(target, { y: 50, duration: 1000 })
2546
+ timeline.play()
2547
+
2548
+ engine.update(300)
2549
+ expect(timeline.items).toHaveLength(1)
2550
+
2551
+ timeline.once(target, { x: 100, duration: 200 })
2552
+ expect(timeline.items).toHaveLength(2)
2553
+
2554
+ engine.update(200)
2555
+ expect(target.x).toBe(100)
2556
+ expect(target.y).toBe(25)
2557
+ expect(timeline.items).toHaveLength(1)
2558
+
2559
+ engine.update(500)
2560
+ expect(target.y).toBe(50)
2561
+ expect(timeline.items).toHaveLength(1)
2562
+
2563
+ engine.update(200)
2564
+ expect(target.y).toBe(50)
2565
+ expect(timeline.items).toHaveLength(1)
2566
+ })
2567
+
2568
+ it("should not re-execute once animation when timeline loops", () => {
2569
+ timeline = createTimeline({ duration: 1000, loop: true, autoplay: false })
2570
+
2571
+ let onceStartCount = 0
2572
+ let onceCompleteCount = 0
2573
+
2574
+ timeline.play()
2575
+ engine.update(200)
2576
+
2577
+ timeline.once(target, {
2578
+ x: 100,
2579
+ duration: 300,
2580
+ onStart: () => onceStartCount++,
2581
+ onComplete: () => onceCompleteCount++,
2582
+ })
2583
+
2584
+ expect(timeline.items).toHaveLength(1)
2585
+
2586
+ engine.update(300)
2587
+ expect(target.x).toBe(100)
2588
+ expect(onceStartCount).toBe(1)
2589
+ expect(onceCompleteCount).toBe(1)
2590
+ expect(timeline.items).toHaveLength(0)
2591
+
2592
+ engine.update(500)
2593
+ expect(timeline.currentTime).toBe(0)
2594
+ expect(target.x).toBe(100)
2595
+ expect(onceStartCount).toBe(1)
2596
+ expect(onceCompleteCount).toBe(1)
2597
+ expect(timeline.items).toHaveLength(0)
2598
+ })
2599
+
2600
+ it("should handle multiple once animations", () => {
2601
+ timeline = createTimeline({ duration: 2000, autoplay: false })
2602
+
2603
+ timeline.play()
2604
+ engine.update(100)
2605
+
2606
+ const target1 = { value: 0 }
2607
+ const target2 = { value: 0 }
2608
+
2609
+ timeline.once(target1, { value: 50, duration: 200 })
2610
+ timeline.once(target2, { value: 100, duration: 300 })
2611
+
2612
+ expect(timeline.items).toHaveLength(2)
2613
+
2614
+ engine.update(200)
2615
+ expect(target1.value).toBe(50)
2616
+ expect(target2.value).toBeCloseTo(66.67, 1)
2617
+ expect(timeline.items).toHaveLength(1)
2618
+
2619
+ engine.update(100)
2620
+ expect(target1.value).toBe(50)
2621
+ expect(target2.value).toBe(100)
2622
+ expect(timeline.items).toHaveLength(0)
2623
+ })
2624
+
2625
+ it("should handle once animations with different easing functions", () => {
2626
+ timeline = createTimeline({ duration: 1000, autoplay: false })
2627
+
2628
+ timeline.play()
2629
+ engine.update(200)
2630
+
2631
+ timeline.once(target, { x: 100, duration: 400, ease: "linear" })
2632
+
2633
+ engine.update(200)
2634
+ expect(target.x).toBe(50)
2635
+
2636
+ engine.update(200)
2637
+ expect(target.x).toBe(100)
2638
+ expect(timeline.items).toHaveLength(0)
2639
+ })
2640
+
2641
+ it("should trigger onUpdate callbacks for once animations", () => {
2642
+ timeline = createTimeline({ duration: 1000, autoplay: false })
2643
+
2644
+ let updateCount = 0
2645
+ const progressValues: number[] = []
2646
+
2647
+ timeline.play()
2648
+ engine.update(100)
2649
+
2650
+ timeline.once(target, {
2651
+ x: 100,
2652
+ duration: 400,
2653
+ onUpdate: (anim: JSAnimation) => {
2654
+ updateCount++
2655
+ progressValues.push(anim.progress)
2656
+ },
2657
+ })
2658
+
2659
+ engine.update(200)
2660
+ expect(updateCount).toBe(1)
2661
+ expect(progressValues[0]).toBe(0.5)
2662
+
2663
+ engine.update(200)
2664
+ expect(updateCount).toBe(2)
2665
+ expect(progressValues[1]).toBe(1)
2666
+ expect(timeline.items).toHaveLength(0)
2667
+ })
2668
+
2669
+ it("should handle zero duration once animations", () => {
2670
+ timeline = createTimeline({ duration: 1000, autoplay: false })
2671
+
2672
+ timeline.play()
2673
+ engine.update(200)
2674
+
2675
+ timeline.once(target, { x: 100, duration: 0 })
2676
+
2677
+ expect(timeline.items).toHaveLength(1)
2678
+
2679
+ engine.update(1)
2680
+ expect(target.x).toBe(100)
2681
+ expect(timeline.items).toHaveLength(0)
2682
+ })
2683
+
2684
+ it("should handle once animations added while timeline is paused", () => {
2685
+ timeline = createTimeline({ duration: 1000, autoplay: false })
2686
+
2687
+ timeline.play()
2688
+ engine.update(300)
2689
+ timeline.pause()
2690
+
2691
+ timeline.once(target, { x: 100, duration: 200 })
2692
+
2693
+ expect(timeline.items).toHaveLength(1)
2694
+ expect(target.x).toBe(0)
2695
+
2696
+ engine.update(100)
2697
+ expect(target.x).toBe(0)
2698
+ expect(timeline.items).toHaveLength(1)
2699
+
2700
+ timeline.play()
2701
+ engine.update(100)
2702
+ expect(target.x).toBe(50)
2703
+
2704
+ engine.update(100)
2705
+ expect(target.x).toBe(100)
2706
+ expect(timeline.items).toHaveLength(0)
2707
+ })
2708
+ })
2709
+ })