@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,751 @@
1
+ /**
2
+ * Root Dance - Bhaskara Formula Visualization
3
+ *
4
+ * A generative art demo visualizing the quadratic formula (Bhaskara).
5
+ * Two particles represent the roots x₁ and x₂ of ax² + bx + c = 0.
6
+ * Watch them dance as coefficients animate, merge when Δ = 0,
7
+ * and spiral into the complex plane when roots become imaginary.
8
+ */
9
+ import { Game, Painter } from "../../src/index.js";
10
+ import { GameObject } from "../../src/game/objects/go.js";
11
+ import { Rectangle } from "../../src/shapes/rect.js";
12
+ import { TextShape } from "../../src/shapes/text.js";
13
+ import { Position } from "../../src/util/position.js";
14
+ import { Synth } from "../../src/sound/synth.js";
15
+
16
+ const CONFIG = {
17
+ // Coefficient ranges for animation
18
+ aRange: [0.5, 2],
19
+ bRange: [-3, 3],
20
+ cRange: [-2, 2],
21
+ aSpeed: 0.3,
22
+ bSpeed: 0.5,
23
+ cSpeed: 0.4,
24
+
25
+ // Particles
26
+ trailLength: 60,
27
+ particleRadius: 8,
28
+ glowLayers: 4,
29
+ glowExpansion: 4,
30
+
31
+ // Coordinate mapping
32
+ rootScale: 75,
33
+ complexScale: 50,
34
+
35
+ // Effects
36
+ mergeThreshold: 0.3,
37
+ mergeFlashDuration: 0.5,
38
+ spiralSpeed: 2.0,
39
+
40
+ // Mouse influence strength
41
+ mouseInfluence: 1.5,
42
+ };
43
+
44
+ // Famous quadratic equations with assigned colors (HSL hues) and percussion
45
+ const EQUATIONS = [
46
+ { name: "easeInOutQuad", a: 2, b: -4, c: 1, hue: 45, perc: "kick" }, // Orange - 2t²-4t+1=0
47
+ { name: "Imaginary Unit", a: 1, b: 0, c: 1, hue: 280, perc: "sweep" }, // Purple - ±i (usually complex)
48
+ { name: "Mandelbrot Origin", a: 1, b: 0, c: 0, hue: 160, perc: "hihat" }, // Teal - c=0 center
49
+ ];
50
+
51
+ /**
52
+ * FormulaPanelGO - A GameObject that displays all 3 equations with color-coded headers
53
+ */
54
+ class FormulaPanelGO extends GameObject {
55
+ constructor(game, options = {}) {
56
+ const panelWidth = 260;
57
+ const rowHeight = 48;
58
+ const panelHeight = rowHeight * EQUATIONS.length + 16;
59
+
60
+ super(game, {
61
+ ...options,
62
+ width: panelWidth,
63
+ height: panelHeight,
64
+ anchor: Position.BOTTOM_LEFT,
65
+ anchorMargin: 20,
66
+ });
67
+
68
+ this.panelWidth = panelWidth;
69
+ this.panelHeight = panelHeight;
70
+ this.rowHeight = rowHeight;
71
+
72
+ // Create shapes
73
+ this.bgRect = new Rectangle({
74
+ width: panelWidth,
75
+ height: panelHeight,
76
+ color: "rgba(0, 0, 0, 0.6)",
77
+ });
78
+
79
+ // Create text shapes for each equation
80
+ this.equationRows = EQUATIONS.map((eq) => ({
81
+ nameText: new TextShape(`${eq.name} [${eq.perc}]`, {
82
+ font: "bold 11px monospace",
83
+ color: `hsl(${eq.hue}, 80%, 65%)`,
84
+ align: "left",
85
+ baseline: "top",
86
+ }),
87
+ formulaText: new TextShape("0.00x² + 0.00x + 0.00 = 0", {
88
+ font: "11px monospace",
89
+ color: "#ccc",
90
+ align: "left",
91
+ baseline: "top",
92
+ }),
93
+ rootsText: new TextShape("x₁ = 0.00, x₂ = 0.00", {
94
+ font: "10px monospace",
95
+ color: "#888",
96
+ align: "left",
97
+ baseline: "top",
98
+ }),
99
+ }));
100
+ }
101
+
102
+ setEquationValues(index, a, b, c, discriminant, x1, x2, isComplex) {
103
+ if (index < 0 || index >= this.equationRows.length) return;
104
+
105
+ const row = this.equationRows[index];
106
+
107
+ // Update formula text
108
+ const aStr = a.toFixed(2);
109
+ const bStr = b >= 0 ? `+ ${b.toFixed(2)}` : `- ${Math.abs(b).toFixed(2)}`;
110
+ const cStr = c >= 0 ? `+ ${c.toFixed(2)}` : `- ${Math.abs(c).toFixed(2)}`;
111
+ row.formulaText.text = `${aStr}x² ${bStr}x ${cStr} = 0`;
112
+
113
+ // Update root values text
114
+ if (isComplex) {
115
+ const r = x1.real.toFixed(2);
116
+ const i = x1.imag.toFixed(2);
117
+ row.rootsText.text = `x = ${r} ± ${i}i`;
118
+ row.rootsText.color = "#a78bfa"; // Purple for complex
119
+ } else {
120
+ row.rootsText.text = `x₁ = ${x1.toFixed(2)}, x₂ = ${x2.toFixed(2)}`;
121
+ row.rootsText.color = "#888";
122
+ }
123
+ }
124
+
125
+ draw() {
126
+ super.draw();
127
+
128
+ // Draw background
129
+ this.bgRect.render();
130
+
131
+ // Draw each equation row
132
+ const left = -this.panelWidth / 2 + 12;
133
+ const startTop = -this.panelHeight / 2 + 10;
134
+
135
+ this.equationRows.forEach((row, i) => {
136
+ const rowTop = startTop + i * this.rowHeight;
137
+
138
+ Painter.save();
139
+ Painter.translate(left, rowTop);
140
+ row.nameText.render();
141
+ Painter.restore();
142
+
143
+ Painter.save();
144
+ Painter.translate(left, rowTop + 14);
145
+ row.formulaText.render();
146
+ Painter.restore();
147
+
148
+ Painter.save();
149
+ Painter.translate(left, rowTop + 28);
150
+ row.rootsText.render();
151
+ Painter.restore();
152
+ });
153
+ }
154
+ }
155
+
156
+ export class BaskaraDemo extends Game {
157
+ constructor(canvas) {
158
+ super(canvas);
159
+ this.backgroundColor = "#0a0a12";
160
+ this.enableFluidSize();
161
+ this.time = 0;
162
+
163
+ // Mouse influence
164
+ this.mouseActive = false;
165
+ this.mouseX = 0;
166
+ this.mouseY = 0;
167
+ this.mouseInactiveTime = 0;
168
+ this.resumeDelay = 1.5; // seconds before animation resumes
169
+ this.setupMouseTracking();
170
+
171
+ // Initialize state for each equation
172
+ this.equations = EQUATIONS.map((eq, i) => ({
173
+ // Base equation info
174
+ name: eq.name,
175
+ baseA: eq.a,
176
+ baseB: eq.b,
177
+ baseC: eq.c,
178
+ hue: eq.hue,
179
+
180
+ // Current animated coefficients
181
+ a: eq.a,
182
+ b: eq.b,
183
+ c: eq.c,
184
+ discriminant: 0,
185
+
186
+ // Root values
187
+ x1: 0,
188
+ x2: 0,
189
+ isComplex: false,
190
+
191
+ // Root screen positions
192
+ root1: { x: 0, y: 0 },
193
+ root2: { x: 0, y: 0 },
194
+
195
+ // Trails
196
+ trail1: [],
197
+ trail2: [],
198
+
199
+ // Merge effect
200
+ merging: false,
201
+ mergeTime: 0,
202
+
203
+ // Phase offsets for unique animation per equation
204
+ phaseOffset: i * 1.2,
205
+
206
+ // Track previous root values for percussion triggers
207
+ prevX1: 0,
208
+ prevX2: 0,
209
+ }));
210
+
211
+ // Percussion timing - steady beat system
212
+ this.bpm = 120;
213
+ this.beatInterval = 60 / this.bpm; // seconds per beat
214
+ this.lastBeatTime = 0;
215
+ this.beatCount = 0;
216
+ }
217
+
218
+ init() {
219
+ super.init();
220
+ this.createFormulaPanel();
221
+ this.initSound();
222
+ }
223
+
224
+ initSound() {
225
+ this.soundEnabled = false;
226
+
227
+ // Initialize on first click (proper user gesture)
228
+ const initAudio = () => {
229
+ if (!Synth.isInitialized) {
230
+ Synth.init({ masterVolume: 0.3 });
231
+ }
232
+ Synth.resume();
233
+ this.soundEnabled = true;
234
+ this.canvas.removeEventListener("click", initAudio);
235
+ };
236
+ this.canvas.addEventListener("click", initAudio);
237
+ }
238
+
239
+ playComplexSound(eq) {
240
+ if (!this.soundEnabled) return;
241
+
242
+ // Musical scale (pentatonic) - maps nicely to any value
243
+ const pentatonic = [261.63, 293.66, 329.63, 392.00, 440.00, 523.25, 587.33, 659.25];
244
+
245
+ // Use real part to pick base note, imaginary magnitude for octave shift
246
+ const realPart = eq.isComplex ? eq.x1.real : 0;
247
+ const imagMag = eq.isComplex ? Math.abs(eq.x1.imag) : 0;
248
+
249
+ // Map real part to scale index
250
+ const noteIndex = Math.abs(Math.floor((realPart + 3) * 1.2)) % pentatonic.length;
251
+ const baseFreq = pentatonic[noteIndex];
252
+
253
+ // Imaginary magnitude shifts octave (higher = further from real axis)
254
+ const octaveShift = 1 + imagMag * 0.5;
255
+ const freq = baseFreq * octaveShift;
256
+
257
+ Synth.osc.sweep(freq, freq * 1.3, 0.35, {
258
+ type: "sine",
259
+ volume: 0.12,
260
+ });
261
+ }
262
+
263
+ playRealSound(eq) {
264
+ if (!this.soundEnabled) return;
265
+
266
+ // Musical scale for return sound
267
+ const pentatonic = [196.00, 220.00, 261.63, 293.66, 329.63, 392.00];
268
+
269
+ // Use root spread to pick note
270
+ const spread = Math.abs(eq.x1 - eq.x2);
271
+ const noteIndex = Math.floor(spread * 1.5) % pentatonic.length;
272
+ const baseFreq = pentatonic[noteIndex];
273
+
274
+ Synth.osc.sweep(baseFreq * 1.5, baseFreq, 0.3, {
275
+ type: "triangle",
276
+ volume: 0.1,
277
+ });
278
+ }
279
+
280
+ // Percussion sounds for real root movement
281
+ playKick() {
282
+ if (!this.soundEnabled) return;
283
+ const ctx = Synth.ctx;
284
+ const now = ctx.currentTime;
285
+
286
+ // Kick = low sine with pitch drop
287
+ const osc = ctx.createOscillator();
288
+ const gain = ctx.createGain();
289
+ osc.type = "sine";
290
+ osc.frequency.setValueAtTime(150, now);
291
+ osc.frequency.exponentialRampToValueAtTime(40, now + 0.1);
292
+ gain.gain.setValueAtTime(0.3, now);
293
+ gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
294
+ osc.connect(gain);
295
+ gain.connect(Synth.master);
296
+ osc.start(now);
297
+ osc.stop(now + 0.2);
298
+ }
299
+
300
+ playHihat() {
301
+ if (!this.soundEnabled) return;
302
+ const ctx = Synth.ctx;
303
+ const now = ctx.currentTime;
304
+
305
+ // Hihat = filtered noise burst
306
+ const noise = ctx.createBufferSource();
307
+ const buffer = ctx.createBuffer(1, ctx.sampleRate * 0.05, ctx.sampleRate);
308
+ const data = buffer.getChannelData(0);
309
+ for (let i = 0; i < data.length; i++) {
310
+ data[i] = Math.random() * 2 - 1;
311
+ }
312
+ noise.buffer = buffer;
313
+
314
+ const filter = ctx.createBiquadFilter();
315
+ filter.type = "highpass";
316
+ filter.frequency.value = 7000;
317
+
318
+ const gain = ctx.createGain();
319
+ gain.gain.setValueAtTime(0.15, now);
320
+ gain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
321
+
322
+ noise.connect(filter);
323
+ filter.connect(gain);
324
+ gain.connect(Synth.master);
325
+ noise.start(now);
326
+ }
327
+
328
+ playTick(pitch = 1) {
329
+ if (!this.soundEnabled) return;
330
+ const ctx = Synth.ctx;
331
+ const now = ctx.currentTime;
332
+
333
+ // Tick = short high sine blip
334
+ const osc = ctx.createOscillator();
335
+ const gain = ctx.createGain();
336
+ osc.type = "sine";
337
+ osc.frequency.value = 800 * pitch;
338
+ gain.gain.setValueAtTime(0.1, now);
339
+ gain.gain.exponentialRampToValueAtTime(0.001, now + 0.03);
340
+ osc.connect(gain);
341
+ gain.connect(Synth.master);
342
+ osc.start(now);
343
+ osc.stop(now + 0.03);
344
+ }
345
+
346
+ checkPercussion(eq, t) {
347
+ // Update previous values for any position-based logic
348
+ eq.prevX1 = eq.x1;
349
+ eq.prevX2 = eq.x2;
350
+ }
351
+
352
+ // Called once per frame to handle steady beat
353
+ updateBeat(t) {
354
+ if (!this.soundEnabled) return;
355
+
356
+ // Check if it's time for a new beat
357
+ if (t - this.lastBeatTime >= this.beatInterval) {
358
+ this.lastBeatTime = t;
359
+ this.beatCount++;
360
+
361
+ const beatInBar = this.beatCount % 4;
362
+
363
+ // Each equation plays its percussion on different beats when real
364
+ this.equations.forEach((eq, i) => {
365
+ if (eq.isComplex) return; // Skip complex roots
366
+
367
+ // Stagger beats: eq0 on beat 0, eq1 on beat 1, eq2 on beat 2
368
+ const eqBeat = i % 4;
369
+
370
+ if (beatInBar === eqBeat || (beatInBar === (eqBeat + 2) % 4)) {
371
+ // Play this equation's assigned percussion
372
+ const pitch = 0.7 + Math.abs(eq.x1) * 0.15; // Pitch tied to root position
373
+
374
+ switch (EQUATIONS[i].perc) {
375
+ case "kick":
376
+ this.playKick();
377
+ break;
378
+ case "hihat":
379
+ this.playHihat();
380
+ break;
381
+ case "sweep":
382
+ // Short tick for sweep equations when real
383
+ this.playTick(pitch);
384
+ break;
385
+ default:
386
+ this.playTick(pitch);
387
+ }
388
+ }
389
+ });
390
+ }
391
+ }
392
+
393
+ createFormulaPanel() {
394
+ this.formulaPanel = new FormulaPanelGO(this, { name: "formulaPanel" });
395
+ this.pipeline.add(this.formulaPanel);
396
+ }
397
+
398
+ setupMouseTracking() {
399
+ this.canvas.addEventListener("mousemove", (e) => {
400
+ const rect = this.canvas.getBoundingClientRect();
401
+ // Normalize to -1 to 1 range from canvas center
402
+ this.mouseX = ((e.clientX - rect.left) / rect.width - 0.5) * 2;
403
+ this.mouseY = ((e.clientY - rect.top) / rect.height - 0.5) * 2;
404
+ this.mouseActive = true;
405
+ });
406
+
407
+ this.canvas.addEventListener("mouseleave", () => {
408
+ this.mouseActive = false;
409
+ this.mouseInactiveTime = this.time;
410
+ });
411
+
412
+ this.canvas.addEventListener("mousemove", () => {
413
+ this.mouseInactiveTime = this.time; // Reset timer on any movement
414
+ });
415
+ }
416
+
417
+ update(dt) {
418
+ super.update(dt);
419
+ this.time += dt;
420
+
421
+ const t = this.time;
422
+
423
+ // Steady beat - always playing when roots are real
424
+ this.updateBeat(t);
425
+
426
+ const lerpSpeed = 3;
427
+ const centerX = this.canvas.width / 2;
428
+ const centerY = this.canvas.height / 2;
429
+
430
+ // Check if we should resume autonomous animation
431
+ const timeSinceInactive = t - this.mouseInactiveTime;
432
+ const shouldAnimate = !this.mouseActive && timeSinceInactive > this.resumeDelay;
433
+
434
+ // Update each equation - roots at their true mathematical positions
435
+ this.equations.forEach((eq, index) => {
436
+ // Phase offset gives each equation unique timing
437
+ const phase = t + eq.phaseOffset;
438
+
439
+ if (this.mouseActive) {
440
+ // Mouse X influences b, Mouse Y influences c
441
+ eq.b = eq.baseB + this.mouseX * 4;
442
+ eq.c = eq.baseC + this.mouseY * 3;
443
+ } else if (shouldAnimate) {
444
+ // Animate coefficients around base values - roots follow the math
445
+ const animB = eq.baseB + Math.sin(phase * 0.4) * 2.5;
446
+ const animC = eq.baseC + Math.sin(phase * 0.25 + index) * 2;
447
+ const blendSpeed = 2 * dt;
448
+ eq.b += (animB - eq.b) * blendSpeed;
449
+ eq.c += (animC - eq.c) * blendSpeed;
450
+ }
451
+ // else: hold at current values during delay
452
+
453
+ eq.a = eq.baseA;
454
+
455
+ // Calculate discriminant
456
+ eq.discriminant = eq.b * eq.b - 4 * eq.a * eq.c;
457
+
458
+ // Check merge state
459
+ const wasMerging = eq.merging;
460
+ eq.merging = Math.abs(eq.discriminant) < CONFIG.mergeThreshold;
461
+ if (eq.merging && !wasMerging) {
462
+ eq.mergeTime = this.time;
463
+ }
464
+
465
+ // Calculate roots
466
+ const twoA = 2 * eq.a;
467
+ const negB = -eq.b;
468
+
469
+ const wasComplex = eq.isComplex;
470
+
471
+ if (eq.discriminant >= 0) {
472
+ // Real roots - on the X axis (Y = 0)
473
+ const sqrtD = Math.sqrt(eq.discriminant);
474
+ eq.x1 = (negB + sqrtD) / twoA;
475
+ eq.x2 = (negB - sqrtD) / twoA;
476
+
477
+ eq.root1.x = centerX + eq.x1 * CONFIG.rootScale;
478
+ eq.root1.y = centerY; // Y = 0 (on real axis)
479
+ eq.root2.x = centerX + eq.x2 * CONFIG.rootScale;
480
+ eq.root2.y = centerY; // Y = 0 (on real axis)
481
+
482
+ eq.isComplex = false;
483
+
484
+ // Sound: returned to real axis
485
+ if (wasComplex) {
486
+ this.playRealSound(eq);
487
+ }
488
+
489
+ // Percussion: trigger when roots cross integer gridlines
490
+ this.checkPercussion(eq, t);
491
+ } else {
492
+ // Complex roots - positioned in complex plane (Y ≠ 0)
493
+ const realPart = negB / twoA;
494
+ const imagPart = Math.sqrt(-eq.discriminant) / twoA;
495
+
496
+ // Root positions ARE their complex values: x + yi
497
+ eq.root1.x = centerX + realPart * CONFIG.rootScale;
498
+ eq.root1.y = centerY - imagPart * CONFIG.complexScale; // -imagPart because Y grows downward
499
+ eq.root2.x = centerX + realPart * CONFIG.rootScale;
500
+ eq.root2.y = centerY + imagPart * CONFIG.complexScale; // conjugate (opposite imaginary)
501
+
502
+ eq.x1 = { real: realPart, imag: imagPart };
503
+ eq.x2 = { real: realPart, imag: -imagPart };
504
+ eq.isComplex = true;
505
+
506
+ // Sound: entered complex plane
507
+ if (!wasComplex) {
508
+ this.playComplexSound(eq);
509
+ }
510
+ }
511
+
512
+ // Update trails
513
+ eq.trail1.unshift({ x: eq.root1.x, y: eq.root1.y });
514
+ eq.trail2.unshift({ x: eq.root2.x, y: eq.root2.y });
515
+
516
+ if (eq.trail1.length > CONFIG.trailLength) {
517
+ eq.trail1.pop();
518
+ eq.trail2.pop();
519
+ }
520
+
521
+ // Update formula panel for this equation
522
+ if (this.formulaPanel) {
523
+ this.formulaPanel.setEquationValues(
524
+ index, eq.a, eq.b, eq.c,
525
+ eq.discriminant, eq.x1, eq.x2, eq.isComplex
526
+ );
527
+ }
528
+ });
529
+ }
530
+
531
+ render() {
532
+ super.render();
533
+ Painter.useCtx((ctx) => {
534
+ // Draw coordinate axes
535
+ this.drawAxes(ctx);
536
+
537
+ // Draw all equations
538
+ this.equations.forEach((eq) => {
539
+ // Draw subtle parabola curve first (behind everything)
540
+ this.drawParabola(ctx, eq);
541
+
542
+ // Draw trails
543
+ this.drawTrails(ctx, eq);
544
+
545
+ // Draw particles with glow
546
+ this.drawParticles(ctx, eq);
547
+
548
+ // Draw merge effect
549
+ if (eq.merging) {
550
+ this.drawMergeEffect(ctx, eq);
551
+ }
552
+ });
553
+ });
554
+ }
555
+
556
+ drawParabola(ctx, eq) {
557
+ // Only show parabola when roots are real (crosses the axis)
558
+ if (eq.isComplex) return;
559
+
560
+ const centerX = this.canvas.width / 2;
561
+ const centerY = this.canvas.height / 2;
562
+ const scale = CONFIG.rootScale;
563
+
564
+ // Draw parabola y = ax² + bx + c
565
+ ctx.strokeStyle = `hsla(${eq.hue}, 60%, 50%, 0.2)`;
566
+ ctx.lineWidth = 2;
567
+ ctx.beginPath();
568
+
569
+ const xRange = 5; // How far left/right to draw
570
+ const steps = 100;
571
+
572
+ for (let i = 0; i <= steps; i++) {
573
+ const x = -xRange + (i / steps) * xRange * 2;
574
+ const y = eq.a * x * x + eq.b * x + eq.c;
575
+
576
+ const screenX = centerX + x * scale;
577
+ const screenY = centerY - y * scale; // Flip Y for screen coords
578
+
579
+ if (i === 0) {
580
+ ctx.moveTo(screenX, screenY);
581
+ } else {
582
+ ctx.lineTo(screenX, screenY);
583
+ }
584
+ }
585
+
586
+ ctx.stroke();
587
+ }
588
+
589
+ drawAxes(ctx) {
590
+ const centerX = this.canvas.width / 2;
591
+ const centerY = this.canvas.height / 2;
592
+
593
+ ctx.strokeStyle = "rgba(255, 255, 255, 0.1)";
594
+ ctx.lineWidth = 1;
595
+
596
+ // X-axis (real)
597
+ ctx.beginPath();
598
+ ctx.moveTo(0, centerY);
599
+ ctx.lineTo(this.canvas.width, centerY);
600
+ ctx.stroke();
601
+
602
+ // Y-axis (imaginary)
603
+ ctx.beginPath();
604
+ ctx.moveTo(centerX, 0);
605
+ ctx.lineTo(centerX, this.canvas.height);
606
+ ctx.stroke();
607
+
608
+ // Axis labels
609
+ ctx.fillStyle = "rgba(255, 255, 255, 0.25)";
610
+ ctx.font = "12px monospace";
611
+ ctx.textAlign = "left";
612
+ ctx.fillText("Re", this.canvas.width - 30, centerY - 10);
613
+ ctx.fillText("Im", centerX + 10, 20);
614
+ }
615
+
616
+ drawTrails(ctx, eq) {
617
+ const hue = eq.hue;
618
+
619
+ for (let i = 0; i < eq.trail1.length - 1; i++) {
620
+ const alpha = (1 - i / eq.trail1.length) * 0.5;
621
+ const width = (1 - i / eq.trail1.length) * 3 + 1;
622
+
623
+ ctx.strokeStyle = `hsla(${hue}, 80%, 60%, ${alpha})`;
624
+ ctx.lineWidth = width;
625
+ ctx.lineCap = "round";
626
+
627
+ // Trail 1
628
+ ctx.beginPath();
629
+ ctx.moveTo(eq.trail1[i].x, eq.trail1[i].y);
630
+ ctx.lineTo(eq.trail1[i + 1].x, eq.trail1[i + 1].y);
631
+ ctx.stroke();
632
+
633
+ // Trail 2
634
+ ctx.beginPath();
635
+ ctx.moveTo(eq.trail2[i].x, eq.trail2[i].y);
636
+ ctx.lineTo(eq.trail2[i + 1].x, eq.trail2[i + 1].y);
637
+ ctx.stroke();
638
+ }
639
+ }
640
+
641
+ drawParticles(ctx, eq) {
642
+ const hue = eq.hue;
643
+ const radius = CONFIG.particleRadius;
644
+
645
+ // Draw glow layers (outer to inner)
646
+ for (let layer = CONFIG.glowLayers; layer >= 0; layer--) {
647
+ const layerRadius = radius + layer * CONFIG.glowExpansion;
648
+ const alpha = 0.12 * (1 - layer / CONFIG.glowLayers);
649
+
650
+ ctx.fillStyle = `hsla(${hue}, 90%, 65%, ${alpha})`;
651
+
652
+ ctx.beginPath();
653
+ ctx.arc(eq.root1.x, eq.root1.y, layerRadius, 0, Math.PI * 2);
654
+ ctx.fill();
655
+
656
+ ctx.beginPath();
657
+ ctx.arc(eq.root2.x, eq.root2.y, layerRadius, 0, Math.PI * 2);
658
+ ctx.fill();
659
+ }
660
+
661
+ // Draw core
662
+ ctx.fillStyle = `hsl(${hue}, 100%, 75%)`;
663
+ ctx.beginPath();
664
+ ctx.arc(eq.root1.x, eq.root1.y, radius * 0.7, 0, Math.PI * 2);
665
+ ctx.fill();
666
+
667
+ ctx.beginPath();
668
+ ctx.arc(eq.root2.x, eq.root2.y, radius * 0.7, 0, Math.PI * 2);
669
+ ctx.fill();
670
+
671
+ // Bright center
672
+ ctx.fillStyle = `hsl(${hue}, 50%, 95%)`;
673
+ ctx.beginPath();
674
+ ctx.arc(eq.root1.x, eq.root1.y, radius * 0.3, 0, Math.PI * 2);
675
+ ctx.fill();
676
+
677
+ ctx.beginPath();
678
+ ctx.arc(eq.root2.x, eq.root2.y, radius * 0.3, 0, Math.PI * 2);
679
+ ctx.fill();
680
+
681
+ // Labels above particles with equation color
682
+ const labelOffset = radius + 18;
683
+ ctx.textAlign = "center";
684
+ ctx.textBaseline = "bottom";
685
+
686
+ // x₁ label
687
+ ctx.font = "bold 11px monospace";
688
+ ctx.fillStyle = `hsl(${hue}, 80%, 75%)`;
689
+ ctx.fillText("x₁", eq.root1.x, eq.root1.y - labelOffset);
690
+
691
+ // x₁ value
692
+ ctx.font = "9px monospace";
693
+ ctx.fillStyle = "#777";
694
+ if (eq.isComplex) {
695
+ const sign = eq.x1.imag >= 0 ? "+" : "";
696
+ ctx.fillText(`${eq.x1.real.toFixed(1)}${sign}${eq.x1.imag.toFixed(1)}i`, eq.root1.x, eq.root1.y - labelOffset + 12);
697
+ } else {
698
+ ctx.fillText(eq.x1.toFixed(2), eq.root1.x, eq.root1.y - labelOffset + 12);
699
+ }
700
+
701
+ // x₂ label
702
+ ctx.font = "bold 11px monospace";
703
+ ctx.fillStyle = `hsl(${hue}, 80%, 75%)`;
704
+ ctx.fillText("x₂", eq.root2.x, eq.root2.y - labelOffset);
705
+
706
+ // x₂ value
707
+ ctx.font = "9px monospace";
708
+ ctx.fillStyle = "#777";
709
+ if (eq.isComplex) {
710
+ const sign = eq.x2.imag >= 0 ? "+" : "";
711
+ ctx.fillText(`${eq.x2.real.toFixed(1)}${sign}${eq.x2.imag.toFixed(1)}i`, eq.root2.x, eq.root2.y - labelOffset + 12);
712
+ } else {
713
+ ctx.fillText(eq.x2.toFixed(2), eq.root2.x, eq.root2.y - labelOffset + 12);
714
+ }
715
+ }
716
+
717
+ drawMergeEffect(ctx, eq) {
718
+ const elapsed = this.time - eq.mergeTime;
719
+ const progress = Math.min(elapsed / CONFIG.mergeFlashDuration, 1);
720
+
721
+ const midX = (eq.root1.x + eq.root2.x) / 2;
722
+ const midY = (eq.root1.y + eq.root2.y) / 2;
723
+
724
+ const ringRadius = 15 + progress * 40;
725
+ const alpha = (1 - progress) * 0.5;
726
+
727
+ ctx.strokeStyle = `hsla(${eq.hue}, 100%, 70%, ${alpha})`;
728
+ ctx.lineWidth = 2 * (1 - progress) + 1;
729
+ ctx.beginPath();
730
+ ctx.arc(midX, midY, ringRadius, 0, Math.PI * 2);
731
+ ctx.stroke();
732
+
733
+ // Inner glow
734
+ const innerAlpha = (1 - progress) * 0.3;
735
+ const innerGradient = ctx.createRadialGradient(midX, midY, 0, midX, midY, ringRadius);
736
+ innerGradient.addColorStop(0, `hsla(${eq.hue}, 100%, 80%, ${innerAlpha})`);
737
+ innerGradient.addColorStop(1, `hsla(${eq.hue}, 100%, 80%, 0)`);
738
+ ctx.fillStyle = innerGradient;
739
+ ctx.beginPath();
740
+ ctx.arc(midX, midY, ringRadius, 0, Math.PI * 2);
741
+ ctx.fill();
742
+ }
743
+ }
744
+
745
+ // Export as MyGame for backwards compatibility with HTML
746
+ export function MyGame(canvas) {
747
+ const demo = new BaskaraDemo(canvas);
748
+ return {
749
+ start: () => demo.start()
750
+ };
751
+ }