@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,204 @@
1
+ import {
2
+ Circle,
3
+ Easing,
4
+ FPSCounter,
5
+ Game,
6
+ GameObject,
7
+ Motion,
8
+ Painter,
9
+ Scene,
10
+ SVGShape,
11
+ Tween,
12
+ } from "../../src/index";
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
+ console.groupCollapsed("init");
24
+ this.scene = new Scene(this, { debug: true, debugColor: "#0f0", anchor: "center" });
25
+ this.ui = new Scene(this, { debug: true, debugColor: "#0f0", anchor: "center" });
26
+ this.pipeline.add(this.scene); // game layer
27
+ this.pipeline.add(this.ui); // UI layer
28
+ console.groupEnd();
29
+ // Add SVG path animation
30
+ console.groupCollapsed("add SVGPathAnimation");
31
+ const svg = new SVGPathAnimation(this, {
32
+ width: 210,
33
+ height: 250,
34
+ offsetX: -70,
35
+ offsetY: -35,
36
+ path: "M 0 30.276 L 0 9.358 L 0 0.845 L 17.139 0.845 L 17.139 -5.247 L 5.189 -5.247 L 5.189 -19.273 L 0 -19.273 L 0 -4.975 L 0 0.845 L -8.618 0.845 L -25.071 0.845 L -25.071 9.757 L -7.593 9.757 L -7.593 30.276 L 0 30.276 Z",
37
+ });
38
+ this.scene.add(svg);
39
+ console.groupEnd();
40
+ setTimeout(() => {
41
+ console.groupCollapsed("add SVGPathAnimation");
42
+ this.scene.add(
43
+ new SVGPathAnimation(this, {
44
+ width: 210,
45
+ height: 250,
46
+ offsetX: 70,
47
+ offsetY: 35,
48
+ path: "M -0.003 20.33 L -0.003 6.031 L -0.003 0.211 L 25.068 0.211 L 25.068 -8.702 L 7.59 -8.702 L 7.59 -29.22 L -0.003 -29.22 L -0.003 -8.303 L -0.003 0.211 L -17.141 0.211 L -17.141 6.304 L -5.194 6.304 L -5.194 20.33 L -0.003 20.33 Z",
49
+ })
50
+ );
51
+ console.groupEnd();
52
+ }, 200);
53
+ // Add FPS counter in the UI scene
54
+ console.groupCollapsed("add FPSCounter");
55
+ this.ui.add(new FPSCounter(this, { anchor: "bottom-right" }));
56
+ console.groupEnd();
57
+ this.glow = Painter.effects.createGlow('rgba(0, 255, 0, 1)', 100, {
58
+ pulseSpeed: 1,
59
+ pulseMin: 0,
60
+ pulseMax: 50,
61
+ colorShift: 0.5
62
+ });
63
+ }
64
+
65
+ update(dt) {
66
+ this.scene.width = this.width - 20;
67
+ this.scene.height = this.height - 20;
68
+ this.glow.update({ pulseSpeed: 1 });
69
+ super.update(dt);
70
+ }
71
+
72
+ render() {
73
+ super.render();
74
+ // Instructions text
75
+ Painter.text.setFont("18px monospace");
76
+ Painter.text.setTextAlign("center");
77
+ Painter.text.setTextBaseline("bottom");
78
+ Painter.text.fillText(
79
+ "Click anywhere to restart the SVG path animation",
80
+ this.width / 2,
81
+ this.height - 100,
82
+ "#0f0"
83
+ );
84
+ }
85
+ }
86
+
87
+ // SVG Path Animation - An animated SVG path drawing
88
+ class SVGPathAnimation extends GameObject {
89
+ constructor(game, options = {}) {
90
+ super(game, options);
91
+ // My Logo as an SVG
92
+ //
93
+ //
94
+ this.offsetX = options.offsetX ?? 0;
95
+ this.offsetY = options.offsetY ?? 0;
96
+ this.animTime = 0;
97
+ const path =
98
+ options.path ??
99
+ "M 50,10 L 50,40 L 20,40 L 20,60 L 50,60 L 50,90 L 70,90 L 70,60 L 100,60 L 100,40 L 70,40 L 70,10 Z";
100
+ // Initialize state
101
+ this.progress = 0;
102
+ this.speed = 0.6; // Speed of animation
103
+ this.complete = false;
104
+ // Create SVG shape with initial 0 progress
105
+ this.svgShape = new SVGShape(path, {
106
+ stroke: "#0f0", // Green color
107
+ lineWidth: 3,
108
+ color: "rgba(0, 255, 0, 0.1)",
109
+ scale: 5,
110
+ animationProgress: 1,
111
+ // debug:true,
112
+ //debugColor:"yellow",
113
+ x: options.offsetX ?? 0,
114
+ y: options.offsetY ?? 0,
115
+ width: 210,
116
+ height: 250,
117
+
118
+ });
119
+ // Create a circle to represent the drawing point
120
+ this.drawingPoint = new Circle(6, {
121
+ x: 0,
122
+ y: 0,
123
+ color: "#fff",
124
+ shadowColor: "rgba(0, 255, 0, 1)",
125
+ shadowBlur: 15,
126
+ shadowOffsetX: 0,
127
+ shadowOffsetY: 0,
128
+ });
129
+ // Canvas click handler to restart animation
130
+ game.canvas.addEventListener("click", () => this.restart());
131
+ console.log("SVGPathAnimation", this.x, this.y);
132
+ this.jittery = Math.random() * 0.2 + 0.2;
133
+ }
134
+
135
+ // Restart the animation
136
+ restart() {
137
+ this.progress = 0;
138
+ this.complete = false;
139
+ this.x = 0;
140
+ this.y = 0;
141
+ this.animTime = 0;
142
+ this.jittery = Math.random() * 0.2 + 0.2;
143
+ }
144
+
145
+ update(dt) {
146
+ //console.log(this.x, this.y);
147
+ // Update progress if animation not complete
148
+ if (!this.complete) {
149
+ this.progress += dt * this.speed;
150
+ if (this.progress >= 1) {
151
+ this.progress = 1;
152
+ this.complete = true;
153
+ this.floatState = null;
154
+ }
155
+ // Apply easing for more natural movement
156
+ const easedProgress = Easing.easeInOutQuad(this.progress);
157
+ // Update SVG shape animation progress
158
+ this.svgShape.setAnimationProgress(easedProgress);
159
+ }
160
+ let x = 0;
161
+ let y = 0;
162
+ // Add gentle bouncing motion when complete
163
+ if (this.complete) {
164
+ this.animTime = this.complete ? (this.animTime ?? 0) + (dt) : 0;
165
+ const floatResult = Motion.float(
166
+ {x:-5,y:-55},
167
+ this.animTime, // elapsed time
168
+ 1, // duration (seconds per full loop)
169
+ 1, // speed multiplier
170
+ this.jittery,
171
+ 50, // radius
172
+ true, // loop
173
+ Easing.easeInOutSine, // optional easing
174
+ {},
175
+ this.floatState // persistent state
176
+ );
177
+
178
+ this.floatState = floatResult.state;
179
+ x = floatResult.x;
180
+ y = floatResult.y;
181
+ this.drawingPoint.visible = false;
182
+ } else {
183
+ // Show the drawing point during animation
184
+ this.drawingPoint.visible = true;
185
+ // Update drawing point position to follow the current path position
186
+ const currentPoint = this.svgShape.getCurrentPoint();
187
+ this.drawingPoint.x = currentPoint.x + this.offsetX;
188
+ this.drawingPoint.y = currentPoint.y + this.offsetY;
189
+ }
190
+ this.x = x;
191
+ this.y = y;
192
+ super.update(dt);
193
+ }
194
+
195
+ draw() {
196
+ super.draw();
197
+ // Draw SVG path
198
+ this.svgShape.render();
199
+ // Draw drawing point
200
+ this.drawingPoint.render();
201
+ }
202
+ }
203
+
204
+ export { MyGame };
@@ -0,0 +1,418 @@
1
+ import { GameObject, Painter, Tweenetik, Easing } from "../../../src/index.js";
2
+ import { keplerianOmega } from "../../../src/math/orbital.js";
3
+ import { CONFIG } from "./config.js";
4
+
5
+ /**
6
+ * AccretionDisk - Keplerian particle disk with gravitational lensing
7
+ *
8
+ * Uses the same proven lensing formula as demos/js/blackhole.js:
9
+ * - Single-pass lensing that pushes particles outward
10
+ * - Einstein ring forms naturally from disk geometry
11
+ * - Doppler beaming for brightness variation
12
+ */
13
+
14
+ const DISK_CONFIG = {
15
+ // Orbital bounds (multiplier of BH radius)
16
+ innerRadiusMultiplier: 1.5,
17
+ outerRadiusMultiplier: 9.0, // Wide disk with margin from screen edges
18
+
19
+ // Particle properties
20
+ maxParticles: 4000,
21
+ particleLifetime: 80,
22
+ spawnRate: 50,
23
+
24
+ // Orbital physics
25
+ baseOrbitalSpeed: 0.8,
26
+
27
+ // Decay mechanics
28
+ decayChanceBase: 0.0002,
29
+ decaySpeedFactor: 0.995,
30
+
31
+ // Disk geometry - thin disk with some spread
32
+ diskThickness: 0.006,
33
+
34
+ // Lensing - pushes particles outward to form Einstein ring
35
+ ringRadiusFactor: 1.8, // Higher = more margin between BH and ring
36
+ lensingFalloff: 1.8, // Slightly wider falloff
37
+
38
+ // Visual - heat gradient (white-hot inner to deep red outer)
39
+ colorHot: { r: 255, g: 250, b: 220 }, // Inner (white-hot)
40
+ colorMid: { r: 255, g: 160, b: 50 }, // Mid (orange)
41
+ colorCool: { r: 180, g: 40, b: 40 }, // Outer (deep red)
42
+
43
+ sizeMin: 1,
44
+ sizeMax: 2.5,
45
+ };
46
+
47
+ export class AccretionDisk extends GameObject {
48
+ constructor(game, options = {}) {
49
+ super(game, options);
50
+
51
+ this.camera = options.camera;
52
+ this.bhRadius = options.bhRadius ?? 50;
53
+ this.bhMass = options.bhMass ?? CONFIG.blackHole.initialMass;
54
+
55
+ // Disk bounds scale with BH radius
56
+ this.innerRadius = this.bhRadius * DISK_CONFIG.innerRadiusMultiplier;
57
+ this.outerRadius = this.bhRadius * DISK_CONFIG.outerRadiusMultiplier;
58
+
59
+ // State
60
+ this.active = false;
61
+ this.lensingStrength = 0; // Ramps up during activation
62
+ this.scale = 0; // For expand-from-BH animation
63
+
64
+ // Callback when particle falls into BH
65
+ this.onParticleConsumed = options.onParticleConsumed ?? null;
66
+
67
+ // Particle array
68
+ this.particles = [];
69
+ }
70
+
71
+ /**
72
+ * Activate disk with expand-from-center animation
73
+ */
74
+ activate() {
75
+ if (this.active) return;
76
+ this.active = true;
77
+ this.scale = 0.3; // Start partially expanded so it's visible immediately
78
+ this.lensingStrength = 0;
79
+ // Expansion from BH center - 2 seconds (was 4, felt too slow)
80
+ Tweenetik.to(this, { scale: 1 }, 2.0, Easing.easeOutQuart);
81
+ // Lensing ramps up alongside scale
82
+ Tweenetik.to(this, { lensingStrength: 1 }, 2.5, Easing.easeOutQuad);
83
+ }
84
+
85
+ init() {
86
+ this.particles = [];
87
+ }
88
+
89
+ /**
90
+ * Get heat-based color for particle at given radius
91
+ */
92
+ getHeatColor(distance) {
93
+ const t = (distance - this.innerRadius) / (this.outerRadius - this.innerRadius);
94
+
95
+ let r, g, b;
96
+ if (t < 0.5) {
97
+ // Inner half: hot -> mid
98
+ const t2 = t * 2;
99
+ r = DISK_CONFIG.colorHot.r + (DISK_CONFIG.colorMid.r - DISK_CONFIG.colorHot.r) * t2;
100
+ g = DISK_CONFIG.colorHot.g + (DISK_CONFIG.colorMid.g - DISK_CONFIG.colorHot.g) * t2;
101
+ b = DISK_CONFIG.colorHot.b + (DISK_CONFIG.colorMid.b - DISK_CONFIG.colorHot.b) * t2;
102
+ } else {
103
+ // Outer half: mid -> cool
104
+ const t2 = (t - 0.5) * 2;
105
+ r = DISK_CONFIG.colorMid.r + (DISK_CONFIG.colorCool.r - DISK_CONFIG.colorMid.r) * t2;
106
+ g = DISK_CONFIG.colorMid.g + (DISK_CONFIG.colorCool.g - DISK_CONFIG.colorMid.g) * t2;
107
+ b = DISK_CONFIG.colorMid.b + (DISK_CONFIG.colorCool.b - DISK_CONFIG.colorMid.b) * t2;
108
+ }
109
+
110
+ return { r: Math.round(r), g: Math.round(g), b: Math.round(b) };
111
+ }
112
+
113
+ /**
114
+ * Spawn a new particle at random position in disk
115
+ */
116
+ spawnParticle() {
117
+ if (this.particles.length >= DISK_CONFIG.maxParticles) return;
118
+
119
+ // Balanced distribution with slight inner bias for lensing visibility
120
+ // Lower power = more particles near inner edge (where lensing is strongest)
121
+ const t = Math.pow(Math.random(), 0.6);
122
+ const distance = this.innerRadius + (this.outerRadius - this.innerRadius) * t;
123
+
124
+ const angle = Math.random() * Math.PI * 2;
125
+
126
+ // Keplerian orbital speed
127
+ const speed = keplerianOmega(distance, this.bhMass, DISK_CONFIG.baseOrbitalSpeed, this.outerRadius);
128
+
129
+ // Small vertical offset for thin disk
130
+ const baseScale = this.game.baseScale ?? Math.min(this.game.width, this.game.height);
131
+ const yOffset = (Math.random() - 0.5) * baseScale * DISK_CONFIG.diskThickness;
132
+
133
+ this.particles.push({
134
+ angle,
135
+ distance,
136
+ yOffset,
137
+ speed,
138
+ // Small random initial age prevents batch death
139
+ age: Math.random() * DISK_CONFIG.particleLifetime * 0.1, // Only 10% spread
140
+ isFalling: false,
141
+ size: DISK_CONFIG.sizeMin + Math.random() * (DISK_CONFIG.sizeMax - DISK_CONFIG.sizeMin),
142
+ baseColor: this.getHeatColor(distance),
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Capture a particle from the tidal stream
148
+ * Converts Cartesian stream particle to polar disk orbit
149
+ */
150
+ captureParticle(streamParticle) {
151
+ if (this.particles.length >= DISK_CONFIG.maxParticles) return;
152
+
153
+ const x = streamParticle.x;
154
+ const z = streamParticle.z;
155
+ const dist = Math.sqrt(x * x + z * z);
156
+
157
+ // Skip if outside disk bounds
158
+ if (dist < this.innerRadius || dist > this.outerRadius) return;
159
+
160
+ const angle = Math.atan2(z, x);
161
+
162
+ // Calculate tangential velocity from stream particle
163
+ const vx = streamParticle.vx ?? 0;
164
+ const vz = streamParticle.vz ?? 0;
165
+ const tangentVx = -z / dist;
166
+ const tangentVz = x / dist;
167
+ const tangentSpeed = vx * tangentVx + vz * tangentVz;
168
+
169
+ // Convert to angular velocity
170
+ const angularVelocity = Math.abs(tangentSpeed) / dist;
171
+
172
+ // Target Keplerian speed
173
+ const keplerianSpeed = keplerianOmega(dist, this.bhMass, DISK_CONFIG.baseOrbitalSpeed, this.outerRadius);
174
+
175
+ // Blend toward Keplerian (captured particles circularize)
176
+ const blendedSpeed = (angularVelocity + keplerianSpeed) / 2;
177
+
178
+ this.particles.push({
179
+ angle,
180
+ distance: dist,
181
+ yOffset: streamParticle.y ?? 0,
182
+ speed: blendedSpeed,
183
+ age: 0,
184
+ isFalling: false,
185
+ size: streamParticle.size ?? (DISK_CONFIG.sizeMin + Math.random() * (DISK_CONFIG.sizeMax - DISK_CONFIG.sizeMin)),
186
+ baseColor: this.getHeatColor(dist),
187
+ });
188
+ }
189
+
190
+ /**
191
+ * Check if particle should begin decay spiral
192
+ */
193
+ checkDecay(p) {
194
+ // Higher decay chance near ISCO (innermost stable circular orbit)
195
+ const iscoProximity = (p.distance - this.innerRadius) / (this.outerRadius - this.innerRadius);
196
+ const ageDecayFactor = Math.min(1, p.age / DISK_CONFIG.particleLifetime);
197
+
198
+ // Particles near inner edge or old ones are more likely to fall
199
+ const decayChance = DISK_CONFIG.decayChanceBase *
200
+ (1 + 3 * (1 - iscoProximity)) *
201
+ (1 + ageDecayFactor);
202
+
203
+ if (Math.random() < decayChance) {
204
+ p.isFalling = true;
205
+ }
206
+ }
207
+
208
+ update(dt) {
209
+ super.update(dt);
210
+
211
+ // Spawn new particles when active
212
+ if (this.active && this.particles.length < DISK_CONFIG.maxParticles) {
213
+ for (let i = 0; i < DISK_CONFIG.spawnRate; i++) {
214
+ this.spawnParticle();
215
+ }
216
+ }
217
+
218
+ // Update particles
219
+ for (let i = this.particles.length - 1; i >= 0; i--) {
220
+ const p = this.particles[i];
221
+ p.age += dt;
222
+
223
+ // Remove old particles
224
+ if (p.age > DISK_CONFIG.particleLifetime) {
225
+ this.particles.splice(i, 1);
226
+ continue;
227
+ }
228
+
229
+ if (p.isFalling) {
230
+ // Spiral inward - exponential decay
231
+ p.distance *= DISK_CONFIG.decaySpeedFactor;
232
+ p.angle += p.speed * dt * 2; // Accelerate as falls
233
+ p.yOffset *= 0.95; // Flatten toward equator
234
+
235
+ // Consumed by black hole
236
+ if (p.distance < this.bhRadius * 0.5) {
237
+ this.particles.splice(i, 1);
238
+ if (this.onParticleConsumed) {
239
+ this.onParticleConsumed();
240
+ }
241
+ continue;
242
+ }
243
+ } else {
244
+ // Normal Keplerian orbit
245
+ p.angle += p.speed * dt;
246
+
247
+ // Check for decay
248
+ this.checkDecay(p);
249
+ }
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Build render list with camera-space lensing
255
+ * Uses the proven formula from demos/js/blackhole.js
256
+ */
257
+ buildRenderList() {
258
+ const renderList = [];
259
+ if (!this.camera || this.particles.length === 0) return renderList;
260
+
261
+ const lensingStrength = this.lensingStrength;
262
+
263
+ for (const p of this.particles) {
264
+ // World coordinates (flat disk in x-z plane)
265
+ const scaledDist = p.distance * this.scale;
266
+ let x = Math.cos(p.angle) * scaledDist;
267
+ let y = p.yOffset * this.scale;
268
+ let z = Math.sin(p.angle) * scaledDist;
269
+
270
+ // Transform to camera space
271
+ const cosY = Math.cos(this.camera.rotationY);
272
+ const sinY = Math.sin(this.camera.rotationY);
273
+ let xCam = x * cosY - z * sinY;
274
+ let zCam = x * sinY + z * cosY;
275
+
276
+ const cosX = Math.cos(this.camera.rotationX);
277
+ const sinX = Math.sin(this.camera.rotationX);
278
+ let yCam = y * cosX - zCam * sinX;
279
+ zCam = y * sinX + zCam * cosX;
280
+
281
+ // === GRAVITATIONAL LENSING (from blackhole.js) ===
282
+ // Only affects particles behind the BH (zCam > 0)
283
+ if (lensingStrength > 0 && zCam > 0) {
284
+ const currentR = Math.sqrt(xCam * xCam + yCam * yCam);
285
+ const ringRadius = this.bhRadius * DISK_CONFIG.ringRadiusFactor;
286
+ const lensFactor = Math.exp(-currentR / (this.bhRadius * DISK_CONFIG.lensingFalloff));
287
+ const warp = lensFactor * 1.2 * lensingStrength;
288
+
289
+ if (currentR > 0) {
290
+ const ratio = (currentR + ringRadius * warp) / currentR;
291
+ xCam *= ratio;
292
+ yCam *= ratio;
293
+ } else {
294
+ yCam = ringRadius * lensingStrength;
295
+ }
296
+ }
297
+
298
+ // Perspective projection
299
+ const perspectiveScale = this.camera.perspective / (this.camera.perspective + zCam);
300
+ const screenX = xCam * perspectiveScale;
301
+ const screenY = yCam * perspectiveScale;
302
+
303
+ // Skip particles behind camera
304
+ if (zCam < -this.camera.perspective + 10) continue;
305
+
306
+ // Doppler beaming - approaching side brighter
307
+ const velocityDir = Math.cos(p.angle + this.camera.rotationY);
308
+ const doppler = 1 + velocityDir * 0.4;
309
+
310
+ // Age-based fade
311
+ const ageRatio = p.age / DISK_CONFIG.particleLifetime;
312
+ const alpha = Math.max(0.3, 1 - Math.pow(ageRatio, 2.5));
313
+
314
+ // Color (redshift for falling particles)
315
+ let color = p.baseColor;
316
+ if (p.isFalling) {
317
+ const fallProgress = 1 - (p.distance / this.innerRadius);
318
+ color = {
319
+ r: Math.round(p.baseColor.r * (1 - fallProgress * 0.5)),
320
+ g: Math.round(p.baseColor.g * (1 - fallProgress * 0.7)),
321
+ b: Math.round(p.baseColor.b * (1 - fallProgress * 0.3)),
322
+ };
323
+ }
324
+
325
+ renderList.push({
326
+ x: screenX,
327
+ y: screenY,
328
+ z: zCam,
329
+ scale: perspectiveScale,
330
+ color,
331
+ doppler,
332
+ alpha,
333
+ size: p.size,
334
+ });
335
+ }
336
+
337
+ // Sort back to front for proper blending
338
+ renderList.sort((a, b) => b.z - a.z);
339
+ return renderList;
340
+ }
341
+
342
+ /**
343
+ * Clear all particles
344
+ */
345
+ clear() {
346
+ this.particles = [];
347
+ }
348
+
349
+ /**
350
+ * Update BH radius - also updates disk bounds since they scale with BH
351
+ * Particles inside the event horizon are consumed; others remain in place
352
+ * and will naturally be replaced by new spawns at correct radii
353
+ */
354
+ updateBHRadius(radius) {
355
+ this.bhRadius = radius;
356
+ // Disk bounds scale with BH radius
357
+ this.innerRadius = this.bhRadius * DISK_CONFIG.innerRadiusMultiplier;
358
+ this.outerRadius = this.bhRadius * DISK_CONFIG.outerRadiusMultiplier;
359
+
360
+ // Consume particles swallowed by event horizon (same threshold as update loop)
361
+ const consumeRadius = this.bhRadius * 0.5;
362
+ for (let i = this.particles.length - 1; i >= 0; i--) {
363
+ const p = this.particles[i];
364
+
365
+ if (p.distance < consumeRadius) {
366
+ this.particles.splice(i, 1);
367
+ if (this.onParticleConsumed) {
368
+ this.onParticleConsumed();
369
+ }
370
+ }
371
+ }
372
+ }
373
+
374
+ render() {
375
+ super.render();
376
+
377
+ if (!this.active || !this.camera || this.particles.length === 0) return;
378
+
379
+ const cx = this.game.width / 2;
380
+ const cy = this.game.height / 2;
381
+ const baseScale = this.game.baseScale ?? Math.min(this.game.width, this.game.height);
382
+ const renderList = this.buildRenderList();
383
+
384
+ Painter.useCtx((ctx) => {
385
+ // Reset transform (bypass Scene3D transforms)
386
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
387
+
388
+ for (const item of renderList) {
389
+ const { r, g, b } = item.color;
390
+ const size = baseScale * 0.003 * item.scale;
391
+ if (size < 0.1) continue;
392
+
393
+ // Apply Doppler brightness
394
+ const dr = Math.min(255, Math.round(r * item.doppler));
395
+ const dg = Math.min(255, Math.round(g * item.doppler * 0.95));
396
+ const db = Math.min(255, Math.round(b * item.doppler * 0.9));
397
+
398
+ const finalAlpha = Math.max(0, Math.min(1, item.alpha * item.doppler));
399
+
400
+ // Core particle
401
+ ctx.fillStyle = `rgba(${dr}, ${dg}, ${db}, ${finalAlpha})`;
402
+ ctx.beginPath();
403
+ ctx.arc(cx + item.x, cy + item.y, size / 2, 0, Math.PI * 2);
404
+ ctx.fill();
405
+
406
+ // Additive glow for bright/close particles (from blackhole.js)
407
+ if (item.doppler > 1.1 && item.alpha > 0.5) {
408
+ ctx.globalCompositeOperation = "screen";
409
+ ctx.fillStyle = `rgba(${dr}, ${dg}, ${db}, ${finalAlpha * 0.4})`;
410
+ ctx.beginPath();
411
+ ctx.arc(cx + item.x, cy + item.y, size, 0, Math.PI * 2);
412
+ ctx.fill();
413
+ ctx.globalCompositeOperation = "source-over";
414
+ }
415
+ }
416
+ });
417
+ }
418
+ }