@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,249 @@
1
+ import { GameObject } from "../objects/go.js";
2
+ import { Rectangle, TextShape, Group } from "../../shapes/index.js";
3
+
4
+ /**
5
+ * Tooltip
6
+ *
7
+ * A GameObject that displays text near the cursor when shown.
8
+ * Supports multiline text with automatic word wrapping.
9
+ *
10
+ * Usage:
11
+ * const tooltip = new Tooltip(game, { ... });
12
+ * game.pipeline.add(tooltip);
13
+ *
14
+ * // On hover:
15
+ * tooltip.show("Hello world", e.x, e.y);
16
+ *
17
+ * // On mouse out:
18
+ * tooltip.hide();
19
+ */
20
+ export class Tooltip extends GameObject {
21
+ /**
22
+ * @param {Game} game - The main game instance.
23
+ * @param {object} [options={}] - Configuration options.
24
+ * @param {string} [options.font="12px monospace"] - Font for the text.
25
+ * @param {string} [options.textColor="#fff"] - Text color.
26
+ * @param {string} [options.bgColor="rgba(0,0,0,0.85)"] - Background color.
27
+ * @param {string} [options.borderColor="rgba(255,255,255,0.3)"] - Border color.
28
+ * @param {number} [options.padding=8] - Padding inside the tooltip.
29
+ * @param {number} [options.offsetX=15] - X offset from cursor.
30
+ * @param {number} [options.offsetY=15] - Y offset from cursor.
31
+ * @param {number} [options.maxWidth=300] - Maximum width before wrapping.
32
+ * @param {number} [options.lineHeight=1.4] - Line height multiplier.
33
+ */
34
+ constructor(game, options = {}) {
35
+ super(game, { ...options, zIndex: 9999 }); // Always on top
36
+
37
+ this.font = options.font || "12px monospace";
38
+ this.textColor = options.textColor || "#fff";
39
+ this.bgColor = options.bgColor || "rgba(0,0,0,0.85)";
40
+ this.borderColor = options.borderColor || "rgba(255,255,255,0.3)";
41
+ this.padding = options.padding ?? 8;
42
+ this.offsetX = options.offsetX ?? 15;
43
+ this.offsetY = options.offsetY ?? 15;
44
+ this.maxWidth = options.maxWidth ?? 300;
45
+ this.lineHeightMultiplier = options.lineHeight ?? 1.4;
46
+
47
+ // Current text and wrapped lines
48
+ this._text = "";
49
+ this._lines = [];
50
+ this._visible = false;
51
+
52
+ // Create background shape
53
+ this.bg = new Rectangle({
54
+ width: 100,
55
+ height: 30,
56
+ color: this.bgColor,
57
+ stroke: this.borderColor,
58
+ lineWidth: 1,
59
+ });
60
+
61
+ // Line shapes will be created dynamically
62
+ this.lineShapes = [];
63
+
64
+ this.group = new Group();
65
+ this.group.add(this.bg);
66
+
67
+ // Follow mouse
68
+ this.game.events.on("inputmove", (e) => {
69
+ if (this._visible) {
70
+ this.updatePosition(e.x, e.y);
71
+ }
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Wrap text into multiple lines based on maxWidth.
77
+ * @param {string} text - Text to wrap.
78
+ * @returns {string[]} Array of lines.
79
+ */
80
+ wrapText(text) {
81
+ const ctx = this.game.ctx;
82
+ ctx.font = this.font;
83
+
84
+ const lines = [];
85
+ // Split by explicit newlines first
86
+ const paragraphs = text.split("\n");
87
+
88
+ for (const paragraph of paragraphs) {
89
+ const words = paragraph.split(" ");
90
+ let currentLine = "";
91
+
92
+ for (const word of words) {
93
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
94
+ const metrics = ctx.measureText(testLine);
95
+
96
+ if (metrics.width > this.maxWidth && currentLine) {
97
+ lines.push(currentLine);
98
+ currentLine = word;
99
+ } else {
100
+ currentLine = testLine;
101
+ }
102
+ }
103
+
104
+ if (currentLine) {
105
+ lines.push(currentLine);
106
+ } else if (paragraph === "") {
107
+ lines.push(""); // Preserve empty lines
108
+ }
109
+ }
110
+
111
+ return lines;
112
+ }
113
+
114
+ /**
115
+ * Show the tooltip with the given text at the specified position.
116
+ * @param {string} text - Text to display (supports newlines and auto-wrapping).
117
+ * @param {number} [mouseX] - X position (defaults to current mouse).
118
+ * @param {number} [mouseY] - Y position (defaults to current mouse).
119
+ */
120
+ show(text, mouseX, mouseY) {
121
+ this._text = text;
122
+ this._visible = true;
123
+
124
+ // Wrap text into lines
125
+ this._lines = this.wrapText(text);
126
+
127
+ // Create/update line shapes
128
+ this.updateLineShapes();
129
+
130
+ // Measure text to size background
131
+ this.updateSize();
132
+
133
+ if (mouseX !== undefined && mouseY !== undefined) {
134
+ this.updatePosition(mouseX, mouseY);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Create or update TextShape objects for each line.
140
+ */
141
+ updateLineShapes() {
142
+ // Remove old line shapes from group
143
+ for (const shape of this.lineShapes) {
144
+ this.group.remove(shape);
145
+ }
146
+
147
+ // Create new line shapes
148
+ this.lineShapes = this._lines.map(
149
+ (line) =>
150
+ new TextShape(line, {
151
+ font: this.font,
152
+ color: this.textColor,
153
+ align: "left",
154
+ baseline: "top",
155
+ })
156
+ );
157
+
158
+ // Add to group
159
+ for (const shape of this.lineShapes) {
160
+ this.group.add(shape);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Hide the tooltip.
166
+ */
167
+ hide() {
168
+ this._visible = false;
169
+ }
170
+
171
+ /**
172
+ * Update the tooltip position, keeping it on screen.
173
+ * Positions tooltip so its top-left is at cursor + offset.
174
+ * @param {number} mouseX - Mouse X position.
175
+ * @param {number} mouseY - Mouse Y position.
176
+ */
177
+ updatePosition(mouseX, mouseY) {
178
+ const width = this.bg.width;
179
+ const height = this.bg.height;
180
+
181
+ // Position so top-left corner is at cursor + offset
182
+ // (since tooltip renders centered, add half width/height)
183
+ let x = mouseX + this.offsetX + width / 2;
184
+ let y = mouseY + this.offsetY + height / 2;
185
+
186
+ // Keep tooltip on screen - adjust if going off right edge
187
+ if (x + width / 2 > this.game.width) {
188
+ x = mouseX - this.offsetX - width / 2;
189
+ }
190
+
191
+ // Adjust if going off bottom edge
192
+ if (y + height / 2 > this.game.height) {
193
+ y = mouseY - this.offsetY - height / 2;
194
+ }
195
+
196
+ // Adjust if going off left edge
197
+ if (x - width / 2 < 0) {
198
+ x = width / 2 + 5;
199
+ }
200
+
201
+ // Adjust if going off top edge
202
+ if (y - height / 2 < 0) {
203
+ y = height / 2 + 5;
204
+ }
205
+
206
+ this.x = x;
207
+ this.y = y;
208
+ }
209
+
210
+ /**
211
+ * Update the background size based on wrapped text lines.
212
+ */
213
+ updateSize() {
214
+ const ctx = this.game.ctx;
215
+ ctx.font = this.font;
216
+
217
+ // Find widest line
218
+ let maxLineWidth = 0;
219
+ for (const line of this._lines) {
220
+ const metrics = ctx.measureText(line);
221
+ maxLineWidth = Math.max(maxLineWidth, metrics.width);
222
+ }
223
+
224
+ const textWidth = Math.min(maxLineWidth, this.maxWidth);
225
+ const fontSize = parseInt(this.font);
226
+ const lineHeight = fontSize * this.lineHeightMultiplier;
227
+ const textHeight = lineHeight * this._lines.length;
228
+
229
+ this.bg.width = textWidth + this.padding * 2;
230
+ this.bg.height = textHeight + this.padding * 2;
231
+
232
+ // Position each line inside bg
233
+ const startX = -this.bg.width / 2 + this.padding;
234
+ const startY = -this.bg.height / 2 + this.padding;
235
+
236
+ for (let i = 0; i < this.lineShapes.length; i++) {
237
+ this.lineShapes[i].x = startX;
238
+ this.lineShapes[i].y = startY + i * lineHeight;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Render the tooltip if visible.
244
+ */
245
+ draw() {
246
+ if (!this._visible) return;
247
+ this.group.render();
248
+ }
249
+ }
package/src/index.js ADDED
@@ -0,0 +1,25 @@
1
+ export * from "./util";
2
+ export * from "./math";
3
+ export * from "./logger";
4
+ export * from "./painter";
5
+ export * from "./shapes";
6
+ export * from "./io";
7
+ export * from "./game";
8
+ export * from "./motion";
9
+ export * from "./mixins";
10
+
11
+ // Fluent API
12
+ export * from "./fluent";
13
+ export * from "./sound";
14
+
15
+ // Collision detection
16
+ export * from "./collision";
17
+
18
+ // State management
19
+ export * from "./state";
20
+
21
+ // Particle system
22
+ export * from "./particle";
23
+
24
+ // WebGL (optional, for shader effects)
25
+ export * from "./webgl";
@@ -0,0 +1,20 @@
1
+ export class EventEmitter {
2
+ constructor() {
3
+ this.listeners = {};
4
+ }
5
+
6
+ on(type, callback) {
7
+ if (!this.listeners[type]) this.listeners[type] = [];
8
+ this.listeners[type].push(callback);
9
+ }
10
+
11
+ off(type, callback) {
12
+ if (!this.listeners[type]) return;
13
+ this.listeners[type] = this.listeners[type].filter((cb) => cb !== callback);
14
+ }
15
+
16
+ emit(type, payload) {
17
+ if (!this.listeners[type]) return;
18
+ this.listeners[type].forEach((cb) => cb(payload));
19
+ }
20
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @module io
3
+ * @description Input/Output handling system for user interactions and event management.
4
+ *
5
+ * This module provides a comprehensive set of classes for handling user input across
6
+ * different devices and input methods:
7
+ *
8
+ * - {@link EventEmitter}: Core event management system for subscribing to and triggering events
9
+ * - {@link Mouse}: Mouse input tracking and event normalization
10
+ * - {@link Keys}: Keyboard input with logical key mapping and state tracking
11
+ * - {@link Touch}: Touch input for mobile devices
12
+ * - {@link Input}: Unified input system that normalizes mouse and touch events
13
+ *
14
+ * The IO module serves as the intermediary between raw browser events and your game logic,
15
+ * providing consistent, normalized events regardless of input source.
16
+ *
17
+ * @example
18
+ * // Basic usage in a game class
19
+ * import { Game } from './core/game.js';
20
+ * import { Keys, Mouse } from './core/io';
21
+ *
22
+ * class MyGame extends Game {
23
+ * init() {
24
+ * super.init();
25
+ *
26
+ * // Listen for a specific key press
27
+ * this.events.on(Keys.SPACE, () => {
28
+ * this.player.jump();
29
+ * });
30
+ *
31
+ * // Check mouse position in update loop
32
+ * this.events.on("mousemove", () => {
33
+ * this.logger.log(`Mouse at ${Mouse.x}, ${Mouse.y}`);
34
+ * });
35
+ *
36
+ * // Listen for unified input events (works with both mouse and touch)
37
+ * this.events.on("inputdown", () => {
38
+ * this.player.shoot();
39
+ * });
40
+ * }
41
+ *
42
+ * update(dt) {
43
+ * super.update(dt);
44
+ *
45
+ * // Check if a key is currently held down
46
+ * if (Keys.isDown(Keys.RIGHT)) {
47
+ * this.player.moveRight(dt);
48
+ * }
49
+ * }
50
+ * }
51
+ *
52
+ * @example
53
+ * // Creating your own custom event system
54
+ * import { EventEmitter } from './core/io';
55
+ *
56
+ * class WeaponSystem {
57
+ * constructor() {
58
+ * this.events = new EventEmitter();
59
+ * this.ammo = 10;
60
+ * }
61
+ *
62
+ * fire() {
63
+ * if (this.ammo > 0) {
64
+ * this.ammo--;
65
+ * this.events.emit('fired', { ammo: this.ammo });
66
+ *
67
+ * if (this.ammo === 0) {
68
+ * this.events.emit('empty');
69
+ * }
70
+ * }
71
+ * }
72
+ * }
73
+ *
74
+ * const weapon = new WeaponSystem();
75
+ * weapon.events.on('fired', (data) => {
76
+ * this.logger.log(`Fired! Ammo remaining: ${data.ammo}`);
77
+ * });
78
+ * weapon.events.on('empty', () => {
79
+ * this.logger.log('Out of ammo! Reload!');
80
+ * });
81
+ */
82
+ export {EventEmitter} from "./events.js";
83
+ export {Input} from "./input.js";
84
+ export {Mouse} from "./mouse.js";
85
+ export {Keys} from "./keys.js";
86
+ export {Touch} from "./touch.js";
@@ -0,0 +1,70 @@
1
+ export class Input {
2
+ static init(game) {
3
+ Input.game = game;
4
+ Input.x = 0;
5
+ Input.y = 0;
6
+ Input.down = false;
7
+ game.events.on("mousedown", Input._onDown);
8
+ game.events.on("mouseup", Input._onUp);
9
+ game.events.on("mousemove", Input._onMove);
10
+ game.events.on("touchstart", Input._onTouchStart);
11
+ game.events.on("touchend", Input._onTouchEnd);
12
+ game.events.on("touchmove", Input._onTouchMove);
13
+ }
14
+
15
+ static _setPosition(x, y) {
16
+ Input.x = x;
17
+ Input.y = y;
18
+ }
19
+
20
+ static _onDown = (e) => {
21
+ Input.down = true;
22
+ Input._setPosition(e.offsetX, e.offsetY);
23
+ Object.defineProperty(e, "x", { value: e.offsetX, configurable: true });
24
+ Object.defineProperty(e, "y", { value: e.offsetY, configurable: true });
25
+ Input.game.events.emit("inputdown", e);
26
+ };
27
+
28
+ static _onUp = (e) => {
29
+ Input.down = false;
30
+ Input._setPosition(e.offsetX, e.offsetY);
31
+ Object.defineProperty(e, "x", { value: e.offsetX, configurable: true });
32
+ Object.defineProperty(e, "y", { value: e.offsetY, configurable: true });
33
+ Input.game.events.emit("inputup", e);
34
+ };
35
+
36
+ static _onMove = (e) => {
37
+ Input._setPosition(e.offsetX, e.offsetY);
38
+ Object.defineProperty(e, "x", { value: e.offsetX, configurable: true });
39
+ Object.defineProperty(e, "y", { value: e.offsetY, configurable: true });
40
+ Input.game.events.emit("inputmove", e);
41
+ };
42
+
43
+ static _onTouchStart = (e) => {
44
+ const touch = e.touches[0];
45
+ const rect = Input.game.canvas.getBoundingClientRect();
46
+ Input.down = true;
47
+ const x = touch.clientX - rect.left;
48
+ const y = touch.clientY - rect.top;
49
+ Input._setPosition(x, y);
50
+ Object.defineProperty(e, "x", { value: x, configurable: true });
51
+ Object.defineProperty(e, "y", { value: y, configurable: true });
52
+ Input.game.events.emit("inputdown", e);
53
+ };
54
+
55
+ static _onTouchEnd = (e) => {
56
+ Input.down = false;
57
+ Input.game.events.emit("inputup", e);
58
+ };
59
+
60
+ static _onTouchMove = (e) => {
61
+ const touch = e.touches[0];
62
+ const rect = Input.game.canvas.getBoundingClientRect();
63
+ const x = touch.clientX - rect.left;
64
+ const y = touch.clientY - rect.top;
65
+ Input._setPosition(x, y);
66
+ Object.defineProperty(e, "x", { value: x, configurable: true });
67
+ Object.defineProperty(e, "y", { value: y, configurable: true });
68
+ Input.game.events.emit("inputmove", e);
69
+ };
70
+ }
package/src/io/keys.js ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Keys.js
3
+ *
4
+ * Provides an abstraction layer over keyboard input. Instead of dealing with
5
+ * raw "KeyA" / "Space" codes, you can subscribe to logical names like Keys.W,
6
+ * Keys.SPACE, Keys.LEFT, etc.
7
+ *
8
+ * Example usage:
9
+ * // Initialization (usually in your Game constructor):
10
+ * Keys.init(this);
11
+ *
12
+ * // Listen for a press:
13
+ * this.events.on(Keys.W, (evt) => {
14
+ * this.logger.log("W pressed!", evt);
15
+ * });
16
+ *
17
+ * // Listen for release:
18
+ * this.events.on(Keys.W + "_up", (evt) => {
19
+ * this.logger.log("W released!");
20
+ * });
21
+ *
22
+ * // Check if pressed in your update(dt):
23
+ * if (Keys.isDown(Keys.SPACE)) {
24
+ * // Spacebar is currently held
25
+ * }
26
+ */
27
+ export class Keys {
28
+ // Named constants for common game keys you might use:
29
+ static W = "W";
30
+ static A = "A";
31
+ static S = "S";
32
+ static D = "D";
33
+ static Q = "Q";
34
+ static E = "E";
35
+ static UP = "UP";
36
+ static DOWN = "DOWN";
37
+ static LEFT = "LEFT";
38
+ static RIGHT = "RIGHT";
39
+ static SPACE = "SPACE";
40
+ static SHIFT = "SHIFT";
41
+ static ENTER = "ENTER";
42
+ static ESC = "ESC";
43
+
44
+ /**
45
+ * Mapping from DOM event.code to one of the above Keys.* constants.
46
+ * Customize this list as needed for your game.
47
+ */
48
+ static _codeMap = {
49
+ // WASD + QE
50
+ KeyW: Keys.W,
51
+ KeyA: Keys.A,
52
+ KeyS: Keys.S,
53
+ KeyD: Keys.D,
54
+ KeyQ: Keys.Q,
55
+ KeyE: Keys.E,
56
+
57
+ // Arrows
58
+ ArrowUp: Keys.UP,
59
+ ArrowDown: Keys.DOWN,
60
+ ArrowLeft: Keys.LEFT,
61
+ ArrowRight: Keys.RIGHT,
62
+
63
+ // Space, Shift, Enter, Esc
64
+ Space: Keys.SPACE,
65
+ ShiftLeft: Keys.SHIFT,
66
+ ShiftRight: Keys.SHIFT,
67
+ Enter: Keys.ENTER,
68
+ NumpadEnter: Keys.ENTER,
69
+ Escape: Keys.ESC,
70
+ };
71
+
72
+ /**
73
+ * A Set of logical key names (Keys.W, Keys.SPACE, etc.) that are currently held down.
74
+ * @type {Set<string>}
75
+ * @private
76
+ */
77
+ static _down = new Set();
78
+
79
+ /**
80
+ * A reference to the main game instance. We store it so we can emit events
81
+ * via game.events whenever a key is pressed or released.
82
+ * @type {Game}
83
+ * @private
84
+ */
85
+ static game = null;
86
+
87
+ /**
88
+ * Initialize keyboard event handling. This attaches global listeners on the
89
+ * window so that whenever a key is pressed or released, we can map it to
90
+ * one of our Keys.* constants and emit the corresponding events on the game's
91
+ * EventEmitter.
92
+ *
93
+ * @param {Game} game - Your main Game instance, which has a central event emitter.
94
+ */
95
+ static init(game) {
96
+ Keys.game = game;
97
+
98
+ // Attach global keydown/keyup listeners
99
+ window.addEventListener("keydown", Keys._onKeyDown);
100
+ window.addEventListener("keyup", Keys._onKeyUp);
101
+ }
102
+
103
+ /**
104
+ * Returns true if the specified logical key (e.g. Keys.W) is currently held down.
105
+ *
106
+ * @param {string} logicalKey - One of the Keys.* constants.
107
+ * @returns {boolean} - True if that key is in the "down" set, false otherwise.
108
+ */
109
+ static isDown(logicalKey) {
110
+ return Keys._down.has(logicalKey);
111
+ }
112
+
113
+ /**
114
+ * Internal method called whenever a key is pressed. We look up which
115
+ * logical key constant it corresponds to, add it to our _down set,
116
+ * and emit an event on the game (e.g. game.events.emit(Keys.W, e)).
117
+ *
118
+ * @param {KeyboardEvent} e - The raw DOM event.
119
+ * @private
120
+ */
121
+ static _onKeyDown(e) {
122
+ const mappedKey = Keys._codeMap[e.code];
123
+ if (mappedKey) {
124
+ if (!Keys._down.has(mappedKey)) {
125
+ // Only emit this event the moment the key transitions from 'up' to 'down'
126
+ Keys._down.add(mappedKey);
127
+ Keys.game.events.emit(mappedKey, e);
128
+ }
129
+ }
130
+ // Dispatch the raw event as well, in case anyone cares about that.
131
+ Keys.game.events.emit(e.type, e);
132
+ }
133
+
134
+ /**
135
+ * Internal method called whenever a key is released. If it was one of our
136
+ * mapped keys, remove it from the _down set and emit an "_up" event.
137
+ *
138
+ * @param {KeyboardEvent} e - The raw DOM event.
139
+ * @private
140
+ */
141
+ static _onKeyUp(e) {
142
+ const mappedKey = Keys._codeMap[e.code];
143
+ if (mappedKey) {
144
+ if (Keys._down.has(mappedKey)) {
145
+ Keys._down.delete(mappedKey);
146
+ Keys.game.events.emit(mappedKey + "_up", e);
147
+ }
148
+ }
149
+ // Dispatch the raw event as well, in case anyone cares about that.
150
+ Keys.game.events.emit(e.type, e);
151
+ }
152
+ }
@@ -0,0 +1,61 @@
1
+ export class Mouse {
2
+ static init(game) {
3
+ Mouse.game = game;
4
+ Mouse.canvas = game.canvas;
5
+ Mouse.x = 0;
6
+ Mouse.y = 0;
7
+ Mouse.leftDown = false;
8
+ Mouse.middleDown = false;
9
+ Mouse.rightDown = false;
10
+
11
+ Mouse.canvas.addEventListener("mousemove", Mouse._onMove);
12
+ Mouse.canvas.addEventListener("mousedown", Mouse._onDown);
13
+ Mouse.canvas.addEventListener("mouseup", Mouse._onUp);
14
+ Mouse.canvas.addEventListener("click", Mouse._onClick);
15
+ Mouse.canvas.addEventListener("wheel", Mouse._onWheel);
16
+ }
17
+
18
+ static _updatePosition(e) {
19
+ const rect = Mouse.canvas.getBoundingClientRect();
20
+ Mouse.x = e.clientX - rect.left;
21
+ Mouse.y = e.clientY - rect.top;
22
+ }
23
+
24
+ static _onMove = (e) => {
25
+ Mouse._updatePosition(e);
26
+ Mouse.game.events.emit("mousemove", e);
27
+ };
28
+
29
+ static _onDown = (e) => {
30
+ Mouse._updatePosition(e);
31
+ if (e.button === 0) Mouse.leftDown = true;
32
+ if (e.button === 1) Mouse.middleDown = true;
33
+ if (e.button === 2) Mouse.rightDown = true;
34
+ Mouse.game.events.emit("mousedown", e);
35
+ };
36
+
37
+ static _onUp = (e) => {
38
+ Mouse._updatePosition(e);
39
+ if (e.button === 0) Mouse.leftDown = false;
40
+ if (e.button === 1) Mouse.middleDown = false;
41
+ if (e.button === 2) Mouse.rightDown = false;
42
+ Mouse.game.events.emit("mouseup", e);
43
+ };
44
+
45
+ static _onClick = (e) => {
46
+ Mouse._updatePosition(e);
47
+ // Emit enhanced event with canvas-relative coordinates
48
+ // Note: e is a MouseEvent, add canvas-relative x/y directly
49
+ e.canvasX = Mouse.x;
50
+ e.canvasY = Mouse.y;
51
+ // Also set x/y for convenience (matches expected fluent API)
52
+ Object.defineProperty(e, 'x', { value: Mouse.x, writable: false });
53
+ Object.defineProperty(e, 'y', { value: Mouse.y, writable: false });
54
+ Mouse.game.events.emit("click", e);
55
+ };
56
+
57
+ static _onWheel = (e) => {
58
+ Mouse._updatePosition(e);
59
+ Mouse.game.events.emit("wheel", e);
60
+ };
61
+ }
@@ -0,0 +1,39 @@
1
+ export class Touch {
2
+ static init(game) {
3
+ Touch.game = game;
4
+ Touch.canvas = game.canvas;
5
+ Touch.x = 0;
6
+ Touch.y = 0;
7
+ Touch.active = false;
8
+
9
+ Touch.canvas.addEventListener("touchstart", Touch._onStart);
10
+ Touch.canvas.addEventListener("touchend", Touch._onEnd);
11
+ Touch.canvas.addEventListener("touchmove", Touch._onMove);
12
+ }
13
+
14
+ static _updatePosition(touch) {
15
+ const rect = Touch.canvas.getBoundingClientRect();
16
+ Touch.x = touch.clientX - rect.left;
17
+ Touch.y = touch.clientY - rect.top;
18
+ }
19
+
20
+ static _onStart = (e) => {
21
+ if (e.touches.length > 0) {
22
+ Touch.active = true;
23
+ Touch._updatePosition(e.touches[0]);
24
+ Touch.game.events.emit("touchstart", e);
25
+ }
26
+ };
27
+
28
+ static _onEnd = (e) => {
29
+ Touch.active = false;
30
+ Touch.game.events.emit("touchend", e);
31
+ };
32
+
33
+ static _onMove = (e) => {
34
+ if (e.touches.length > 0) {
35
+ Touch._updatePosition(e.touches[0]);
36
+ Touch.game.events.emit("touchmove", e);
37
+ }
38
+ };
39
+ }