@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,692 @@
1
+ import {
2
+ Game,
3
+ Scene,
4
+ FPSCounter,
5
+ Button,
6
+ HorizontalLayout,
7
+ VerticalLayout,
8
+ Painter,
9
+ ToggleButton,
10
+ Cursor,
11
+ TextShape,
12
+ Circle,
13
+ Rectangle,
14
+ BezierShape,
15
+ ShapeGOFactory,
16
+ Position,
17
+ } from "../../src/index";
18
+
19
+ /**
20
+ * Main Game class for the Bezier Demo
21
+ */
22
+ class BezierDemoGame extends Game {
23
+ constructor(canvas) {
24
+ super(canvas);
25
+ this.enableFluidSize();
26
+ this.backgroundColor = "black";
27
+ }
28
+
29
+ /**
30
+ * Check if we're on a touch device
31
+ */
32
+ isTouchDevice() {
33
+ return (
34
+ "ontouchstart" in window ||
35
+ navigator.maxTouchPoints > 0 ||
36
+ navigator.msMaxTouchPoints > 0
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Check if screen is narrow (mobile width)
42
+ */
43
+ isMobile() {
44
+ return this.width < 600;
45
+ }
46
+
47
+ /**
48
+ * Get responsive configuration based on screen size
49
+ */
50
+ getResponsiveConfig() {
51
+ const isMobile = this.isMobile();
52
+ return {
53
+ // Use shorter labels on mobile
54
+ addLabel: isMobile ? "➕ Add" : "➕ Add Points",
55
+ editLabel: isMobile ? "✋ Edit" : "✋ Edit Points",
56
+ cutLabel: isMobile ? "✂️ Cut" : "✂️ Cut Shape",
57
+ clearLabel: isMobile ? "🧼" : "🧼 Clear",
58
+ // Narrower buttons on mobile
59
+ buttonWidth: isMobile ? 70 : 125,
60
+ clearWidth: isMobile ? 40 : 100,
61
+ // UI layout dimensions
62
+ layoutWidth: isMobile ? 280 : 500,
63
+ layoutHeight: 50,
64
+ };
65
+ }
66
+
67
+ init() {
68
+ super.init();
69
+ // Create the scenes
70
+ this.bezierScene = new BezierScene(this, {
71
+ debug: true,
72
+ debugColor: "yellow",
73
+ });
74
+
75
+ const config = this.getResponsiveConfig();
76
+
77
+ this.uiScene = new BezierUIScene(this, this.bezierScene, {
78
+ debug: true,
79
+ debugColor: "magenta",
80
+ width: config.layoutWidth,
81
+ height: config.layoutHeight,
82
+ padding: 10,
83
+ anchor: Position.BOTTOM_CENTER,
84
+ anchorOffsetY: -20,
85
+ });
86
+
87
+ // Add them to the pipeline
88
+ this.pipeline.add(this.bezierScene);
89
+ this.pipeline.add(this.uiScene);
90
+ this.uiScene.init();
91
+
92
+ // Show FPS counter
93
+ this.pipeline.add(new FPSCounter(this, { anchor: "bottom-right" }));
94
+
95
+ // Only setup custom cursor on non-touch devices
96
+ if (!this.isTouchDevice()) {
97
+ this.addCursor = new TextShape("➕", {
98
+ font: "24px monospace",
99
+ color: "white",
100
+ });
101
+ this.editCursor = new TextShape("✋", {
102
+ font: "24px monospace",
103
+ color: "white",
104
+ });
105
+ this.cursor = new Cursor(this, this.addCursor, this.addCursor, {
106
+ x: 0,
107
+ y: 0,
108
+ });
109
+ }
110
+ }
111
+
112
+ onResize() {
113
+ if (this.uiScene) {
114
+ this.uiScene.onResize();
115
+ }
116
+ }
117
+ }
118
+
119
+ /**
120
+ * The BezierScene:
121
+ * - Allows users to place control points by clicking
122
+ * - Visualizes the Bezier curve as it's being created
123
+ * - Provides UI to finish/clear the curve
124
+ */
125
+ class BezierScene extends Scene {
126
+ constructor(game, options = {}) {
127
+ super(game, options);
128
+ this.MARGIN = 0;
129
+ this.interactive = true;
130
+ // Control points the user has placed
131
+ this.points = [];
132
+ // Current bezier path that will be visualized
133
+ this.bezierPath = [];
134
+ // The bezier shape that will be rendered
135
+ this.bezierShape = null;
136
+ // Point being dragged (if any)
137
+ this.draggedPointIndex = -1;
138
+ // Visual elements for control points
139
+ this.controlPointShapes = [];
140
+ // User interface state
141
+ this.mode = "add"; // "add" or "edit"
142
+ // Forward input events to the bezier scene
143
+ this.game.events.on("inputdown", (e) => {
144
+ //console.log("inputdown", e);
145
+ const x = e.x - this.width / 2;
146
+ const y = e.y - this.height / 2;
147
+ this.pointerDown(x, y);
148
+ });
149
+ // Forward input move event to the bezier scene
150
+ this.game.events.on("inputmove", (e) => {
151
+ const x = e.x - this.width / 2;
152
+ const y = e.y - this.height / 2;
153
+ this.pointerMove(x, y);
154
+ });
155
+ // Forward input up event to the bezier scene
156
+ this.game.events.on("inputup", (e) => {
157
+ this.pointerUp();
158
+ });
159
+ //
160
+ this.userBeziers = [];
161
+ }
162
+
163
+ /**
164
+ * Set the current mode (add points or edit existing points)
165
+ */
166
+ setMode(mode) {
167
+ this.mode = mode;
168
+
169
+ // Only update cursor on non-touch devices
170
+ if (this.game.cursor) {
171
+ if (mode === "add") {
172
+ this.game.cursor.normalShape = this.game.cursor.pressedShape =
173
+ this.game.addCursor;
174
+ this.game.cursor.offsetX = 1;
175
+ this.game.cursor.offsetY = -6;
176
+ } else {
177
+ this.game.cursor.normalShape = this.game.cursor.pressedShape =
178
+ this.game.editCursor;
179
+ this.game.cursor.offsetX = 3;
180
+ this.game.cursor.offsetY = -3;
181
+ }
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Add a control point at the specified position
187
+ */
188
+ addPoint(x, y) {
189
+ x = x - 50;
190
+ y = y - 50;
191
+ this.points.push({ x, y });
192
+ // Create a visual representation of the control point
193
+ const pointShape = new Circle(6, {
194
+ color: "#00FF00",
195
+ stroke: "#e2FFe2",
196
+ lineWidth: 2,
197
+ });
198
+ const go = ShapeGOFactory.create(this.game, pointShape, { x: x, y: y });
199
+ this.controlPointShapes.push(go);
200
+ this.add(go);
201
+ // Update the bezier path
202
+ this.updateBezierPath();
203
+ }
204
+
205
+ /**
206
+ * Updates the Bezier path based on current points
207
+ * This is where we convert our points array into path commands
208
+ */
209
+ /**
210
+ * Updates the Bezier path based on current points
211
+ * This is where we convert our points array into path commands
212
+ */
213
+ updateBezierPath() {
214
+ if (this.points.length < 2) {
215
+ // Need at least 2 points to create a path
216
+ this.bezierPath = [];
217
+ if (this.bezierShape) {
218
+ this.remove(this.bezierShape);
219
+ this.bezierShape = null;
220
+ }
221
+ return;
222
+ }
223
+ // Start with a move command to the first point
224
+ const path = [["M", this.points[0].x, this.points[0].y]];
225
+ // Create appropriate curve segments based on the number of points
226
+ if (this.points.length === 2) {
227
+ // With just 2 points, create a line
228
+ path.push(["L", this.points[1].x, this.points[1].y]);
229
+ } else if (this.points.length === 3) {
230
+ // With 3 points, create a quadratic curve (one control point)
231
+ path.push([
232
+ "Q",
233
+ this.points[1].x,
234
+ this.points[1].y,
235
+ this.points[2].x,
236
+ this.points[2].y,
237
+ ]);
238
+ } else {
239
+ // For 4+ points, create cubic Bezier segments
240
+ for (let i = 1; i < this.points.length; i += 3) {
241
+ if (i + 2 < this.points.length) {
242
+ // We have enough points for a cubic curve
243
+ path.push([
244
+ "C",
245
+ this.points[i].x,
246
+ this.points[i].y,
247
+ this.points[i + 1].x,
248
+ this.points[i + 1].y,
249
+ this.points[i + 2].x,
250
+ this.points[i + 2].y,
251
+ ]);
252
+ } else if (i + 1 < this.points.length) {
253
+ // Just enough for a quadratic curve
254
+ path.push([
255
+ "Q",
256
+ this.points[i].x,
257
+ this.points[i].y,
258
+ this.points[i + 1].x,
259
+ this.points[i + 1].y,
260
+ ]);
261
+ } else {
262
+ // Just add a line to the last point
263
+ path.push(["L", this.points[i].x, this.points[i].y]);
264
+ }
265
+ }
266
+ }
267
+
268
+ this.bezierPath = path;
269
+
270
+ // If we already have a shape, update its path
271
+ if (this.bezierShape) {
272
+ this.bezierShape.shape.path = path;
273
+ this.bezierShape.render();
274
+ } else {
275
+ const centerX = 0;
276
+ const centerY = 0;
277
+ // Create the bezier shape
278
+ const bezierShapeObj = new BezierShape(path, {
279
+ color: Painter.colors.randomColorHSL(),
280
+ stroke: "rgba(255, 255, 255, 0.8)",
281
+ lineWidth: 3,
282
+ });
283
+
284
+ // Create a GameObject using the factory
285
+ this.bezierShape = ShapeGOFactory.create(this.game, bezierShapeObj, {
286
+ x: centerX,
287
+ y: centerY,
288
+ });
289
+
290
+ // Add the GameObject to the scene
291
+ this.add(this.bezierShape);
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Cut the current bezier shape and store it with jitter animation
297
+ */
298
+ cutShape() {
299
+ // Only proceed if we have a valid bezier curve
300
+ if (this.points.length >= 2 && this.bezierShape) {
301
+ // Create a deep copy of the current bezier data
302
+ const cutBezier = {
303
+ points: [...this.points.map((p) => ({ ...p }))],
304
+ path: [...this.bezierPath.map((cmd) => [...cmd])],
305
+ shape: this.bezierShape,
306
+ originalPath: [...this.bezierPath.map((cmd) => [...cmd])], // Store original path for animation
307
+ jitterAmount: 5, // Base jitter amount
308
+ jitterSpeed: 1.5 + Math.random() * 0.5, // Random speed variation
309
+ jitterPhase: Math.random() * Math.PI * 2, // Random starting phase
310
+ };
311
+
312
+ cutBezier.jitterAmount = 5 * cutBezier.points.length;
313
+
314
+ // Add to our collection of user beziers
315
+ this.userBeziers.push(cutBezier);
316
+
317
+ // Remove the current bezier shape from control but keep it in the scene
318
+ this.bezierShape = null;
319
+
320
+ // Clear current editing points but keep the shape visible
321
+ this.points = [];
322
+ this.bezierPath = [];
323
+
324
+ // Remove visual control points
325
+ for (const pointShape of this.controlPointShapes) {
326
+ this.remove(pointShape);
327
+ }
328
+ this.controlPointShapes = [];
329
+ this.draggedPointIndex = -1;
330
+
331
+ //console.log(`Cut bezier shape - ${this.userBeziers.length} total shapes`);
332
+ } else {
333
+ //console.log("Need at least 2 points to cut a shape");
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Clear all points and reset the demo
339
+ * Updated to also clear all cut bezier shapes
340
+ */
341
+ clear() {
342
+ // Remove visual elements
343
+ for (const shape of this.controlPointShapes) {
344
+ this.remove(shape);
345
+ }
346
+
347
+ // Remove the current bezier shape if exists
348
+ if (this.bezierShape) {
349
+ this.remove(this.bezierShape);
350
+ this.bezierShape = null;
351
+ }
352
+
353
+ // Additionally remove all cut bezier shapes
354
+ for (const bezier of this.userBeziers) {
355
+ this.remove(bezier.shape);
356
+ }
357
+
358
+ // Reset all data
359
+ this.points = [];
360
+ this.bezierPath = [];
361
+ this.controlPointShapes = [];
362
+ this.draggedPointIndex = -1;
363
+ this.userBeziers = [];
364
+ }
365
+
366
+ /**
367
+ * Return the bezier path as a string for display/export
368
+ */
369
+ getPathString() {
370
+ if (!this.bezierPath.length) return "No path defined";
371
+
372
+ return JSON.stringify(this.bezierPath);
373
+ }
374
+
375
+ /**
376
+ * Find the index of a control point near the given coordinates
377
+ */
378
+ findNearbyPoint(x, y, radius = 20) {
379
+ for (let i = 0; i < this.points.length; i++) {
380
+ const point = this.points[i];
381
+ const dx = point.x - x;
382
+ const dy = point.y - y;
383
+ const distance = Math.sqrt(dx * dx + dy * dy);
384
+
385
+ if (distance <= radius) {
386
+ return i;
387
+ }
388
+ }
389
+ return -1;
390
+ }
391
+
392
+ /**
393
+ * Check if a point is within the UI area
394
+ */
395
+ isInUIArea(x, y) {
396
+ // Get screen coordinates (input coords are already screen coords)
397
+ const screenY = y + this.height / 2;
398
+ // UI is at bottom, check if click is in bottom 80px
399
+ return screenY > this.game.height - 80;
400
+ }
401
+
402
+ /**
403
+ * Pointer down handler
404
+ */
405
+ pointerDown(x, y) {
406
+ // Skip if clicking on UI area (buttons)
407
+ if (this.game.canvas.style.cursor === "pointer" || this.isInUIArea(x, y)) {
408
+ return;
409
+ }
410
+ if (this.mode === "add") {
411
+ // In add mode, place a new control point
412
+ this.addPoint(x, y);
413
+ } else {
414
+ // In edit mode, check if we're clicking on an existing point
415
+ const pointIndex = this.findNearbyPoint(x - 50, y - 50);
416
+ if (pointIndex >= 0) {
417
+ this.draggedPointIndex = pointIndex;
418
+ }
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Pointer move handler
424
+ */
425
+ pointerMove(x, y) {
426
+ if (this.draggedPointIndex >= 0) {
427
+ // Update the dragged point position
428
+ this.points[this.draggedPointIndex].x = x - 50;
429
+ this.points[this.draggedPointIndex].y = y - 50;
430
+
431
+ // Update the visual control point
432
+ const pointShape = this.controlPointShapes[this.draggedPointIndex];
433
+ pointShape.x = x - 50;
434
+ pointShape.y = y - 50;
435
+
436
+ // Update the Bezier curve
437
+ this.updateBezierPath();
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Pointer up handler
443
+ */
444
+ pointerUp() {
445
+ this.draggedPointIndex = -1;
446
+ }
447
+
448
+ /**
449
+ * Render additional visual helpers
450
+ */
451
+ draw() {
452
+ super.draw();
453
+ // If we have at least 2 points, draw guide lines between control points
454
+ if (this.points.length >= 2) {
455
+ Painter.save();
456
+ // Draw dashed lines connecting control points
457
+ Painter.colors.setStrokeColor("rgba(0, 255, 0, 0.8)");
458
+ Painter.lines.setLineWidth(1);
459
+ // Set up a dashed line style
460
+ Painter.ctx.setLineDash([5, 5]);
461
+ Painter.lines.beginPath();
462
+ Painter.lines.moveTo(this.points[0].x, this.points[0].y);
463
+
464
+ for (let i = 1; i < this.points.length; i++) {
465
+ Painter.lines.lineTo(this.points[i].x, this.points[i].y);
466
+ }
467
+
468
+ Painter.colors.stroke();
469
+ Painter.ctx.setLineDash([]); // Reset dash
470
+ Painter.restore();
471
+ }
472
+ }
473
+
474
+ static gco = [
475
+ "source-over",
476
+ "multiply",
477
+ "screen",
478
+ "overlay",
479
+ "darken",
480
+ "lighten",
481
+ "color-dodge",
482
+ "color-burn",
483
+ "hard-light",
484
+ "soft-light",
485
+ "difference",
486
+ "exclusion",
487
+ "hue",
488
+ "saturation",
489
+ "color",
490
+ "luminosity",
491
+ ];
492
+
493
+ #prevWidth = 0;
494
+ #prevHeight = 0;
495
+
496
+ /**
497
+ * Update method that adds the jitter animation to cut bezier shapes
498
+ */
499
+ update(dt) {
500
+ // Update scene dimensions based on margin
501
+ this.width = this.game.width - this.MARGIN * 2;
502
+ this.height = this.game.height - this.MARGIN * 2;
503
+
504
+ // Center the scene in the game
505
+ this.x = this.game.width / 2;
506
+ this.y = this.game.height / 2;
507
+ const filter = Painter.ctx.globalCompositeOperation;
508
+ Painter.ctx.globalCompositeOperation = "screen";
509
+ // Update jitter animation for all cut beziers
510
+ for (const bezier of this.userBeziers) {
511
+ // Update the jitter phase
512
+ bezier.jitterPhase += dt * bezier.jitterSpeed * 5;
513
+
514
+ // Create a new jittered path based on the original
515
+ const jitteredPath = [];
516
+ for (
517
+ let cmdIndex = 0;
518
+ cmdIndex < bezier.originalPath.length;
519
+ cmdIndex++
520
+ ) {
521
+ const originalCmd = bezier.originalPath[cmdIndex];
522
+ const newCmd = [...originalCmd]; // Make a copy
523
+
524
+ // Only modify coordinate values (not the command type at index 0)
525
+ for (let i = 1; i < newCmd.length; i++) {
526
+ if (typeof newCmd[i] === "number") {
527
+ // Apply a sine wave jitter with unique offset for each point
528
+ const offset =
529
+ Math.sin(bezier.jitterPhase + i * 0.3 + cmdIndex * 0.7) *
530
+ bezier.jitterAmount;
531
+ newCmd[i] = originalCmd[i] + offset;
532
+ }
533
+ }
534
+ jitteredPath.push(newCmd);
535
+ }
536
+
537
+ // Apply the jittered path to the shape
538
+ bezier.shape.shape.path = jitteredPath;
539
+ }
540
+ //Painter.effects.setBlendMode(filter);
541
+ super.update(dt);
542
+
543
+ if (this.#prevWidth !== this.width || this.#prevHeight !== this.height) {
544
+ this.markBoundsDirty();
545
+ }
546
+ this.#prevWidth = this.width;
547
+ this.#prevHeight = this.height;
548
+ }
549
+ }
550
+
551
+ /**
552
+ * UI for the Bezier Demo with controls for adding/editing points,
553
+ * clearing the canvas, and switching between modes.
554
+ */
555
+ class BezierUIScene extends Scene {
556
+ constructor(game, bezierScene, options = {}) {
557
+ super(game, options);
558
+ this.bezierScene = bezierScene;
559
+ this.onMenu = false;
560
+ this.currentMode = null;
561
+ this._lastMobileState = null;
562
+ }
563
+
564
+ /**
565
+ * Get button style configuration (transparent background)
566
+ */
567
+ getButtonStyle() {
568
+ return {
569
+ colorHoverBg: "transparent",
570
+ colorDefaultBg: "transparent",
571
+ colorPressedBg: "transparent",
572
+ colorDefaultText: "white",
573
+ };
574
+ }
575
+
576
+ init() {
577
+ this.createButtons();
578
+ }
579
+
580
+ /**
581
+ * Create buttons with current responsive config
582
+ */
583
+ createButtons() {
584
+ // Clear existing layout
585
+ if (this.layout) {
586
+ this.remove(this.layout);
587
+ this.layout = null;
588
+ }
589
+
590
+ const config = this.game.getResponsiveConfig();
591
+ const buttonStyle = this.getButtonStyle();
592
+
593
+ this.layout = new HorizontalLayout(this.game, {
594
+ width: config.layoutWidth,
595
+ height: config.layoutHeight,
596
+ spacing: 5,
597
+ });
598
+
599
+ this.addModeButton = this.layout.add(
600
+ new ToggleButton(this.game, {
601
+ text: config.addLabel,
602
+ width: config.buttonWidth,
603
+ height: 32,
604
+ ...buttonStyle,
605
+ startToggled: true,
606
+ onToggle: (active) => {
607
+ if (this.currentMode && this.currentMode !== this.addModeButton) {
608
+ this.currentMode.toggle(false);
609
+ }
610
+ if (active) {
611
+ this.bezierScene.setMode("add");
612
+ this.currentMode = this.addModeButton;
613
+ }
614
+ },
615
+ })
616
+ );
617
+
618
+ this.editModeButton = this.layout.add(
619
+ new ToggleButton(this.game, {
620
+ text: config.editLabel,
621
+ width: config.buttonWidth,
622
+ height: 32,
623
+ ...buttonStyle,
624
+ onToggle: (active) => {
625
+ if (this.currentMode && this.currentMode !== this.editModeButton) {
626
+ this.currentMode.toggle(false);
627
+ }
628
+ if (active) {
629
+ this.bezierScene.setMode("edit");
630
+ this.currentMode = this.editModeButton;
631
+ }
632
+ },
633
+ })
634
+ );
635
+
636
+ this.cutModeButton = this.layout.add(
637
+ new Button(this.game, {
638
+ text: config.cutLabel,
639
+ width: config.buttonWidth,
640
+ height: 32,
641
+ ...buttonStyle,
642
+ onClick: () => {
643
+ if (this.editModeButton) this.editModeButton.toggle(false);
644
+ if (this.addModeButton) this.addModeButton.toggle(false);
645
+ this.currentMode = null;
646
+ this.bezierScene.cutShape();
647
+ },
648
+ })
649
+ );
650
+
651
+ this.clearButton = this.layout.add(
652
+ new Button(this.game, {
653
+ text: config.clearLabel,
654
+ width: config.clearWidth,
655
+ height: 32,
656
+ ...buttonStyle,
657
+ onClick: () => {
658
+ this.bezierScene.clear();
659
+ },
660
+ })
661
+ );
662
+
663
+ this.add(this.layout);
664
+ this.currentMode = this.addModeButton;
665
+
666
+ // Update scene dimensions
667
+ this.width = config.layoutWidth;
668
+ this.height = config.layoutHeight;
669
+ this.markBoundsDirty();
670
+ }
671
+
672
+ /**
673
+ * Handle resize - recreate buttons if mobile state changed
674
+ */
675
+ onResize() {
676
+ const isMobile = this.game.isMobile();
677
+
678
+ // Only recreate if mobile state changed
679
+ if (this._lastMobileState !== isMobile) {
680
+ this._lastMobileState = isMobile;
681
+ this.createButtons();
682
+ }
683
+
684
+ // Update scene dimensions
685
+ const config = this.game.getResponsiveConfig();
686
+ this.width = config.layoutWidth;
687
+ this.height = config.layoutHeight;
688
+ this.markBoundsDirty();
689
+ }
690
+ }
691
+ /// Main game class
692
+ window.BezierDemoGame = BezierDemoGame;