@guinetik/gcanvas 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/.github/workflows/release.yaml +70 -0
  2. package/.jshintrc +4 -0
  3. package/.vscode/settings.json +22 -0
  4. package/CLAUDE.md +310 -0
  5. package/blackhole.jpg +0 -0
  6. package/demo.png +0 -0
  7. package/demos/CNAME +1 -0
  8. package/demos/animations.html +31 -0
  9. package/demos/basic.html +38 -0
  10. package/demos/baskara.html +31 -0
  11. package/demos/bezier.html +35 -0
  12. package/demos/beziersignature.html +29 -0
  13. package/demos/blackhole.html +28 -0
  14. package/demos/blob.html +35 -0
  15. package/demos/demos.css +289 -0
  16. package/demos/easing.html +28 -0
  17. package/demos/events.html +195 -0
  18. package/demos/fluent.html +647 -0
  19. package/demos/fractals.html +36 -0
  20. package/demos/genart.html +26 -0
  21. package/demos/gendream.html +26 -0
  22. package/demos/group.html +36 -0
  23. package/demos/home.html +587 -0
  24. package/demos/index.html +364 -0
  25. package/demos/isometric.html +34 -0
  26. package/demos/js/animations.js +452 -0
  27. package/demos/js/basic.js +204 -0
  28. package/demos/js/baskara.js +751 -0
  29. package/demos/js/bezier.js +692 -0
  30. package/demos/js/beziersignature.js +241 -0
  31. package/demos/js/blackhole/accretiondisk.obj.js +379 -0
  32. package/demos/js/blackhole/blackhole.obj.js +318 -0
  33. package/demos/js/blackhole/index.js +409 -0
  34. package/demos/js/blackhole/particle.js +56 -0
  35. package/demos/js/blackhole/starfield.obj.js +218 -0
  36. package/demos/js/blob.js +2263 -0
  37. package/demos/js/easing.js +477 -0
  38. package/demos/js/fluent.js +183 -0
  39. package/demos/js/fractals.js +931 -0
  40. package/demos/js/fractalworker.js +93 -0
  41. package/demos/js/genart.js +268 -0
  42. package/demos/js/gendream.js +209 -0
  43. package/demos/js/group.js +140 -0
  44. package/demos/js/info-toggle.js +25 -0
  45. package/demos/js/isometric.js +863 -0
  46. package/demos/js/kerr.js +1556 -0
  47. package/demos/js/lavalamp.js +590 -0
  48. package/demos/js/layout.js +354 -0
  49. package/demos/js/mondrian.js +285 -0
  50. package/demos/js/opacity.js +275 -0
  51. package/demos/js/painter.js +484 -0
  52. package/demos/js/particles-showcase.js +514 -0
  53. package/demos/js/particles.js +299 -0
  54. package/demos/js/patterns.js +397 -0
  55. package/demos/js/penrose/artifact.js +69 -0
  56. package/demos/js/penrose/blackhole.js +121 -0
  57. package/demos/js/penrose/constants.js +73 -0
  58. package/demos/js/penrose/game.js +943 -0
  59. package/demos/js/penrose/lore.js +278 -0
  60. package/demos/js/penrose/penrosescene.js +892 -0
  61. package/demos/js/penrose/ship.js +216 -0
  62. package/demos/js/penrose/sounds.js +211 -0
  63. package/demos/js/penrose/voidparticle.js +55 -0
  64. package/demos/js/penrose/voidscene.js +258 -0
  65. package/demos/js/penrose/voidship.js +144 -0
  66. package/demos/js/penrose/wormhole.js +46 -0
  67. package/demos/js/pipeline.js +555 -0
  68. package/demos/js/scene.js +304 -0
  69. package/demos/js/scenes.js +320 -0
  70. package/demos/js/schrodinger.js +410 -0
  71. package/demos/js/schwarzschild.js +1023 -0
  72. package/demos/js/shapes.js +628 -0
  73. package/demos/js/space/alien.js +171 -0
  74. package/demos/js/space/boom.js +98 -0
  75. package/demos/js/space/boss.js +353 -0
  76. package/demos/js/space/buff.js +73 -0
  77. package/demos/js/space/bullet.js +102 -0
  78. package/demos/js/space/constants.js +85 -0
  79. package/demos/js/space/game.js +1884 -0
  80. package/demos/js/space/hud.js +112 -0
  81. package/demos/js/space/laserbeam.js +179 -0
  82. package/demos/js/space/lightning.js +277 -0
  83. package/demos/js/space/minion.js +192 -0
  84. package/demos/js/space/missile.js +212 -0
  85. package/demos/js/space/player.js +430 -0
  86. package/demos/js/space/powerup.js +90 -0
  87. package/demos/js/space/starfield.js +58 -0
  88. package/demos/js/space/starpower.js +90 -0
  89. package/demos/js/spacetime.js +559 -0
  90. package/demos/js/svgtween.js +204 -0
  91. package/demos/js/tde/accretiondisk.js +418 -0
  92. package/demos/js/tde/blackhole.js +219 -0
  93. package/demos/js/tde/blackholescene.js +209 -0
  94. package/demos/js/tde/config.js +59 -0
  95. package/demos/js/tde/index.js +695 -0
  96. package/demos/js/tde/jets.js +290 -0
  97. package/demos/js/tde/lensedstarfield.js +147 -0
  98. package/demos/js/tde/tdestar.js +317 -0
  99. package/demos/js/tde/tidalstream.js +356 -0
  100. package/demos/js/tde_old/blackhole.obj.js +354 -0
  101. package/demos/js/tde_old/debris.obj.js +791 -0
  102. package/demos/js/tde_old/flare.obj.js +239 -0
  103. package/demos/js/tde_old/index.js +448 -0
  104. package/demos/js/tde_old/star.obj.js +812 -0
  105. package/demos/js/tiles.js +312 -0
  106. package/demos/js/tweendemo.js +79 -0
  107. package/demos/js/visibility.js +102 -0
  108. package/demos/kerr.html +28 -0
  109. package/demos/lavalamp.html +27 -0
  110. package/demos/layouts.html +37 -0
  111. package/demos/logo.svg +4 -0
  112. package/demos/loop.html +84 -0
  113. package/demos/mondrian.html +32 -0
  114. package/demos/og_image.png +0 -0
  115. package/demos/opacity.html +36 -0
  116. package/demos/painter.html +39 -0
  117. package/demos/particles-showcase.html +28 -0
  118. package/demos/particles.html +24 -0
  119. package/demos/patterns.html +33 -0
  120. package/demos/penrose-game.html +31 -0
  121. package/demos/pipeline.html +737 -0
  122. package/demos/scene.html +33 -0
  123. package/demos/scenes.html +96 -0
  124. package/demos/schrodinger.html +27 -0
  125. package/demos/schwarzschild.html +27 -0
  126. package/demos/shapes.html +16 -0
  127. package/demos/space.html +85 -0
  128. package/demos/spacetime.html +27 -0
  129. package/demos/svgtween.html +29 -0
  130. package/demos/tde.html +28 -0
  131. package/demos/tiles.html +28 -0
  132. package/demos/transforms.html +400 -0
  133. package/demos/tween.html +45 -0
  134. package/demos/visibility.html +33 -0
  135. package/disk_example.png +0 -0
  136. package/docs/README.md +222 -0
  137. package/docs/concepts/architecture-overview.md +204 -0
  138. package/docs/concepts/lifecycle.md +255 -0
  139. package/docs/concepts/rendering-pipeline.md +279 -0
  140. package/docs/concepts/tde-zorder.md +106 -0
  141. package/docs/concepts/two-layer-architecture.md +229 -0
  142. package/docs/getting-started/first-game.md +354 -0
  143. package/docs/getting-started/hello-world.md +269 -0
  144. package/docs/getting-started/installation.md +157 -0
  145. package/docs/modules/collision/README.md +453 -0
  146. package/docs/modules/fluent/README.md +1075 -0
  147. package/docs/modules/game/README.md +303 -0
  148. package/docs/modules/isometric-camera.md +210 -0
  149. package/docs/modules/isometric.md +275 -0
  150. package/docs/modules/painter/README.md +328 -0
  151. package/docs/modules/particle/README.md +559 -0
  152. package/docs/modules/shapes/README.md +221 -0
  153. package/docs/modules/shapes/base/euclidian.md +123 -0
  154. package/docs/modules/shapes/base/geometry2d.md +204 -0
  155. package/docs/modules/shapes/base/renderable.md +215 -0
  156. package/docs/modules/shapes/base/shape.md +262 -0
  157. package/docs/modules/shapes/base/transformable.md +243 -0
  158. package/docs/modules/shapes/hierarchy.md +218 -0
  159. package/docs/modules/state/README.md +577 -0
  160. package/docs/modules/util/README.md +99 -0
  161. package/docs/modules/util/camera3d.md +412 -0
  162. package/docs/modules/util/scene3d.md +395 -0
  163. package/index.html +17 -0
  164. package/jsdoc.json +50 -0
  165. package/package.json +55 -0
  166. package/readme.md +599 -0
  167. package/scripts/build-demo.js +69 -0
  168. package/scripts/bundle4llm.js +276 -0
  169. package/scripts/clearconsole.js +48 -0
  170. package/src/collision/collision-system.js +332 -0
  171. package/src/collision/collision.js +303 -0
  172. package/src/collision/index.js +10 -0
  173. package/src/fluent/fluent-game.js +430 -0
  174. package/src/fluent/fluent-go.js +1060 -0
  175. package/src/fluent/fluent-layer.js +152 -0
  176. package/src/fluent/fluent-scene.js +291 -0
  177. package/src/fluent/index.js +98 -0
  178. package/src/fluent/sketch.js +380 -0
  179. package/src/game/game.js +467 -0
  180. package/src/game/index.js +49 -0
  181. package/src/game/objects/go.js +220 -0
  182. package/src/game/objects/imagego.js +30 -0
  183. package/src/game/objects/index.js +54 -0
  184. package/src/game/objects/isometric-scene.js +260 -0
  185. package/src/game/objects/layoutscene.js +549 -0
  186. package/src/game/objects/scene.js +175 -0
  187. package/src/game/objects/scene3d.js +118 -0
  188. package/src/game/objects/text.js +221 -0
  189. package/src/game/objects/wrapper.js +232 -0
  190. package/src/game/pipeline.js +243 -0
  191. package/src/game/ui/button.js +396 -0
  192. package/src/game/ui/cursor.js +93 -0
  193. package/src/game/ui/fps.js +91 -0
  194. package/src/game/ui/index.js +5 -0
  195. package/src/game/ui/togglebutton.js +93 -0
  196. package/src/game/ui/tooltip.js +249 -0
  197. package/src/index.js +25 -0
  198. package/src/io/events.js +20 -0
  199. package/src/io/index.js +86 -0
  200. package/src/io/input.js +70 -0
  201. package/src/io/keys.js +152 -0
  202. package/src/io/mouse.js +61 -0
  203. package/src/io/touch.js +39 -0
  204. package/src/logger/debugtab.js +138 -0
  205. package/src/logger/index.js +3 -0
  206. package/src/logger/loggable.js +47 -0
  207. package/src/logger/logger.js +113 -0
  208. package/src/math/complex.js +37 -0
  209. package/src/math/constants.js +1 -0
  210. package/src/math/fractal.js +1271 -0
  211. package/src/math/gr.js +201 -0
  212. package/src/math/heat.js +202 -0
  213. package/src/math/index.js +12 -0
  214. package/src/math/noise.js +433 -0
  215. package/src/math/orbital.js +191 -0
  216. package/src/math/patterns.js +1339 -0
  217. package/src/math/penrose.js +259 -0
  218. package/src/math/quantum.js +115 -0
  219. package/src/math/random.js +195 -0
  220. package/src/math/tensor.js +1009 -0
  221. package/src/mixins/anchor.js +131 -0
  222. package/src/mixins/draggable.js +72 -0
  223. package/src/mixins/index.js +2 -0
  224. package/src/motion/bezier.js +132 -0
  225. package/src/motion/bounce.js +58 -0
  226. package/src/motion/easing.js +349 -0
  227. package/src/motion/float.js +130 -0
  228. package/src/motion/follow.js +125 -0
  229. package/src/motion/hop.js +52 -0
  230. package/src/motion/index.js +82 -0
  231. package/src/motion/motion.js +1124 -0
  232. package/src/motion/orbit.js +49 -0
  233. package/src/motion/oscillate.js +39 -0
  234. package/src/motion/parabolic.js +141 -0
  235. package/src/motion/patrol.js +147 -0
  236. package/src/motion/pendulum.js +48 -0
  237. package/src/motion/pulse.js +88 -0
  238. package/src/motion/shake.js +83 -0
  239. package/src/motion/spiral.js +144 -0
  240. package/src/motion/spring.js +150 -0
  241. package/src/motion/swing.js +47 -0
  242. package/src/motion/tween.js +92 -0
  243. package/src/motion/tweenetik.js +139 -0
  244. package/src/motion/waypoint.js +210 -0
  245. package/src/painter/index.js +8 -0
  246. package/src/painter/painter.colors.js +331 -0
  247. package/src/painter/painter.effects.js +230 -0
  248. package/src/painter/painter.img.js +229 -0
  249. package/src/painter/painter.js +295 -0
  250. package/src/painter/painter.lines.js +189 -0
  251. package/src/painter/painter.opacity.js +41 -0
  252. package/src/painter/painter.shapes.js +277 -0
  253. package/src/painter/painter.text.js +273 -0
  254. package/src/particle/emitter.js +124 -0
  255. package/src/particle/index.js +11 -0
  256. package/src/particle/particle-system.js +322 -0
  257. package/src/particle/particle.js +71 -0
  258. package/src/particle/updaters.js +170 -0
  259. package/src/shapes/arc.js +43 -0
  260. package/src/shapes/arrow.js +33 -0
  261. package/src/shapes/bezier.js +42 -0
  262. package/src/shapes/circle.js +62 -0
  263. package/src/shapes/clouds.js +56 -0
  264. package/src/shapes/cone.js +219 -0
  265. package/src/shapes/cross.js +70 -0
  266. package/src/shapes/cube.js +244 -0
  267. package/src/shapes/cylinder.js +254 -0
  268. package/src/shapes/diamond.js +48 -0
  269. package/src/shapes/euclidian.js +111 -0
  270. package/src/shapes/figure.js +115 -0
  271. package/src/shapes/geometry.js +220 -0
  272. package/src/shapes/group.js +375 -0
  273. package/src/shapes/heart.js +42 -0
  274. package/src/shapes/hexagon.js +26 -0
  275. package/src/shapes/image.js +192 -0
  276. package/src/shapes/index.js +111 -0
  277. package/src/shapes/line.js +29 -0
  278. package/src/shapes/pattern.js +90 -0
  279. package/src/shapes/pin.js +44 -0
  280. package/src/shapes/poly.js +31 -0
  281. package/src/shapes/prism.js +226 -0
  282. package/src/shapes/rect.js +35 -0
  283. package/src/shapes/renderable.js +333 -0
  284. package/src/shapes/ring.js +26 -0
  285. package/src/shapes/roundrect.js +95 -0
  286. package/src/shapes/shape.js +117 -0
  287. package/src/shapes/slice.js +26 -0
  288. package/src/shapes/sphere.js +314 -0
  289. package/src/shapes/sphere3d.js +537 -0
  290. package/src/shapes/square.js +15 -0
  291. package/src/shapes/star.js +99 -0
  292. package/src/shapes/svg.js +408 -0
  293. package/src/shapes/text.js +553 -0
  294. package/src/shapes/traceable.js +83 -0
  295. package/src/shapes/transform.js +357 -0
  296. package/src/shapes/transformable.js +172 -0
  297. package/src/shapes/triangle.js +26 -0
  298. package/src/sound/index.js +17 -0
  299. package/src/sound/sound.js +473 -0
  300. package/src/sound/synth.analyzer.js +149 -0
  301. package/src/sound/synth.effects.js +207 -0
  302. package/src/sound/synth.envelope.js +59 -0
  303. package/src/sound/synth.js +229 -0
  304. package/src/sound/synth.musical.js +160 -0
  305. package/src/sound/synth.noise.js +85 -0
  306. package/src/sound/synth.oscillators.js +293 -0
  307. package/src/state/index.js +10 -0
  308. package/src/state/state-machine.js +371 -0
  309. package/src/util/camera3d.js +438 -0
  310. package/src/util/index.js +6 -0
  311. package/src/util/isometric-camera.js +235 -0
  312. package/src/util/layout.js +317 -0
  313. package/src/util/position.js +147 -0
  314. package/src/util/tasks.js +47 -0
  315. package/src/util/zindex.js +287 -0
  316. package/src/webgl/index.js +9 -0
  317. package/src/webgl/shaders/sphere-shaders.js +994 -0
  318. package/src/webgl/webgl-renderer.js +388 -0
  319. package/tde.png +0 -0
  320. package/test/math/orbital.test.js +61 -0
  321. package/test/math/tensor.test.js +114 -0
  322. package/test/particle/emitter.test.js +204 -0
  323. package/test/particle/particle-system.test.js +310 -0
  324. package/test/particle/particle.test.js +116 -0
  325. package/test/particle/updaters.test.js +386 -0
  326. package/test/setup.js +120 -0
  327. package/test/shapes/euclidian.test.js +44 -0
  328. package/test/shapes/geometry.test.js +86 -0
  329. package/test/shapes/group.test.js +86 -0
  330. package/test/shapes/rectangle.test.js +64 -0
  331. package/test/shapes/transform.test.js +379 -0
  332. package/test/util/camera3d.test.js +428 -0
  333. package/test/util/scene3d.test.js +352 -0
  334. package/types/collision.d.ts +249 -0
  335. package/types/common.d.ts +155 -0
  336. package/types/game.d.ts +497 -0
  337. package/types/index.d.ts +309 -0
  338. package/types/io.d.ts +188 -0
  339. package/types/logger.d.ts +127 -0
  340. package/types/math.d.ts +268 -0
  341. package/types/mixins.d.ts +92 -0
  342. package/types/motion.d.ts +678 -0
  343. package/types/painter.d.ts +378 -0
  344. package/types/shapes.d.ts +864 -0
  345. package/types/sound.d.ts +672 -0
  346. package/types/state.d.ts +251 -0
  347. package/types/util.d.ts +253 -0
  348. package/vite.config.js +50 -0
  349. package/vitest.config.js +13 -0
@@ -0,0 +1,144 @@
1
+ import { Motion } from "./motion";
2
+ import { Tween } from "./tween";
3
+ /**
4
+ * Spiral motion animation
5
+ *
6
+ * @param {number} centerX - X coordinate of spiral center
7
+ * @param {number} centerY - Y coordinate of spiral center
8
+ * @param {number} startRadius - Starting radius of the spiral
9
+ * @param {number} endRadius - Ending radius of the spiral
10
+ * @param {number} startAngle - Starting angle in radians
11
+ * @param {number} revolutions - Number of complete revolutions
12
+ * @param {number} elapsedTime - Total elapsed time in seconds
13
+ * @param {number} duration - Duration of animation in seconds
14
+ * @param {boolean} [loop=false] - Whether animation should loop (restart from beginning)
15
+ * @param {boolean} [yoyo=false] - Whether animation should reverse direction at the end
16
+ * @param {Function} [easingFn=null] - Optional easing function to apply
17
+ * @param {Object} [callbacks] - Optional callback functions
18
+ * @param {Object} [state] - Internal state tracking for callbacks
19
+ * @returns {Object} Animation result with x, y coordinates and metadata
20
+ */
21
+ export function spiralV1(
22
+ centerX,
23
+ centerY,
24
+ startRadius,
25
+ endRadius,
26
+ startAngle,
27
+ revolutions,
28
+ elapsedTime,
29
+ duration,
30
+ loop = false,
31
+ yoyo = false,
32
+ easingFn = null,
33
+ callbacks = {},
34
+ state = null
35
+ ) {
36
+ // Initialize state if needed
37
+ if (!state) {
38
+ state = {
39
+ started: false,
40
+ loopCount: 0,
41
+ direction: 1, // 1 = forward, -1 = backward (for yoyo)
42
+ lastDirection: 1,
43
+ };
44
+ }
45
+ // Calculate normalized time (0-1)
46
+ let t = duration > 0 ? elapsedTime / duration : 1;
47
+ let completed = false;
48
+ let activeCallbacks = { ...callbacks };
49
+ // Handle yoyo and loop logic
50
+ if (yoyo || loop) {
51
+ // For yoyo + loop, or just loop: calculate cycle
52
+ if (loop) {
53
+ if (yoyo) {
54
+ // For yoyo + loop: full cycle is 2x duration (forward + backward)
55
+ const fullCycle = duration * 2;
56
+ const cycleTime = elapsedTime % fullCycle;
57
+ const cycleCount = Math.floor(elapsedTime / fullCycle);
58
+ // Determine direction and adjusted t
59
+ const newDirection = cycleTime < duration ? 1 : -1;
60
+ t =
61
+ newDirection === 1 ? cycleTime / duration : 2 - cycleTime / duration;
62
+ // Check for direction change for callbacks
63
+ if (newDirection !== state.direction) {
64
+ state.direction = newDirection;
65
+ if (state.direction === 1 && activeCallbacks.onLoop) {
66
+ activeCallbacks.onLoop(cycleCount);
67
+ }
68
+ }
69
+ // Track loop count
70
+ if (cycleCount > state.loopCount) {
71
+ state.loopCount = cycleCount;
72
+ }
73
+ } else {
74
+ // Just loop, no yoyo: reset t at each cycle
75
+ t = t % 1;
76
+ // Track loop count for callbacks
77
+ const newLoopCount = Math.floor(elapsedTime / duration);
78
+ if (newLoopCount > state.loopCount && activeCallbacks.onLoop) {
79
+ activeCallbacks.onLoop(newLoopCount);
80
+ state.loopCount = newLoopCount;
81
+ }
82
+ }
83
+ } else if (yoyo && !loop) {
84
+ // Yoyo without loop: complete one cycle then stop
85
+ if (t <= 1) {
86
+ // Forward part of the cycle
87
+ state.direction = 1;
88
+ } else if (t <= 2) {
89
+ // Backward part of the cycle
90
+ t = 2 - t;
91
+ state.direction = -1;
92
+ } else {
93
+ // Complete
94
+ t = 0;
95
+ completed = true;
96
+ state.direction = 1;
97
+ }
98
+ }
99
+ } else {
100
+ // No loop or yoyo: standard behavior
101
+ if (t >= 1) {
102
+ t = 1;
103
+ completed = true;
104
+ }
105
+ }
106
+ // Call onStart callback once
107
+ if (!state.started && activeCallbacks.onStart) {
108
+ activeCallbacks.onStart();
109
+ state.started = true;
110
+ }
111
+ // Call onComplete callback once when non-looping animation completes
112
+ if (completed && !state.completed && activeCallbacks.onComplete) {
113
+ activeCallbacks.onComplete();
114
+ state.completed = true;
115
+ }
116
+ // Apply easing if provided
117
+ const easedT = easingFn ? easingFn(t) : t;
118
+ // Calculate current radius using linear interpolation
119
+ const radius = Tween.lerp(startRadius, endRadius, easedT);
120
+ // Calculate current angle (start angle + number of revolutions)
121
+ const angle = startAngle + easedT * revolutions * Math.PI * 2;
122
+ // Convert polar coordinates to Cartesian
123
+ const x = centerX + radius * Math.cos(angle);
124
+ const y = centerY + radius * Math.sin(angle);
125
+ // Update state for next frame
126
+ const newState = {
127
+ ...state,
128
+ lastDirection: state.direction,
129
+ };
130
+ // Return standardized result
131
+ return Motion.animationResult(
132
+ {
133
+ x,
134
+ y,
135
+ radius,
136
+ angle,
137
+ direction: state.direction, // Include current direction (1 = forward, -1 = backward)
138
+ },
139
+ t,
140
+ loop || (yoyo && !completed),
141
+ completed,
142
+ newState
143
+ );
144
+ }
@@ -0,0 +1,150 @@
1
+ import { Easing } from "./easing";
2
+ import { Motion } from "./motion";
3
+ import { Tween } from "./tween";
4
+
5
+ /**
6
+ * Spring animation that uses elastic easing functions for a bouncy spring effect
7
+ * Stateless implementation that doesn't require previous frame state
8
+ *
9
+ * @param {number} initial - Initial value (starting position)
10
+ * @param {number} target - Target value (ending position)
11
+ * @param {number} elapsedTime - Total elapsed time in seconds
12
+ * @param {number} duration - Duration of one complete cycle in seconds
13
+ * @param {boolean} [loop=false] - Whether animation should loop
14
+ * @param {boolean} [yoyo=false] - Whether animation should return to initial value
15
+ * @param {Object} [springParams] - Spring parameters
16
+ * @param {number} [springParams.stiffness=0.3] - Spring stiffness (0-1)
17
+ * @param {number} [springParams.damping=0.6] - Damping factor (0-1)
18
+ * @param {Object} [callbacks] - Optional callback functions
19
+ * @param {Function} [callbacks.onStart] - Called when animation starts
20
+ * @param {Function} [callbacks.onComplete] - Called when animation completes
21
+ * @param {Function} [callbacks.onLoop] - Called when animation loops
22
+ * @param {Function} [callbacks.onYoyoTurn] - Called when yoyo animation changes direction
23
+ * @returns {Object} Animation result with value, velocity and metadata
24
+ */
25
+ export function springV1(
26
+ initial,
27
+ target,
28
+ elapsedTime,
29
+ duration,
30
+ loop = false,
31
+ yoyo = false,
32
+ springParams = {},
33
+ callbacks = {}
34
+ ) {
35
+ // Early return for zero duration
36
+ if (duration <= 0) {
37
+ return this.animationResult(
38
+ { value: target, velocity: 0, done: true, phase: "complete" },
39
+ 1,
40
+ false,
41
+ true
42
+ );
43
+ }
44
+ // Normalize time (0-1) within current cycle
45
+ let t = elapsedTime / duration;
46
+ let yoyoPhase = "forward";
47
+ let loopCount = 0;
48
+ // Handle looping vs. non-looping
49
+ if (loop) {
50
+ // Calculate loop count (for callbacks)
51
+ loopCount = Math.floor(t);
52
+ // Use only the fractional part for looping (0-1 repeating)
53
+ t = t % 1;
54
+ // Call onLoop callback if provided and we crossed a loop boundary
55
+ if (loopCount > 0 && callbacks.onLoop) {
56
+ callbacks.onLoop(loopCount);
57
+ }
58
+ } else {
59
+ // Clamp to 1 for non-looping animations
60
+ if (t > 1) t = 1;
61
+ }
62
+ // Call onStart callback if needed (only when animation begins)
63
+ if (t > 0 && elapsedTime <= duration && callbacks.onStart) {
64
+ callbacks.onStart();
65
+ }
66
+ // For yoyo, adjust the target value based on which half of the cycle we're in
67
+ let currentTarget, currentInitial, adjustedT;
68
+ if (yoyo) {
69
+ // If in the second half of the animation, we're going back
70
+ if (t >= 0.5) {
71
+ // Swap target and initial
72
+ currentTarget = initial;
73
+ currentInitial = target;
74
+ // Rescale t to 0-1 for the second half
75
+ adjustedT = (t - 0.5) * 2;
76
+ yoyoPhase = "return";
77
+ // Call onYoyoTurn callback at the turning point
78
+ if (t >= 0.5 && t < 0.51 && callbacks.onYoyoTurn) {
79
+ callbacks.onYoyoTurn();
80
+ }
81
+ } else {
82
+ // First half of animation
83
+ currentTarget = target;
84
+ currentInitial = initial;
85
+ adjustedT = t * 2;
86
+ yoyoPhase = "forward";
87
+ }
88
+ } else {
89
+ currentTarget = target;
90
+ currentInitial = initial;
91
+ adjustedT = t;
92
+ }
93
+ // Get spring parameters with defaults
94
+ const stiffness =
95
+ springParams.stiffness !== undefined ? springParams.stiffness : 0.3;
96
+ const damping =
97
+ springParams.damping !== undefined ? springParams.damping : 0.6;
98
+ // Convert stiffness and damping to elastic easing parameters
99
+ const amplitude = Math.max(0.1, 1.0 / (damping * 1.5)); // Higher damping = lower amplitude
100
+ const period = Math.max(0.1, 0.8 / (stiffness * 1.5 + 0.5)); // Higher stiffness = higher frequency
101
+ // Use the elastic easing to generate spring-like motion
102
+ // For springs, easeOutElastic works best (starts fast, then oscillates to rest)
103
+ let easedT;
104
+ if (adjustedT < 0.99) {
105
+ // Use elastic easing for most of the animation
106
+ easedT = Easing.easeOutElastic(adjustedT, amplitude, period);
107
+ } else {
108
+ // Smoothly settle to the exact target value at the end
109
+ // This prevents small oscillations at t=1
110
+ const blendFactor = (adjustedT - 0.99) / 0.01;
111
+ const elasticT = Easing.easeOutElastic(0.99, amplitude, period);
112
+ easedT = elasticT * (1 - blendFactor) + 1 * blendFactor;
113
+ }
114
+ // Interpolate between initial and target values using the eased time
115
+ const position = Tween.lerp(currentInitial, currentTarget, easedT);
116
+ // Calculate approximate velocity by comparing positions at t and t+dt
117
+ const dt = 0.01;
118
+ const nextT = Math.min(adjustedT + dt, 1);
119
+ // Calculate next position for velocity estimation
120
+ let nextEasedT;
121
+ if (nextT < 0.99) {
122
+ nextEasedT = Easing.easeOutElastic(nextT, amplitude, period);
123
+ } else {
124
+ const blendFactor = (nextT - 0.99) / 0.01;
125
+ const elasticT = Easing.easeOutElastic(0.99, amplitude, period);
126
+ nextEasedT = elasticT * (1 - blendFactor) + 1 * blendFactor;
127
+ }
128
+ const nextPosition = Tween.lerp(currentInitial, currentTarget, nextEasedT);
129
+ // Calculate velocity (scaled by duration to account for time)
130
+ const velocity = ((nextPosition - position) / dt) * duration;
131
+ // Check if non-looping animation is complete
132
+ const isDone = !loop && t >= 1;
133
+ // Call onComplete if animation has completed
134
+ if (isDone && callbacks.onComplete) {
135
+ callbacks.onComplete();
136
+ }
137
+ // Return standardized result with additional spring metadata
138
+ return Motion.animationResult(
139
+ {
140
+ value: position,
141
+ velocity: velocity,
142
+ delta: yoyoPhase === "forward" ? target - position : initial - position,
143
+ done: isDone,
144
+ phase: yoyoPhase,
145
+ },
146
+ t,
147
+ loop,
148
+ isDone
149
+ );
150
+ }
@@ -0,0 +1,47 @@
1
+ import { Motion } from "./motion";
2
+
3
+ /**
4
+ * Swing animation - oscillates around a fixed center angle
5
+ *
6
+ * @param {number} centerX - X pivot (not used for angle, just for semantic)
7
+ * @param {number} centerY - Y pivot (not used for angle, just for semantic)
8
+ * @param {number} maxAngle - Maximum swing angle in radians (e.g. Math.PI / 6)
9
+ * @param {number} elapsedTime - Total elapsed time in seconds
10
+ * @param {number} duration - Duration of one full swing cycle
11
+ * @param {boolean} [loop=true] - Whether the animation loops
12
+ * @param {boolean} [yoyo=true] - Whether the swing reverses
13
+ * @param {Function} [easingFn=null] - Optional easing function
14
+ * @param {Object} [callbacks={}] - Optional callbacks (onStart, onComplete, etc)
15
+ * @param {Object} [state=null] - Internal state tracking
16
+ * @returns {Object} Animation result with angle and metadata
17
+ */
18
+ export function swingV1(
19
+ centerX,
20
+ centerY,
21
+ maxAngle,
22
+ elapsedTime,
23
+ duration,
24
+ loop = true,
25
+ yoyo = true,
26
+ easingFn = null,
27
+ callbacks = {},
28
+ state = null
29
+ ) {
30
+ // Process time and easing
31
+ const {
32
+ t,
33
+ easedT,
34
+ completed,
35
+ state: newState,
36
+ } = Motion._frame(elapsedTime, duration, loop, easingFn, callbacks, state);
37
+
38
+ // Swing back and forth using sine curve
39
+ const phase = yoyo
40
+ ? Math.sin(easedT * Math.PI * 2)
41
+ : Math.sin(easedT * Math.PI);
42
+
43
+ // Calculate angle: center ± maxAngle
44
+ const angle = phase * maxAngle;
45
+
46
+ return Motion.animationResult({ angle }, t, loop, completed, newState);
47
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Tween module providing core interpolation utilities for animations.
3
+ *
4
+ * @class Tween
5
+ * @description
6
+ * Tween is a stateless utility class for performing basic and advanced interpolation operations.
7
+ * These methods are low-level math tools used across both simulation-driven animations (like {@link Motion})
8
+ * and UI-driven transitions (like {@link Tweenetik}).
9
+ *
10
+ * The class includes:
11
+ * - Scalar and angle interpolation
12
+ * - Color blending in RGB and HSL spaces
13
+ * - Angle-safe easing
14
+ *
15
+ * All functions are deterministic, pure, and suitable for use in render loops or transition systems.
16
+ *
17
+ * @example
18
+ * const x = Tween.lerp(0, 100, 0.25); // 25
19
+ * const angle = Tween.lerpAngle(Math.PI, -Math.PI, 0.5); // shortest-path rotation
20
+ * const rgb = Tween.tweenColor([255, 0, 0], [0, 0, 255], 0.5); // purple
21
+ *
22
+ * @see {@link Motion} for game-loop-driven animation patterns
23
+ * @see {@link Tweenetik} for time-based UI transitions
24
+ */
25
+ export class Tween {
26
+ /**
27
+ * Linear interpolation between two scalar values.
28
+ * Also known as Lerp.
29
+ * @param {number} start
30
+ * @param {number} end
31
+ * @param {number} t - Normalized interpolation factor (0 to 1)
32
+ * @returns {number} Interpolated scalar
33
+ */
34
+ static lerp(start, end, t) {
35
+ return start + (end - start) * t;
36
+ }
37
+
38
+ /**
39
+ * Smoothly interpolates between two angles (in radians),
40
+ * always taking the shortest path around the circle.
41
+ *
42
+ * Prevents snapping when angles wrap across -π to π boundaries.
43
+ *
44
+ * @param {number} a - Starting angle in radians
45
+ * @param {number} b - Target angle in radians
46
+ * @param {number} t - Interpolation factor (0 to 1)
47
+ * @returns {number} Interpolated angle in radians
48
+ */
49
+ static lerpAngle(a, b, t) {
50
+ // Calculate raw difference
51
+ let diff = b - a;
52
+ // Wrap difference to range [-PI, PI] for shortest rotation
53
+ while (diff < -Math.PI) diff += Math.PI * 2;
54
+ while (diff > Math.PI) diff -= Math.PI * 2;
55
+ // Interpolate from a toward b using the wrapped difference
56
+ return a + diff * t;
57
+ }
58
+
59
+ /**
60
+ * Interpolate between two colors in RGB space
61
+ * @param {Array<number>} color1 - Start color [r, g, b] (0-255)
62
+ * @param {Array<number>} color2 - End color [r, g, b] (0-255)
63
+ * @param {number} t - Interpolation factor (0-1)
64
+ * @returns {Array<number>} Interpolated color [r, g, b]
65
+ */
66
+ static tweenColor(color1, color2, t) {
67
+ return color1.map((c, i) => Tween.lerp(c, color2[i], t));
68
+ }
69
+
70
+ /**
71
+ * Interpolate between two HSL colors
72
+ * @param {Array<number>} color1 - Start color [h, s, l]
73
+ * @param {Array<number>} color2 - End color [h, s, l]
74
+ * @param {number} t - Interpolation factor (0-1)
75
+ * @returns {Array<number>} Interpolated color [h, s, l]
76
+ */
77
+ static tweenGradient(color1, color2, t) {
78
+ // Special handling for hue to handle the circular nature
79
+ let h1 = color1[0];
80
+ let h2 = color2[0];
81
+ // Find the shortest path around the circle
82
+ if (Math.abs(h2 - h1) > 180) {
83
+ if (h1 < h2) h1 += 360;
84
+ else h2 += 360;
85
+ }
86
+ // tween each channel individually
87
+ const h = Tween.lerp(h1, h2, t) % 360;
88
+ const s = Tween.lerp(color1[1], color2[1], t);
89
+ const l = Tween.lerp(color1[2], color2[2], t);
90
+ return [h, s, l];
91
+ }
92
+ }
@@ -0,0 +1,139 @@
1
+ // Tweenetik.js
2
+ import { Easing } from "./easing.js";
3
+ import { Tween } from "./tween.js";
4
+ /**
5
+ * Tweenetik module for declarative, time-managed UI animations.
6
+ *
7
+ * @class Tweenetik
8
+ * @description
9
+ * Tweenetik provides a lightweight, self-managed tweening system that animates object properties over time.
10
+ * Unlike {@link Motion}, which returns stateless values for use in real-time game loops,
11
+ * Tweenetik mutates the target object directly and manages its own internal time progression.
12
+ * <br/>
13
+ * It is ideal for UI transitions, hover effects, attention animations, or any element
14
+ * that animates independently of the simulation/game loop logic.
15
+ * <br/>
16
+ * Tweenetik is declarative: you configure a tween once and let it run in the background.
17
+ * You must call {@link Tweenetik.updateAll} on each frame to drive all active tweens.
18
+ * <br/>
19
+ * @example
20
+ * Tweenetik.to(button, { scaleX: 1.2, scaleY: 1.2 }, 0.5, Easing.easeOutBack);
21
+ *
22
+ * @see {@link Tween} for interpolation utilities
23
+ * @see {@link Motion} for stateless simulation-driven animations
24
+ */
25
+
26
+ export class Tweenetik {
27
+ /**
28
+ * @param {Object} target - The object whose properties will be tweened.
29
+ * @param {Object} toProps - An object containing the property values we want to end up at (e.g. { x: 100, y: 200 }).
30
+ * @param {number} duration - How long (in seconds) the tween should take.
31
+ * @param {Function} easingFn - One of the easing functions from the Tween class (e.g. Easing.easeOutBounce).
32
+ * @param {Object} [options]
33
+ * @param {number} [options.delay=0] - Delay in seconds before starting.
34
+ * @param {Function} [options.onStart] - Callback invoked once when the tween actually begins.
35
+ * @param {Function} [options.onComplete] - Callback invoked once when the tween finishes.
36
+ * @param {Function} [options.onUpdate] - Callback invoked every frame (after the object’s properties are updated).
37
+ */
38
+ constructor(target, toProps, duration, easingFn, options = {}) {
39
+ this.target = target;
40
+ this.toProps = { ...toProps };
41
+ this.duration = duration;
42
+ this.easingFn = easingFn || Easing.easeOutQuad;
43
+
44
+ // Options
45
+ this.delay = options.delay || 0;
46
+ this.onStart = options.onStart || null;
47
+ this.onComplete = options.onComplete || null;
48
+ this.onUpdate = options.onUpdate || null;
49
+
50
+ // Internal
51
+ this._elapsed = 0;
52
+ this._started = false;
53
+ this._finished = false;
54
+ this._startProps = {};
55
+
56
+ for (const prop in this.toProps) {
57
+ if (prop in this.target) {
58
+ this._startProps[prop] = this.target[prop];
59
+ }
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Creates and registers a new Tweenetik instance in the global list.
65
+ * @param {Object} target
66
+ * @param {Object} toProps
67
+ * @param {number} duration
68
+ * @param {Function} easingFn
69
+ * @param {Object} [options]
70
+ */
71
+ static to(target, toProps, duration, easingFn, options) {
72
+ const tween = new Tweenetik(target, toProps, duration, easingFn, options);
73
+ Tweenetik._active.push(tween);
74
+ return tween;
75
+ }
76
+
77
+ /**
78
+ * Updates this tween by the given delta time (in seconds).
79
+ * @param {number} dt
80
+ */
81
+ update(dt) {
82
+ if (this._finished) return;
83
+
84
+ this._elapsed += dt;
85
+
86
+ // Wait until the delay has passed
87
+ if (this._elapsed < this.delay) {
88
+ return;
89
+ }
90
+
91
+ // Time in tween (after delay)
92
+ const timeSinceDelay = this._elapsed - this.delay;
93
+ const t = Math.min(timeSinceDelay / this.duration, 1);
94
+
95
+ // onStart callback (once)
96
+ if (!this._started && t > 0) {
97
+ this._started = true;
98
+ if (this.onStart) {
99
+ this.onStart();
100
+ }
101
+ }
102
+
103
+ // Calculate eased factor
104
+ const eased = this.easingFn(t);
105
+ //console.log("Tweenetik.update", this._startProps);
106
+ // Apply interpolations for each property
107
+ for (const prop in this._startProps) {
108
+ const startVal = this._startProps[prop];
109
+ const endVal = this.toProps[prop];
110
+ // Use Tween.go(...) to interpolate
111
+ this.target[prop] = Tween.lerp(startVal, endVal, eased);
112
+ }
113
+
114
+ // onUpdate callback (each frame)
115
+ if (this.onUpdate) {
116
+ this.onUpdate();
117
+ }
118
+
119
+ // Check if finished
120
+ if (t >= 1) {
121
+ this._finished = true;
122
+ if (this.onComplete) {
123
+ this.onComplete();
124
+ }
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Update all active tweens by dt. Call this once each frame in your main loop.
130
+ * @param {number} dt
131
+ */
132
+ static updateAll(dt) {
133
+ for (const tween of Tweenetik._active) {
134
+ tween.update(dt);
135
+ }
136
+ // Remove finished tweens from the list
137
+ Tweenetik._active = Tweenetik._active.filter((t) => !t._finished);
138
+ }
139
+ }