@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,241 @@
1
+ import {
2
+ BezierShape,
3
+ Circle,
4
+ FPSCounter,
5
+ Game,
6
+ GameObject,
7
+ Tween,
8
+ Painter,
9
+ Scene,
10
+ Easing,
11
+ } from "../../src/index";
12
+
13
+ class MyGame extends Game {
14
+ constructor(canvas) {
15
+ super(canvas);
16
+ this.enableFluidSize();
17
+ this.backgroundColor = "black";
18
+ }
19
+
20
+ init() {
21
+ super.init();
22
+ // Set up scenes
23
+ this.scene = new Scene(this);
24
+ this.ui = new Scene(this);
25
+ this.pipeline.add(this.scene); // game layer
26
+ this.pipeline.add(this.ui); // UI layer
27
+
28
+ // Add signature animation
29
+ this.signature = new SignatureAnimation(this, {
30
+ debug: true,
31
+ anchor: "center",
32
+ });
33
+ this.signature.width = 400;
34
+ this.signature.height = 150;
35
+ this.scene.add(this.signature);
36
+
37
+ // Add FPS counter in the UI scene
38
+ this.ui.add(new FPSCounter(this, { anchor: "bottom-right" }));
39
+ }
40
+ }
41
+
42
+ // Signature Animation - A progressive bezier curve animation
43
+ class SignatureAnimation extends GameObject {
44
+ constructor(game, options = {}) {
45
+ super(game, options);
46
+
47
+ // The signature path - represents a cursive signature
48
+ this.signaturePath = [
49
+ // First part - the cursive letter (stylized name)
50
+ ["M", -200, 20],
51
+ ["C", -180, -40, -160, 40, -140, 10],
52
+ ["C", -120, -30, -100, 40, -80, 0],
53
+ ["C", -60, -40, -40, 40, -20, 10],
54
+ ["C", 0, -20, 20, -30, 40, 10],
55
+ ["C", 60, 40, 80, 40, 100, 20],
56
+ ["C", 120, 0, 140, -10, 160, 10],
57
+ ["C", 180, 30, 200, 30, 220, 10],
58
+
59
+ // The underline swoop
60
+ ["M", -180, 40],
61
+ ["C", -100, 60, 100, 80, 250, 30],
62
+ ];
63
+
64
+ // Initialize state
65
+ this.progress = 0;
66
+ this.speed = 0.3; // Speed of animation
67
+ this.complete = false;
68
+
69
+ // Create visible bezier shape for the signature
70
+ this.signature = new BezierShape(
71
+ [], // Start with empty path
72
+ {
73
+ stroke: "#fff",
74
+ lineWidth: 3,
75
+ color: null,
76
+ debug: true,
77
+ }
78
+ );
79
+
80
+ // Create a circle to represent the pen tip
81
+ this.penTip = new Circle(8, {
82
+ x: game.width / 2,
83
+ y: game.height / 2,
84
+ color: "#4f8",
85
+ shadowColor: "rgba(64, 255, 128, 0.8)",
86
+ shadowBlur: 15,
87
+ });
88
+
89
+ // Canvas click handler to restart animation
90
+ game.canvas.addEventListener("click", () => this.restart());
91
+ }
92
+
93
+ // Restart the animation
94
+ restart() {
95
+ this.progress = 0;
96
+ this.complete = false;
97
+ }
98
+
99
+ // Calculate the current point along a particular bezier curve segment
100
+ getBezierPoint(segment, t) {
101
+ if (segment[0] === "M") {
102
+ // For move commands, just return the point
103
+ return { x: segment[1], y: segment[2] };
104
+ } else if (segment[0] === "C") {
105
+ // For Cubic Bezier curves, calculate the point at t
106
+ const startX = this.prevX || 0;
107
+ const startY = this.prevY || 0;
108
+ const cp1x = segment[1];
109
+ const cp1y = segment[2];
110
+ const cp2x = segment[3];
111
+ const cp2y = segment[4];
112
+ const endX = segment[5];
113
+ const endY = segment[6];
114
+
115
+ // Cubic Bezier formula
116
+ const x =
117
+ Math.pow(1 - t, 3) * startX +
118
+ 3 * Math.pow(1 - t, 2) * t * cp1x +
119
+ 3 * (1 - t) * Math.pow(t, 2) * cp2x +
120
+ Math.pow(t, 3) * endX;
121
+
122
+ const y =
123
+ Math.pow(1 - t, 3) * startY +
124
+ 3 * Math.pow(1 - t, 2) * t * cp1y +
125
+ 3 * (1 - t) * Math.pow(t, 2) * cp2y +
126
+ Math.pow(t, 3) * endY;
127
+
128
+ return { x, y };
129
+ }
130
+
131
+ return { x: 0, y: 0 };
132
+ }
133
+
134
+ // Get a subset of the path up to the current progress
135
+ getPartialPath() {
136
+ const result = [];
137
+ let totalSegments = this.signaturePath.length;
138
+ let segmentIndex = Math.floor(this.progress * totalSegments);
139
+ let segmentProgress = (this.progress * totalSegments) % 1;
140
+
141
+ // Add all completed segments
142
+ for (let i = 0; i < segmentIndex; i++) {
143
+ result.push([...this.signaturePath[i]]);
144
+
145
+ // Keep track of the last point for calculating bezier curves
146
+ if (this.signaturePath[i][0] === "M") {
147
+ this.prevX = this.signaturePath[i][1];
148
+ this.prevY = this.signaturePath[i][2];
149
+ } else if (this.signaturePath[i][0] === "C") {
150
+ this.prevX = this.signaturePath[i][5];
151
+ this.prevY = this.signaturePath[i][6];
152
+ }
153
+ }
154
+
155
+ // Add the current segment with partial progress
156
+ if (segmentIndex < totalSegments) {
157
+ const currentSegment = this.signaturePath[segmentIndex];
158
+
159
+ if (currentSegment[0] === "M") {
160
+ // For move commands, add the full command
161
+ result.push([...currentSegment]);
162
+ this.prevX = currentSegment[1];
163
+ this.prevY = currentSegment[2];
164
+
165
+ // Position pen tip at the move point
166
+ this.penTipPos = {
167
+ x: currentSegment[1],
168
+ y: currentSegment[2],
169
+ };
170
+ } else if (currentSegment[0] === "C") {
171
+ // For bezier curves, calculate the partial command
172
+ const point = this.getBezierPoint(currentSegment, segmentProgress);
173
+
174
+ // Add a partial curve to the result
175
+ result.push([
176
+ "C",
177
+ currentSegment[1],
178
+ currentSegment[2],
179
+ currentSegment[3],
180
+ currentSegment[4],
181
+ point.x,
182
+ point.y,
183
+ ]);
184
+
185
+ // Position pen tip at the end of the partial curve
186
+ this.penTipPos = point;
187
+ }
188
+ }
189
+
190
+ return result;
191
+ }
192
+
193
+ update(dt) {
194
+ // Update progress if animation not complete
195
+ if (!this.complete) {
196
+ this.progress += dt * this.speed;
197
+
198
+ if (this.progress >= 1) {
199
+ this.progress = 1;
200
+ this.complete = true;
201
+ }
202
+ // Calculate partial path based on current progress
203
+ this.currentPath = this.getPartialPath();
204
+ // Update signature path
205
+ this.signature.path = this.currentPath;
206
+ }
207
+
208
+ // Add gentle bouncing motion when complete
209
+ if (this.complete) {
210
+ const time = performance.now() / 1000;
211
+ this.signature.y = Math.sin(time * 2) * 5;
212
+ }
213
+
214
+ // Update pen tip position
215
+ if (this.penTipPos) {
216
+ this.penTip.x = this.penTipPos.x;
217
+ this.penTip.y = this.penTipPos.y;
218
+ }
219
+ super.update(dt);
220
+ }
221
+
222
+ draw() {
223
+ super.draw();
224
+ // Draw signature
225
+ this.signature.render();
226
+ // Draw pen tip if animation is not complete
227
+ if (!this.complete) {
228
+ this.penTip.render();
229
+ }
230
+ }
231
+
232
+ render() {
233
+ super.render();
234
+ Painter.text.setFont("18px monospace");
235
+ Painter.text.setTextAlign("center");
236
+ Painter.text.setTextBaseline("bottom");
237
+ Painter.text.fillText("Click anywhere to restart the signature animation", this.game.width / 2, this.game.height - 40, "#4f8");
238
+ }
239
+ }
240
+
241
+ export { MyGame };
@@ -0,0 +1,379 @@
1
+ /**
2
+ * AccretionDisk - Manages disk particles and formation animation
3
+ *
4
+ * Handles particle creation, formation state transitions,
5
+ * camera-space projection with gravitational lensing, and Doppler effects.
6
+ */
7
+ import { GameObject, Easing } from "../../../src/index.js";
8
+ import { Particle } from "./particle.js";
9
+
10
+ // Formation source configuration
11
+ const INFALL_SOURCE_ANGLE = Math.PI * 1.25; // Top-right corner
12
+ const INFALL_STREAM_WIDTH = 0.12;
13
+ const INFALL_SPIRAL_TURNS = 1.5;
14
+
15
+ export class AccretionDisk extends GameObject {
16
+ /**
17
+ * @param {Game} game - Game instance
18
+ * @param {Object} options
19
+ * @param {Camera3D} options.camera - Camera for projection
20
+ * @param {StateMachine} options.formationFSM - Formation state machine
21
+ * @param {number} options.baseScale - Base scale for sizing
22
+ * @param {number} options.bhRadius - Black hole radius
23
+ * @param {number} options.diskInner - Inner disk radius
24
+ * @param {number} options.diskOuter - Outer disk radius
25
+ * @param {number} [options.particleCount=2500] - Number of particles
26
+ * @param {number} [options.diskTilt=0] - Disk tilt in radians
27
+ * @param {Object} [options.colors] - Color configuration
28
+ */
29
+ constructor(game, options = {}) {
30
+ super(game, options);
31
+
32
+ this.camera = options.camera;
33
+ this.formationFSM = options.formationFSM;
34
+
35
+ // Sizing (updated from main demo on resize)
36
+ this.baseScale = options.baseScale ?? 500;
37
+ this.bhRadius = options.bhRadius ?? 40;
38
+ this.diskInner = options.diskInner ?? 60;
39
+ this.diskOuter = options.diskOuter ?? 175;
40
+ this.diskTilt = options.diskTilt ?? 0;
41
+
42
+ // Particle configuration
43
+ this.particleCount = options.particleCount ?? 2500;
44
+ this.particles = [];
45
+
46
+ // Colors for temperature gradient
47
+ this.colors = options.colors ?? {
48
+ inner: [255, 250, 220], // White-hot
49
+ mid: [255, 160, 50], // Orange
50
+ outer: [180, 40, 40], // Deep red
51
+ };
52
+
53
+ // Consumption tracking
54
+ this.particlesConsumed = 0;
55
+ this.totalParticleMass = this.particleCount;
56
+ }
57
+
58
+ /**
59
+ * Update sizing when window resizes.
60
+ */
61
+ updateSizing(baseScale, bhRadius, diskInner, diskOuter) {
62
+ this.baseScale = baseScale;
63
+ this.bhRadius = bhRadius;
64
+ this.diskInner = diskInner;
65
+ this.diskOuter = diskOuter;
66
+ }
67
+
68
+ /**
69
+ * Get temperature-based color for a particle at radius r.
70
+ */
71
+ getHeatColor(r) {
72
+ const t = (r - this.diskInner) / (this.diskOuter - this.diskInner);
73
+ const c1 = t < 0.3 ? this.colors.inner : this.colors.mid;
74
+ const c2 = t < 0.3 ? this.colors.mid : this.colors.outer;
75
+ const mix = t < 0.3 ? t / 0.3 : (t - 0.3) / 0.7;
76
+
77
+ return {
78
+ r: c1[0] + (c2[0] - c1[0]) * mix,
79
+ g: c1[1] + (c2[1] - c1[1]) * mix,
80
+ b: c1[2] + (c2[2] - c1[2]) * mix,
81
+ a: 1 - t,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Initialize particles at their final disk positions.
87
+ * Used for initial load and resize.
88
+ */
89
+ initParticles() {
90
+ this.particles = [];
91
+ for (let i = 0; i < this.particleCount; i++) {
92
+ const angle = Math.random() * Math.PI * 2;
93
+ const t = Math.random();
94
+ // Bias toward inner (hotter) region
95
+ const r = this.diskInner + t * t * (this.diskOuter - this.diskInner);
96
+
97
+ const speed = (1 / Math.sqrt(r)) * 600; // Keplerian
98
+ const yOffset = (Math.random() - 0.5) * this.baseScale * 0.006;
99
+ const baseColor = this.getHeatColor(r);
100
+
101
+ this.particles.push(
102
+ Particle.createForDisk(angle, r, yOffset, speed, baseColor),
103
+ );
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Reset particles for infall animation.
109
+ * Sets up continuous stream from top-right corner.
110
+ */
111
+ initParticlesForInfall() {
112
+ this.particlesConsumed = 0;
113
+ this.totalParticleMass = this.particles.length;
114
+
115
+ for (let i = 0; i < this.particles.length; i++) {
116
+ const p = this.particles[i];
117
+
118
+ // Stream offset - like beads on a string
119
+ p.streamOffset = i / this.particles.length;
120
+
121
+ // Start position: far off-screen
122
+ const angleOffset = (Math.random() - 0.5) * INFALL_STREAM_WIDTH;
123
+ p.startAngle = INFALL_SOURCE_ANGLE + angleOffset;
124
+ p.startDistance =
125
+ this.baseScale * 0.9 + Math.random() * this.baseScale * 0.3;
126
+ p.startYOffset = (Math.random() - 0.5) * this.baseScale * 0.08;
127
+
128
+ // ~40% fall into black hole, ~60% form the disk
129
+ p.willFallIn = Math.random() < 0.4;
130
+ p.consumed = false;
131
+
132
+ if (p.willFallIn) {
133
+ p.targetDistance = 0;
134
+ p.spiralTurns = INFALL_SPIRAL_TURNS * (1.5 + Math.random() * 0.5);
135
+ } else {
136
+ p.targetAngle = Math.random() * Math.PI * 2;
137
+ const t = Math.random();
138
+ p.targetDistance =
139
+ this.diskInner + t * t * (this.diskOuter - this.diskInner);
140
+ p.spiralTurns =
141
+ INFALL_SPIRAL_TURNS +
142
+ (p.targetAngle - INFALL_SOURCE_ANGLE) / (Math.PI * 2);
143
+ }
144
+
145
+ p.targetYOffset = (Math.random() - 0.5) * this.baseScale * 0.006;
146
+
147
+ // Initialize to start position
148
+ p.angle = p.startAngle;
149
+ p.distance = p.startDistance;
150
+ p.yOffset = p.startYOffset;
151
+
152
+ // Color based on final position
153
+ p.baseColor = p.willFallIn
154
+ ? { r: 255, g: 200, b: 150, a: 1 }
155
+ : this.getHeatColor(p.targetDistance);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Normalize angle to [-PI, PI] range.
161
+ */
162
+ normalizeAngle(angle) {
163
+ while (angle > Math.PI) angle -= Math.PI * 2;
164
+ while (angle < -Math.PI) angle += Math.PI * 2;
165
+ return angle;
166
+ }
167
+
168
+ update(dt) {
169
+ super.update(dt);
170
+ this.updateParticleFormation(dt);
171
+ }
172
+
173
+ /**
174
+ * Update particle positions based on formation state.
175
+ */
176
+ updateParticleFormation(dt) {
177
+ const state = this.formationFSM.state;
178
+ const progress = this.formationFSM.progress;
179
+
180
+ for (const p of this.particles) {
181
+ if (p.consumed) continue;
182
+
183
+ if (state === "infall") {
184
+ this.updateInfall(p, progress);
185
+ } else if (state === "collapse") {
186
+ this.updateCollapse(p, progress, dt);
187
+ } else if (state === "circularize") {
188
+ this.updateCircularize(p, progress, dt);
189
+ } else if (state === "stable") {
190
+ this.updateStable(p, dt);
191
+ }
192
+ }
193
+ }
194
+
195
+ updateInfall(p, progress) {
196
+ const particleProgress = progress + p.streamOffset * 0.5;
197
+ const t = Math.min(1, Easing.easeInQuad(Math.max(0, particleProgress)));
198
+
199
+ // Spiral inward
200
+ const spiralAngle = p.startAngle + p.spiralTurns * Math.PI * 2 * t;
201
+ p.angle = spiralAngle;
202
+
203
+ const targetDist = p.willFallIn ? 0 : this.bhRadius * 2;
204
+ p.distance = Easing.lerp(p.startDistance, targetDist, t);
205
+ p.yOffset = Easing.lerp(p.startYOffset, 0, t);
206
+
207
+ // Check if consumed
208
+ if (p.willFallIn && p.distance < this.bhRadius * 0.5) {
209
+ p.consumed = true;
210
+ this.particlesConsumed++;
211
+ }
212
+ }
213
+
214
+ updateCollapse(p, progress, dt) {
215
+ // Delay particle spreading so black hole grows first
216
+ // Particles wait until 20% into collapse before spreading
217
+ const delayedProgress = Math.max(0, (progress - 0.2) / 0.8);
218
+ const t = Easing.easeOutQuad(delayedProgress);
219
+
220
+ if (p.willFallIn) {
221
+ // Falling particles spiral in immediately
222
+ const fallT = Easing.easeInQuad(progress);
223
+ p.distance = Easing.lerp(p.distance, 0, fallT * 0.7);
224
+ p.angle += p.spiralTurns * 0.1 * (1 - progress);
225
+
226
+ if (p.distance < this.bhRadius * 0.5) {
227
+ p.consumed = true;
228
+ this.particlesConsumed++;
229
+ }
230
+ } else if (delayedProgress > 0) {
231
+ // Disk particles spread slowly - reduced factors for gradual expansion
232
+ p.distance = Easing.lerp(p.distance, p.targetDistance, t * 0.3);
233
+ p.angle = Easing.lerp(p.angle, p.targetAngle, t * 0.2);
234
+ p.yOffset = Easing.lerp(p.yOffset, p.targetYOffset, t * 0.2);
235
+ }
236
+ }
237
+
238
+ updateCircularize(p, progress, dt) {
239
+ const t = Easing.easeOutCubic(progress) * p.circularizeSpeed;
240
+ const clampedT = Math.min(1, t);
241
+
242
+ p.distance = Easing.lerp(p.distance, p.targetDistance, clampedT * 0.15);
243
+ p.yOffset = Easing.lerp(p.yOffset, p.targetYOffset, clampedT * 0.2);
244
+
245
+ // Start orbital motion
246
+ const orbitSpeed = p.speed * dt * 0.01 * clampedT;
247
+ p.angle += orbitSpeed;
248
+
249
+ // Drift toward target angle
250
+ const angleDiff = this.normalizeAngle(p.targetAngle - p.angle);
251
+ p.angle += angleDiff * 0.03 * clampedT;
252
+ }
253
+
254
+ updateStable(p, dt) {
255
+ // Occasionally a particle loses angular momentum and falls in
256
+ if (
257
+ !p.isFalling &&
258
+ p.distance < this.diskInner * 1.5 &&
259
+ Math.random() < 0.0001
260
+ ) {
261
+ p.isFalling = true;
262
+ }
263
+
264
+ if (p.isFalling) {
265
+ p.distance *= 0.985;
266
+ p.angle += p.speed * dt * 0.03;
267
+ p.yOffset *= 0.95;
268
+
269
+ if (p.distance < this.bhRadius * 0.5) {
270
+ p.consumed = true;
271
+ this.particlesConsumed++;
272
+ }
273
+ } else {
274
+ // Normal Keplerian orbits
275
+ p.angle += p.speed * dt * 0.01;
276
+ p.distance = Easing.lerp(p.distance, p.targetDistance, 0.02);
277
+ p.yOffset = Easing.lerp(p.yOffset, p.targetYOffset, 0.02);
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Build render list with projected particles.
283
+ * Returns array of render items for depth sorting.
284
+ */
285
+ buildRenderList() {
286
+ const state = this.formationFSM.state;
287
+ const renderList = [];
288
+
289
+ const diskAlpha =
290
+ state === "infall" || state === "collapse"
291
+ ? 0.9
292
+ : state === "stable"
293
+ ? 1
294
+ : Math.max(0.5, this.formationFSM.progress);
295
+
296
+ // Calculate lensing strength based on formation state
297
+ let lensingStrength = 0;
298
+ if (state === "stable") {
299
+ lensingStrength = 1;
300
+ } else if (state === "circularize") {
301
+ lensingStrength =
302
+ 0.4 + Easing.easeOutCubic(this.formationFSM.progress) * 0.6;
303
+ } else if (state === "collapse") {
304
+ lensingStrength = Easing.easeInQuad(this.formationFSM.progress) * 0.4;
305
+ }
306
+
307
+ const cosTilt = Math.cos(this.diskTilt);
308
+ const sinTilt = Math.sin(this.diskTilt);
309
+
310
+ for (const p of this.particles) {
311
+ if (p.consumed) continue;
312
+
313
+ // World coordinates (flat disk)
314
+ let x = Math.cos(p.angle) * p.distance;
315
+ let z = Math.sin(p.angle) * p.distance;
316
+ let y = p.yOffset;
317
+
318
+ // Apply disk tilt
319
+ const yTilted = y * cosTilt - z * sinTilt;
320
+ const zTilted = y * sinTilt + z * cosTilt;
321
+ y = yTilted;
322
+ z = zTilted;
323
+
324
+ // Transform to camera space
325
+ const cosY = Math.cos(this.camera.rotationY);
326
+ const sinY = Math.sin(this.camera.rotationY);
327
+ let xCam = x * cosY - z * sinY;
328
+ let zCam = x * sinY + z * cosY;
329
+
330
+ const cosX = Math.cos(this.camera.rotationX);
331
+ const sinX = Math.sin(this.camera.rotationX);
332
+ let yCam = y * cosX - zCam * sinX;
333
+ zCam = y * sinX + zCam * cosX;
334
+
335
+ // Apply gravitational lensing
336
+ if (lensingStrength > 0 && zCam > 0) {
337
+ const currentR = Math.sqrt(xCam * xCam + yCam * yCam);
338
+ const ringRadius = this.bhRadius * 1.3;
339
+ const lensFactor = Math.exp(-currentR / (this.bhRadius * 1.5));
340
+ const warp = lensFactor * 1.2 * lensingStrength;
341
+
342
+ if (currentR > 0) {
343
+ const ratio = (currentR + ringRadius * warp) / currentR;
344
+ xCam *= ratio;
345
+ yCam *= ratio;
346
+ } else {
347
+ yCam = ringRadius * lensingStrength;
348
+ }
349
+ }
350
+
351
+ // Perspective projection
352
+ const perspectiveScale =
353
+ this.camera.perspective / (this.camera.perspective + zCam);
354
+ const screenX = xCam * perspectiveScale;
355
+ const screenY = yCam * perspectiveScale;
356
+
357
+ if (zCam < -this.camera.perspective + 10) continue;
358
+
359
+ // Doppler effect
360
+ const velocityDir = Math.cos(p.angle + this.camera.rotationY);
361
+ const doppler = 1 + velocityDir * 0.4;
362
+
363
+ renderList.push({
364
+ type: "particle",
365
+ z: zCam,
366
+ x: screenX,
367
+ y: screenY,
368
+ scale: perspectiveScale,
369
+ color: p.baseColor,
370
+ doppler: doppler,
371
+ diskAlpha: diskAlpha,
372
+ isFalling: p.isFalling || p.willFallIn,
373
+ horizonProximity: p.distance / this.bhRadius,
374
+ });
375
+ }
376
+
377
+ return renderList;
378
+ }
379
+ }