@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,484 @@
1
+ import {
2
+ Game,
3
+ Scene,
4
+ FPSCounter,
5
+ Button,
6
+ HorizontalLayout,
7
+ Painter,
8
+ ToggleButton,
9
+ Cursor,
10
+ TextShape,
11
+ Circle,
12
+ GameObject,
13
+ Position,
14
+ } from "../../src/index";
15
+
16
+ /**
17
+ * The PaintScene:
18
+ * - Holds an array of "strokes" (either lines or freehand paths).
19
+ * - Each stroke is a plain JS object with fields: type, points, color, etc.
20
+ */
21
+ class PaintScene extends GameObject {
22
+ constructor(game, options = {}) {
23
+ super(game, options);
24
+ this.MARGIN = 0;
25
+ this.currentTool = "pencil"; // 'line', 'pencil', or 'eraser'
26
+ this.lineStart = null; // used for line tool
27
+ this.activeStroke = null; // stroke being drawn right now
28
+ this.strokes = []; // all finalized strokes
29
+ this.pencilCursor = new TextShape("✏️", {
30
+ font: "30px monospace",
31
+ color: "green",
32
+ });
33
+ this.eraserCursor = new TextShape(" 🦯", { font: "30px monospace" });
34
+ this.lineCursor = new TextShape("✒️", { font: "30px monospace" });
35
+ this.lineCursorP = new Circle(5, { color: "red" });
36
+ this.cursor = new Cursor(game, this.pencilCursor, this.pencilCursor, {
37
+ x: 0,
38
+ y: 0,
39
+ });
40
+ this.cursor.offsetX = -10;
41
+ this.cursor.offsetY = -25;
42
+ //
43
+ this.totalPoints = 0; // Track total points across all strokes
44
+ this.MAX_POINTS = 10000; // Maximum number of points to keep
45
+ }
46
+
47
+ /**
48
+ * Set the current tool to use.
49
+ */
50
+ setTool(tool) {
51
+ this.currentTool = tool;
52
+ this.lineStart = null;
53
+ this.activeStroke = null;
54
+ if (tool === "eraser") {
55
+ this.cursor.normalShape = this.cursor.pressedShape = this.eraserCursor;
56
+ // this.cursor.offsetX = 10;
57
+ this.cursor.offsetY = -25;
58
+ } else if (tool === "line") {
59
+ this.cursor.normalShape = this.cursor.pressedShape = this.lineCursor;
60
+ this.cursor.offsetX = 12;
61
+ this.cursor.offsetY = -25;
62
+ } else {
63
+ this.cursor.normalShape = this.cursor.pressedShape = this.pencilCursor;
64
+ this.cursor.offsetX = -10;
65
+ this.cursor.offsetY = -25;
66
+ }
67
+ }
68
+
69
+ #prevWidth = 0;
70
+ #prevHeight = 0;
71
+
72
+ update(dt) {
73
+ // Update scene dimensions based on margin
74
+ this.width = this.game.width - this.MARGIN * 2;
75
+ this.height = this.game.height - this.MARGIN * 2;
76
+ // Center the scene in the game
77
+ this.x = this.game.width / 2;
78
+ this.y = this.game.height / 2;
79
+ super.update(dt);
80
+ if (this.#prevWidth !== this.width || this.#prevHeight !== this.height) {
81
+ this.markBoundsDirty();
82
+ }
83
+ this.#prevWidth = this.width;
84
+ this.#prevHeight = this.height;
85
+ }
86
+
87
+ /**
88
+ * This is called every frame after update() to draw everything.
89
+ */
90
+ draw() {
91
+ super.draw();
92
+ this.logger.log("PaintScene.draw", Painter.ctx.fillStyle);
93
+ // Draw each finalized stroke
94
+ for (let s of this.strokes) {
95
+ this.drawStroke(s, false); // false = not active
96
+ }
97
+
98
+ // Draw the active stroke differently
99
+ if (this.activeStroke) {
100
+ this.drawStroke(this.activeStroke, true); // true = active
101
+ }
102
+
103
+ // Draw line preview if needed
104
+ if (this.lineStart && this.currentTool === "line") {
105
+ this.drawLinePreview(this.lineStart, this.currentMousePos);
106
+ }
107
+ }
108
+
109
+ static gco = [
110
+ "source-over",
111
+ "multiply",
112
+ "screen",
113
+ "overlay",
114
+ "darken",
115
+ "lighten",
116
+ "color-dodge",
117
+ "color-burn",
118
+ "hard-light",
119
+ "soft-light",
120
+ "difference",
121
+ "exclusion",
122
+ "hue",
123
+ "saturation",
124
+ "color",
125
+ "luminosity",
126
+ ];
127
+
128
+ drawStroke(stroke, isActive = false) {
129
+ // Common setup
130
+ Painter.ctx.save();
131
+ Painter.ctx.globalAlpha = 1;
132
+
133
+ // Set line properties explicitly
134
+ Painter.ctx.lineWidth = stroke.lineWidth || 2;
135
+ Painter.ctx.strokeStyle = stroke.color || "#fff";
136
+
137
+ // IMPORTANT: Different handling for active strokes
138
+ if (isActive) {
139
+ // For active strokes, ensure we're using regular luminosity compositing
140
+ Painter.ctx.globalCompositeOperation =
141
+ stroke.compositeOp || "source-over";
142
+ } else {
143
+ // For completed strokes, use the specified composite operation
144
+ Painter.ctx.globalCompositeOperation =
145
+ stroke.compositeOp || "source-over";
146
+ }
147
+
148
+ // Begin the path
149
+ Painter.ctx.beginPath();
150
+
151
+ const pts = stroke.points;
152
+ if (!pts || pts.length < 2) {
153
+ Painter.ctx.restore();
154
+ return;
155
+ }
156
+ // Start at the first point
157
+ Painter.ctx.moveTo(pts[0].x, pts[0].y);
158
+ //Apply effects
159
+ if (stroke.type === "eraser") {
160
+ Painter.ctx.globalAlpha = 1;
161
+ Painter.ctx.fillStyle = "black";
162
+ Painter.ctx.fill();
163
+ } else {
164
+ Painter.ctx.fillStyle = "transparent";
165
+ Painter.ctx.globalAlpha = Math.random();
166
+ Painter.ctx.globalCompositeOperation =
167
+ PaintScene.gco[Math.floor(Math.random() * PaintScene.gco.length)];
168
+ }
169
+ // Draw lines to each point
170
+ for (let i = 1; i < pts.length; i++) {
171
+ Painter.ctx.lineTo(pts[i].x, pts[i].y);
172
+ }
173
+ Painter.ctx.stroke();
174
+ // Restore the context
175
+ Painter.ctx.restore();
176
+ Painter.ctx.globalAlpha = 1;
177
+ }
178
+
179
+ /**
180
+ * Draw a preview line from start point to current mouse position
181
+ */
182
+ drawLinePreview(start, end) {
183
+ Painter.ctx.save();
184
+
185
+ // Use a different style for the preview
186
+ Painter.ctx.lineWidth = 2;
187
+ Painter.ctx.strokeStyle = "#FFFFFF"; // Lighter color for preview
188
+ Painter.ctx.setLineDash([5, 3]); // Dashed line for preview
189
+
190
+ Painter.ctx.beginPath();
191
+ Painter.ctx.moveTo(start.x, start.y);
192
+ Painter.ctx.lineTo(end.x, end.y);
193
+ Painter.ctx.stroke();
194
+
195
+ // Draw small circles at the start and end points
196
+ Painter.ctx.fillStyle = "#fff";
197
+ Painter.ctx.beginPath();
198
+ Painter.ctx.arc(start.x, start.y, 4, 0, Math.PI * 2);
199
+ Painter.ctx.fill();
200
+
201
+ Painter.ctx.beginPath();
202
+ Painter.ctx.arc(end.x, end.y, 4, 0, Math.PI * 2);
203
+ Painter.ctx.fill();
204
+
205
+ Painter.ctx.restore();
206
+ }
207
+
208
+ /**
209
+ * The scene can handle pointer events directly.
210
+ * We'll do "line" logic vs "pencil"/"eraser" logic.
211
+ */
212
+ pointerDown(x, y) {
213
+ this.activeStroke = null;
214
+ this.game.uiScene.visible = false;
215
+ this.game.uiScene.active = false;
216
+ // If user has chosen "line":
217
+ if (this.currentTool === "line") {
218
+ if (this.lineStart == null) {
219
+ // first click: store start
220
+ this.lineStart = { x, y };
221
+ this.currentMousePos = { x, y }; // Initialize current mouse position
222
+ } else {
223
+ // second click: finalize line stroke
224
+ const stroke = {
225
+ type: "line",
226
+ lineWidth: 4,
227
+ color: Painter.colors.randomColorHSL(),
228
+ compositeOp: "luminosity",
229
+ points: [
230
+ { x: this.lineStart.x, y: this.lineStart.y },
231
+ { x, y },
232
+ ],
233
+ };
234
+ this.strokes.push(stroke);
235
+ this.lineStart = null; // Reset line start for next line
236
+ }
237
+ return;
238
+ }
239
+
240
+ // If "pencil" or "eraser", start a new active stroke
241
+ const erasing = this.currentTool === "eraser";
242
+ this.activeStroke = {
243
+ type: erasing ? "eraser" : "pencil",
244
+ lineWidth: erasing ? 50 : 8, // eraser is wider
245
+ color: erasing ? "#000" : Painter.colors.randomColorHSL(), // color doesn't matter for eraser, but we'll use #000
246
+ compositeOp: erasing ? "destination-out" : "source-over",
247
+ points: [{ x, y }],
248
+ };
249
+ }
250
+
251
+ pointerMove(x, y) {
252
+ // Always update current mouse position for preview
253
+ this.currentMousePos = { x, y };
254
+
255
+ // If we're currently drawing a stroke (pencil/eraser),
256
+ // add the new point.
257
+ if (this.activeStroke) {
258
+ this.activeStroke.points.push({ x, y });
259
+ }
260
+ }
261
+
262
+ pointerUp(x, y) {
263
+ this.game.uiScene.visible = true;
264
+ this.game.uiScene.active = true;
265
+ // If we have an active stroke, finalize it
266
+ if (this.activeStroke) {
267
+ // Make sure it has at least 2 points
268
+ if (this.activeStroke.points.length < 2) {
269
+ // Add the current point if needed
270
+ this.activeStroke.points.push({ x, y });
271
+ }
272
+
273
+ // Make a fresh copy to avoid reference issues
274
+ const finalStroke = {
275
+ type: this.activeStroke.type,
276
+ lineWidth: this.activeStroke.lineWidth,
277
+ color: this.activeStroke.color,
278
+ compositeOp: this.activeStroke.compositeOp,
279
+ points: this.activeStroke.points.map((p) => ({ ...p })), // Deep copy points
280
+ };
281
+
282
+ this.strokes.push(finalStroke);
283
+ this.activeStroke = null;
284
+ }
285
+ this.enforceStrokeLimit();
286
+ }
287
+
288
+ enforceStrokeLimit() {
289
+ // First check total point count
290
+ this.totalPoints = this.strokes.reduce(
291
+ (sum, stroke) => sum + stroke.points.length,
292
+ 0
293
+ );
294
+
295
+ // If too many points, start removing oldest strokes
296
+ while (this.totalPoints > this.MAX_POINTS && this.strokes.length > 0) {
297
+ const removedStroke = this.strokes.shift(); // Remove oldest
298
+ this.totalPoints -= removedStroke.points.length;
299
+ }
300
+
301
+ // Also enforce max stroke count
302
+ if (this.strokes.length > this.MAX_STROKES) {
303
+ // Get points in strokes to be removed
304
+ const pointsToRemove = this.strokes
305
+ .slice(0, this.REMOVE_BATCH)
306
+ .reduce((sum, stroke) => sum + stroke.points.length, 0);
307
+
308
+ // Remove the oldest REMOVE_BATCH strokes
309
+ this.strokes = this.strokes.slice(this.REMOVE_BATCH);
310
+ this.totalPoints -= pointsToRemove;
311
+
312
+ console.log(
313
+ `Removed ${this.REMOVE_BATCH} oldest strokes. ${this.strokes.length} strokes remaining.`
314
+ );
315
+ }
316
+ }
317
+ }
318
+
319
+ /**
320
+ * A small UI scene that has 3 buttons in a horizontal layout:
321
+ * "Line", "Pencil", "Eraser."
322
+ * When clicked, they call paintScene.setTool(...).
323
+ */
324
+ class UIScene extends Scene {
325
+ constructor(game, paintScene, options = {}) {
326
+ super(game, options);
327
+ this.debug = true;
328
+ this.debugColor = "yellow";
329
+ this.paintScene = paintScene;
330
+ this.layout = new HorizontalLayout(game, {
331
+ x: 0,
332
+ y: 0,
333
+ spacing: 8,
334
+ padding: 0,
335
+ });
336
+ this.toolPencil = this.layout.add(
337
+ new ToggleButton(game, {
338
+ text: "✏️Pencil",
339
+ width: 80,
340
+ height: 32,
341
+ colorHoverBg: "transparent",
342
+ colorDefaultBg: "transparent",
343
+ colorPressedBg: "transparent",
344
+ colorDefaultText: "white",
345
+ startToggled: true,
346
+ onToggle: (active) => {
347
+ if (currentTool) {
348
+ currentTool.toggle(false);
349
+ }
350
+ if (active) {
351
+ paintScene.setTool("pencil");
352
+ currentTool = this.toolPencil;
353
+ }
354
+ },
355
+ })
356
+ );
357
+ this.toolEraser = this.layout.add(
358
+ new ToggleButton(game, {
359
+ text: "🦯Eraser",
360
+ width: 80,
361
+ height: 32,
362
+ colorHoverBg: "transparent",
363
+ colorDefaultBg: "transparent",
364
+ colorPressedBg: "transparent",
365
+ colorDefaultText: "white",
366
+ onToggle: (active) => {
367
+ if (currentTool) {
368
+ currentTool.toggle(false);
369
+ }
370
+ if (active) {
371
+ paintScene.setTool("eraser");
372
+ currentTool = this.toolEraser;
373
+ }
374
+ },
375
+ })
376
+ );
377
+ this.toolLine = this.layout.add(
378
+ new ToggleButton(game, {
379
+ text: "✒️Line",
380
+ width: 80,
381
+ height: 32,
382
+ colorHoverBg: "transparent",
383
+ colorDefaultBg: "transparent",
384
+ colorPressedBg: "transparent",
385
+ colorDefaultText: "white",
386
+ onToggle: (active) => {
387
+ if (currentTool) {
388
+ currentTool.toggle(false);
389
+ }
390
+ if (active) {
391
+ currentTool = this.toolLine;
392
+ paintScene.setTool("line");
393
+ }
394
+ },
395
+ })
396
+ );
397
+ this.layout.add(
398
+ new Button(game, {
399
+ text: "🧼Clear",
400
+ height: 32,
401
+ width: 80,
402
+ colorHoverBg: "transparent",
403
+ colorDefaultBg: "transparent",
404
+ colorPressedBg: "transparent",
405
+ colorDefaultText: "white",
406
+ onClick: () => {
407
+ this.paintScene.strokes = [];
408
+ this.paintScene.activeStroke = null;
409
+ this.paintScene.lineStart = null;
410
+ },
411
+ })
412
+ );
413
+ let currentTool = this.toolPencil;
414
+ this.add(this.layout);
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Our main Game class.
420
+ * We create two Scenes: one for painting, one for UI.
421
+ * We forward pointer events to the paint scene.
422
+ */
423
+ class DemoGame extends Game {
424
+ constructor(canvas) {
425
+ super(canvas);
426
+ this.enableFluidSize();
427
+ this.backgroundColor = "black";
428
+ }
429
+
430
+ addFPSCounter() {
431
+ this.pipeline.add(
432
+ new FPSCounter(this, {
433
+ anchor: "bottom-right",
434
+ width: 20,
435
+ height: 20,
436
+ })
437
+ );
438
+ }
439
+
440
+ createUI() {
441
+ // Create the UI scene
442
+ this.uiScene = new UIScene(this, this.paintScene, {
443
+ debug: true,
444
+ debugColor: "yellow",
445
+ anchor: Position.BOTTOM_CENTER,
446
+ anchorRelative: this.paintScene,
447
+ width: 80 + 80 + 80 + 80 + 32,
448
+ height: 40,
449
+ });
450
+ this.pipeline.add(this.uiScene);
451
+ }
452
+
453
+ init() {
454
+ super.init();
455
+ // Create the paint scene
456
+ this.paintScene = new PaintScene(this, { debug: true, anchor: "center" });
457
+ this.pipeline.add(this.paintScene);
458
+ // Add them to the pipeline
459
+ this.createUI();
460
+ this.addFPSCounter();
461
+ this.cursor = this.paintScene.cursor;
462
+ // Listen for pointer events global ly and hand them to paintScene
463
+ this.events.on("inputdown", (e) => {
464
+ // translate canvas point to scene point (center is 0,0)
465
+ const x = e.x - this.width / 2;
466
+ const y = e.y - this.height / 2;
467
+ this.paintScene.pointerDown(x, y);
468
+ });
469
+ this.events.on("inputmove", (e) => {
470
+ // translate canvas point to scene point (center is 0,0)
471
+ const x = e.x - this.width / 2;
472
+ const y = e.y - this.height / 2;
473
+ this.paintScene.pointerMove(x, y);
474
+ });
475
+ this.events.on("inputup", (e) => {
476
+ // translate canvas point to scene point (center is 0,0)
477
+ const x = e.x - this.width / 2;
478
+ const y = e.y - this.height / 2;
479
+ this.paintScene.pointerUp(x, y);
480
+ });
481
+ }
482
+ }
483
+
484
+ export { DemoGame };