@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,277 @@
1
+ import { Painter } from "./painter";
2
+
3
+ export class PainterShapes {
4
+ // =========================================================================
5
+ // BASIC SHAPES
6
+ // =========================================================================
7
+
8
+ // Drawing functions that ensure opacity is applied
9
+ static rect(x, y, width, height, color) {
10
+ // Save current fillStyle
11
+ const oldFillStyle = Painter.ctx.fillStyle;
12
+ // Apply fill
13
+ Painter.colors.fill(color);
14
+ Painter.ctx.fillRect(x, y, width, height);
15
+ // Restore fillStyle
16
+ Painter.ctx.fillStyle = oldFillStyle;
17
+ }
18
+
19
+ static outlineRect(x, y, width, height, color, lineWidth = 1) {
20
+ // Save current styles
21
+ const oldStrokeStyle = Painter.ctx.strokeStyle;
22
+ const oldLineWidth = Painter.ctx.lineWidth;
23
+ // Apply stroke
24
+ Painter.ctx.strokeStyle = color;
25
+ Painter.ctx.lineWidth = lineWidth;
26
+ Painter.ctx.strokeRect(x, y, width, height);
27
+ // Restore styles
28
+ Painter.ctx.strokeStyle = oldStrokeStyle;
29
+ Painter.ctx.lineWidth = oldLineWidth;
30
+ }
31
+
32
+ /**
33
+ * Draw a rounded rectangle
34
+ * @param {number} x - X coordinate (top-left)
35
+ * @param {number} y - Y coordinate (top-left)
36
+ * @param {number} width - Width
37
+ * @param {number} height - Height
38
+ * @param {number|number[]} radii - Corner radius or array of radii for each corner
39
+ * @param {string|CanvasGradient} [fillColor] - Fill color
40
+ * @param {string|CanvasGradient} [strokeColor] - Stroke color
41
+ * @param {number} [lineWidth] - Line width
42
+ * @returns {void}
43
+ */
44
+ static roundRect(
45
+ x,
46
+ y,
47
+ width,
48
+ height,
49
+ radii = 0,
50
+ fillColor,
51
+ strokeColor,
52
+ lineWidth
53
+ ) {
54
+ // Handle radius either as a single value or array
55
+ let radiusArray;
56
+ if (typeof radii === "number") {
57
+ radiusArray = [radii, radii, radii, radii]; // [topLeft, topRight, bottomRight, bottomLeft]
58
+ } else if (Array.isArray(radii)) {
59
+ // Ensure we have exactly 4 values
60
+ radiusArray =
61
+ radii.length === 4
62
+ ? radii
63
+ : [
64
+ radii[0] || 0,
65
+ radii[1] || radii[0] || 0,
66
+ radii[2] || radii[0] || 0,
67
+ radii[3] || radii[1] || radii[0] || 0,
68
+ ];
69
+ } else {
70
+ radiusArray = [0, 0, 0, 0];
71
+ }
72
+
73
+ const [tlRadius, trRadius, brRadius, blRadius] = radiusArray;
74
+ const right = x + width;
75
+ const bottom = y + height;
76
+
77
+ Painter.lines.beginPath();
78
+
79
+ // Start from the top-left corner and draw clockwise
80
+ Painter.lines.moveTo(x + tlRadius, y);
81
+
82
+ // Top edge and top-right corner
83
+ Painter.lines.lineTo(right - trRadius, y);
84
+ this.arc(right - trRadius, y + trRadius, trRadius, -Math.PI / 2, 0);
85
+
86
+ // Right edge and bottom-right corner
87
+ Painter.lines.lineTo(right, bottom - brRadius);
88
+ this.arc(
89
+ right - brRadius,
90
+ bottom - brRadius,
91
+ brRadius,
92
+ 0,
93
+ Math.PI / 2
94
+ );
95
+
96
+ // Bottom edge and bottom-left corner
97
+ Painter.lines.lineTo(x + blRadius, bottom);
98
+ this.arc(
99
+ x + blRadius,
100
+ bottom - blRadius,
101
+ blRadius,
102
+ Math.PI / 2,
103
+ Math.PI
104
+ );
105
+
106
+ // Left edge and top-left corner
107
+ Painter.lines.lineTo(x, y + tlRadius);
108
+ this.arc(
109
+ x + tlRadius,
110
+ y + tlRadius,
111
+ tlRadius,
112
+ Math.PI,
113
+ -Math.PI / 2
114
+ );
115
+
116
+ Painter.lines.closePath();
117
+
118
+ if (fillColor) {
119
+ Painter.fillStyle = fillColor;
120
+ Painter.colors.fill(fillColor);
121
+ }
122
+
123
+ if (strokeColor) {
124
+ Painter.colors.stroke(strokeColor, lineWidth);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Draw a filled rounded rectangle
130
+ * @param {number} x - X coordinate (top-left)
131
+ * @param {number} y - Y coordinate (top-left)
132
+ * @param {number} width - Width
133
+ * @param {number} height - Height
134
+ * @param {number|number[]} radii - Corner radius or array of radii
135
+ * @param {string|CanvasGradient} [color] - Fill color
136
+ * @returns {void}
137
+ */
138
+ static fillRoundRect(x, y, width, height, radii = 0, color) {
139
+ this.roundRect(x, y, width, height, radii, color, null);
140
+ }
141
+
142
+ /**
143
+ * Draw a stroked rounded rectangle
144
+ * @param {number} x - X coordinate (top-left)
145
+ * @param {number} y - Y coordinate (top-left)
146
+ * @param {number} width - Width
147
+ * @param {number} height - Height
148
+ * @param {number|number[]} radii - Corner radius or array of radii
149
+ * @param {string|CanvasGradient} [color] - Stroke color
150
+ * @param {number} [lineWidth] - Line width
151
+ * @returns {void}
152
+ */
153
+ static strokeRoundRect(x, y, width, height, radii = 0, color, lineWidth) {
154
+ this.roundRect(x, y, width, height, radii, null, color, lineWidth);
155
+ }
156
+
157
+ /**
158
+ * Draw a filled circle
159
+ * @param {number} x - Center X
160
+ * @param {number} y - Center Y
161
+ * @param {number} radius - Circle radius
162
+ * @param {string|CanvasGradient} [color] - Fill color
163
+ * @returns {void}
164
+ */
165
+ static fillCircle(x, y, radius, color) {
166
+ Painter.logger.log("PainterShapes.fillCircle", x, y, radius, color);
167
+ Painter.lines.beginPath();
168
+ this.arc(x, y, radius, 0, Math.PI * 2);
169
+ Painter.colors.fill(color);
170
+ }
171
+
172
+ static arc(x, y, radius, startAngle, endAngle, counterclockwise) {
173
+ Painter.ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise);
174
+ }
175
+
176
+ /**
177
+ * Draw a stroked circle
178
+ * @param {number} x - Center X
179
+ * @param {number} y - Center Y
180
+ * @param {number} radius - Circle radius
181
+ * @param {string|CanvasGradient} [color] - Stroke color
182
+ * @param {number} [lineWidth] - Line width
183
+ * @returns {void}
184
+ */
185
+ static strokeCircle(x, y, radius, color, lineWidth) {
186
+ Painter.lines.beginPath();
187
+ this.arc(x, y, radius, 0, Math.PI * 2);
188
+ Painter.colors.stroke(color, lineWidth);
189
+ }
190
+
191
+ /**
192
+ * Draw a filled ellipse
193
+ * @param {number} x - Center X
194
+ * @param {number} y - Center Y
195
+ * @param {number} radiusX - X radius
196
+ * @param {number} radiusY - Y radius
197
+ * @param {number} [rotation=0] - Rotation in radians
198
+ * @param {string|CanvasGradient} [color] - Fill color
199
+ * @returns {void}
200
+ */
201
+ static fillEllipse(x, y, radiusX, radiusY, rotation = 0, color) {
202
+ Painter.lines.beginPath();
203
+ this.ellipse(x, y, radiusX, radiusY, rotation, 0, Math.PI * 2);
204
+ if (color) Painter.fillStyle = color;
205
+ Painter.colors.fill(color);
206
+ }
207
+
208
+ /**
209
+ * Draw a stroked ellipse
210
+ * @param {number} x - Center X
211
+ * @param {number} y - Center Y
212
+ * @param {number} radiusX - X radius
213
+ * @param {number} radiusY - Y radius
214
+ * @param {number} [rotation=0] - Rotation in radians
215
+ * @param {string|CanvasGradient} [color] - Stroke color
216
+ * @param {number} [lineWidth] - Line width
217
+ * @returns {void}
218
+ */
219
+ static strokeEllipse(x, y, radiusX, radiusY, rotation = 0, color, lineWidth) {
220
+ Painter.lines.beginPath();
221
+ this.ellipse(x, y, radiusX, radiusY, rotation, 0, Math.PI * 2);
222
+ if (color) Painter.strokeStyle = color;
223
+ if (lineWidth !== undefined) Painter.lineWidth = lineWidth;
224
+ Painter.colors.stroke(color, lineWidth);
225
+ }
226
+
227
+ static ellipse(
228
+ x,
229
+ y,
230
+ radiusX,
231
+ radiusY,
232
+ rotation,
233
+ startAngle,
234
+ endAngle,
235
+ counterclockwise
236
+ ) {
237
+ Painter.ctx.ellipse(
238
+ x,
239
+ y,
240
+ radiusX,
241
+ radiusY,
242
+ rotation,
243
+ startAngle,
244
+ endAngle,
245
+ counterclockwise
246
+ );
247
+ }
248
+
249
+ /**
250
+ * Draw a polygon
251
+ * @param {Array<{x: number, y: number}>} points - Array of points
252
+ * @param {string|CanvasGradient} [fillColor] - Fill color
253
+ * @param {string|CanvasGradient} [strokeColor] - Stroke color
254
+ * @param {number} [lineWidth] - Line width
255
+ * @returns {void}
256
+ */
257
+ static polygon(points, fillColor, strokeColor, lineWidth) {
258
+ if (points.length < 2) return;
259
+
260
+ Painter.lines.beginPath();
261
+ Painter.lines.moveTo(points[0].x, points[0].y);
262
+
263
+ for (let i = 1; i < points.length; i++) {
264
+ Painter.lines.lineTo(points[i].x, points[i].y);
265
+ }
266
+
267
+ Painter.lines.closePath();
268
+
269
+ if (fillColor) {
270
+ Painter.colors.fill(fillColor);
271
+ }
272
+
273
+ if (strokeColor) {
274
+ Painter.colors.stroke(strokeColor, lineWidth);
275
+ }
276
+ }
277
+ }
@@ -0,0 +1,273 @@
1
+ import { Painter } from "./painter";
2
+ export class PainterText {
3
+
4
+ static font() {
5
+ return Painter.ctx.font;
6
+ }
7
+
8
+ /**
9
+ * Set font for text drawing
10
+ * @param {string} font - Font specification (e.g. "24px 'Courier New', monospace")
11
+ * @returns {void}
12
+ */
13
+ static setFont(font) {
14
+ Painter.ctx.font = font;
15
+ }
16
+
17
+ /**
18
+ * Set text alignment
19
+ * @param {CanvasTextAlign} align - Text alignment ('left', 'right', 'center', 'start', 'end')
20
+ * @returns {void}
21
+ */
22
+ static setTextAlign(align) {
23
+ Painter.ctx.textAlign = align;
24
+ }
25
+
26
+ /**
27
+ * Set text baseline
28
+ * @param {CanvasTextBaseline} baseline - Text baseline ('top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom')
29
+ * @returns {void}
30
+ */
31
+ static setTextBaseline(baseline) {
32
+ Painter.ctx.textBaseline = baseline;
33
+ }
34
+
35
+ /**
36
+ * Draw filled text
37
+ * @param {string} text - Text to draw
38
+ * @param {number} x - X coordinate
39
+ * @param {number} y - Y coordinate
40
+ * @param {string|CanvasGradient} [color] - Text color
41
+ * @param {string} [font] - Font specification
42
+ * @returns {void}
43
+ */
44
+ static fillText(text, x, y, color, font) {
45
+ if (color) Painter.ctx.fillStyle = color;
46
+ if (font) Painter.ctx.font = font;
47
+ Painter.ctx.fillText(text, x, y);
48
+ }
49
+
50
+ /**
51
+ * Draw stroked text
52
+ * @param {string} text - Text to draw
53
+ * @param {number} x - X coordinate
54
+ * @param {number} y - Y coordinate
55
+ * @param {string|CanvasGradient} [color] - Text color
56
+ * @param {number} [lineWidth] - Line width
57
+ * @param {string} [font] - Font specification
58
+ * @returns {void}
59
+ */
60
+ static strokeText(text, x, y, color, lineWidth, font) {
61
+ if (color) Painter.ctx.strokeStyle = color;
62
+ if (lineWidth !== undefined) Painter.ctx.lineWidth = lineWidth;
63
+ if (font) Painter.ctx.font = font;
64
+ Painter.ctx.strokeText(text, x, y);
65
+ }
66
+
67
+ /**
68
+ * Improved text measurement that accounts for baseline adjustments
69
+ * @param {string} text - Text to measure
70
+ * @param {string} [font] - Font specification
71
+ * @param {string} [align="start"] - Text alignment
72
+ * @param {string} [baseline="alphabetic"] - Text baseline
73
+ * @returns {{width: number, height: number, verticalAdjustment: number}} Text dimensions with adjustment
74
+ */
75
+ static measureTextDimensions(
76
+ text,
77
+ font,
78
+ align = "start",
79
+ baseline = "alphabetic"
80
+ ) {
81
+ if (font) Painter.ctx.font = font;
82
+ const metrics = Painter.ctx.measureText(text);
83
+ const width = metrics.width;
84
+ const height =
85
+ metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
86
+
87
+ // Calculate vertical adjustment based on baseline
88
+ let verticalAdjustment = 0;
89
+
90
+ if (baseline === "middle") {
91
+ // For middle baseline, text is visually slightly higher than the mathematical middle
92
+ // This small correction factor improves visual alignment
93
+ verticalAdjustment = -1.5; // Empirically determined adjustment
94
+ }
95
+
96
+ return {
97
+ width: width,
98
+ height: height,
99
+ verticalAdjustment: verticalAdjustment,
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Measure text width
105
+ * @param {string} text - Text to measure
106
+ * @returns {number} Text width
107
+ */
108
+ static measureTextWidth(text, font) {
109
+ if (font) Painter.ctx.font = font;
110
+ return Painter.ctx.measureText(text).width;
111
+ }
112
+
113
+ /**
114
+ * Draw filled and stroked text with outline
115
+ * @param {string} text - Text to draw
116
+ * @param {number} x - X coordinate
117
+ * @param {number} y - Y coordinate
118
+ * @param {string} fillColor - Fill color
119
+ * @param {string} strokeColor - Stroke color
120
+ * @param {number} strokeWidth - Stroke width
121
+ * @param {string} [font] - Font specification
122
+ * @returns {void}
123
+ */
124
+ static outlinedText(text, x, y, fillColor, strokeColor, strokeWidth, font) {
125
+ if (font) Painter.ctx.font = font;
126
+
127
+ // Draw the stroke first
128
+ Painter.ctx.strokeStyle = strokeColor;
129
+ Painter.ctx.lineWidth = strokeWidth;
130
+ Painter.ctx.strokeText(text, x, y);
131
+
132
+ // Then draw the fill
133
+ Painter.ctx.fillStyle = fillColor;
134
+ Painter.ctx.fillText(text, x, y);
135
+ }
136
+
137
+ /**
138
+ * Draw text with a maximum width, wrapping to new lines as needed
139
+ * @param {string} text - Text to draw
140
+ * @param {number} x - X coordinate
141
+ * @param {number} y - Y coordinate
142
+ * @param {number} maxWidth - Maximum width before wrapping
143
+ * @param {number} lineHeight - Line height for wrapped text
144
+ * @param {string} [color] - Text color
145
+ * @param {string} [font] - Font specification
146
+ * @returns {number} Total height of drawn text
147
+ */
148
+ static wrappedText(text, x, y, maxWidth, lineHeight, color, font) {
149
+ if (color) Painter.ctx.fillStyle = color;
150
+ if (font) Painter.ctx.font = font;
151
+
152
+ const words = text.split(" ");
153
+ let line = "";
154
+ let testLine = "";
155
+ let lineCount = 1;
156
+
157
+ for (let i = 0; i < words.length; i++) {
158
+ testLine = line + words[i] + " ";
159
+ const metrics = Painter.ctx.measureText(testLine);
160
+ const testWidth = metrics.width;
161
+
162
+ if (testWidth > maxWidth && i > 0) {
163
+ Painter.ctx.fillText(line, x, y);
164
+ line = words[i] + " ";
165
+ y += lineHeight;
166
+ lineCount++;
167
+ } else {
168
+ line = testLine;
169
+ }
170
+ }
171
+
172
+ Painter.ctx.fillText(line, x, y);
173
+ return lineCount * lineHeight;
174
+ }
175
+
176
+ /**
177
+ * Draw text along a path (like an arc)
178
+ * @param {string} text - Text to draw
179
+ * @param {Array} path - Array of points {x, y} defining the path
180
+ * @param {string} [color] - Text color
181
+ * @param {string} [font] - Font specification
182
+ * @param {boolean} [reverse=false] - Whether to draw text in reverse direction
183
+ * @returns {void}
184
+ */
185
+ static textOnPath(text, path, color, font, reverse = false) {
186
+ if (path.length < 2) return;
187
+
188
+ if (color) Painter.ctx.fillStyle = color;
189
+ if (font) Painter.ctx.font = font;
190
+
191
+ // Get characters and their widths
192
+ const chars = text.split("");
193
+ const charWidths = chars.map((char) => Painter.ctx.measureText(char).width);
194
+
195
+ if (reverse) {
196
+ chars.reverse();
197
+ charWidths.reverse();
198
+ path.reverse();
199
+ }
200
+
201
+ // Calculate total length of the path
202
+ let pathLength = 0;
203
+ for (let i = 1; i < path.length; i++) {
204
+ const dx = path[i].x - path[i - 1].x;
205
+ const dy = path[i].y - path[i - 1].y;
206
+ pathLength += Math.sqrt(dx * dx + dy * dy);
207
+ }
208
+
209
+ // Calculate total width of text
210
+ const textWidth = charWidths.reduce((total, width) => total + width, 0);
211
+
212
+ // Calculate starting offset to center text on path
213
+ let offset = (pathLength - textWidth) / 2;
214
+ if (offset < 0) offset = 0;
215
+
216
+ // Draw each character
217
+ let currentOffset = offset;
218
+ for (let i = 0; i < chars.length; i++) {
219
+ const charWidth = charWidths[i];
220
+
221
+ // Find position and angle on path
222
+ const { x, y, angle } = getPositionOnPath(path, currentOffset);
223
+
224
+ // Draw the character
225
+ Painter.ctx.save();
226
+ Painter.ctx.translate(x, y);
227
+ Painter.ctx.rotate(angle);
228
+ Painter.ctx.fillText(chars[i], 0, 0);
229
+ Painter.ctx.restore();
230
+
231
+ currentOffset += charWidth;
232
+ }
233
+ }
234
+
235
+ // Helper function for textOnPath
236
+ static getPositionOnPath(path, offset) {
237
+ let currentLength = 0;
238
+
239
+ for (let i = 1; i < path.length; i++) {
240
+ const p1 = path[i - 1];
241
+ const p2 = path[i];
242
+ const dx = p2.x - p1.x;
243
+ const dy = p2.y - p1.y;
244
+ const segmentLength = Math.sqrt(dx * dx + dy * dy);
245
+
246
+ if (currentLength + segmentLength >= offset) {
247
+ // Calculate position between p1 and p2
248
+ const t = (offset - currentLength) / segmentLength;
249
+ const x = p1.x + dx * t;
250
+ const y = p1.y + dy * t;
251
+ const angle = Math.atan2(dy, dx);
252
+
253
+ return { x, y, angle };
254
+ }
255
+
256
+ currentLength += segmentLength;
257
+ }
258
+
259
+ // If offset is beyond path length, return last point
260
+ const lastPoint = path[path.length - 1];
261
+ const secondLastPoint = path[path.length - 2];
262
+ const angle = Math.atan2(
263
+ lastPoint.y - secondLastPoint.y,
264
+ lastPoint.x - secondLastPoint.x
265
+ );
266
+
267
+ return {
268
+ x: lastPoint.x,
269
+ y: lastPoint.y,
270
+ angle,
271
+ };
272
+ }
273
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * ParticleEmitter - Configuration and spawning logic for particles
3
+ *
4
+ * Defines spawn position, velocity, lifetime, and appearance properties.
5
+ * Supports continuous emission (rate) and burst spawning.
6
+ */
7
+ export class ParticleEmitter {
8
+ /**
9
+ * @param {Object} options - Emitter configuration
10
+ * @param {number} [options.rate=10] - Particles per second (continuous emission)
11
+ * @param {Object} [options.position] - Spawn position { x, y, z }
12
+ * @param {Object} [options.spread] - Position randomization { x, y, z }
13
+ * @param {Object} [options.velocity] - Initial velocity { x, y, z }
14
+ * @param {Object} [options.velocitySpread] - Velocity randomization { x, y, z }
15
+ * @param {Object} [options.lifetime] - Particle lifetime { min, max } in seconds
16
+ * @param {Object} [options.size] - Particle size { min, max }
17
+ * @param {Object} [options.color] - Base color { r, g, b, a }
18
+ * @param {string} [options.shape] - Particle shape: "circle", "square", "triangle"
19
+ */
20
+ constructor(options = {}) {
21
+ // Emission rate (particles per second)
22
+ this.rate = options.rate ?? 10;
23
+
24
+ // Spawn position and spread
25
+ this.position = { x: 0, y: 0, z: 0, ...options.position };
26
+ this.spread = { x: 0, y: 0, z: 0, ...options.spread };
27
+
28
+ // Initial velocity and spread
29
+ this.velocity = { x: 0, y: 0, z: 0, ...options.velocity };
30
+ this.velocitySpread = { x: 0, y: 0, z: 0, ...options.velocitySpread };
31
+
32
+ // Particle lifetime range (seconds)
33
+ this.lifetime = { min: 1, max: 2, ...options.lifetime };
34
+
35
+ // Particle size range
36
+ this.size = { min: 1, max: 1, ...options.size };
37
+
38
+ // Base color
39
+ this.color = { r: 255, g: 255, b: 255, a: 1, ...options.color };
40
+
41
+ // Particle shape
42
+ this.shape = options.shape ?? "circle";
43
+
44
+ // Emitter state
45
+ this.active = options.active !== false;
46
+ this._timer = 0;
47
+ }
48
+
49
+ /**
50
+ * Random value in range [-spread, spread].
51
+ * @private
52
+ */
53
+ _spread(spread) {
54
+ return (Math.random() - 0.5) * 2 * spread;
55
+ }
56
+
57
+ /**
58
+ * Random value in range [min, max].
59
+ * @private
60
+ */
61
+ _range(min, max) {
62
+ return min + Math.random() * (max - min);
63
+ }
64
+
65
+ /**
66
+ * Initialize a particle with emitter settings.
67
+ * @param {Particle} p - Particle to initialize
68
+ */
69
+ emit(p) {
70
+ // Position with spread
71
+ p.x = this.position.x + this._spread(this.spread.x);
72
+ p.y = this.position.y + this._spread(this.spread.y);
73
+ p.z = this.position.z + this._spread(this.spread.z);
74
+
75
+ // Velocity with spread
76
+ p.vx = this.velocity.x + this._spread(this.velocitySpread.x);
77
+ p.vy = this.velocity.y + this._spread(this.velocitySpread.y);
78
+ p.vz = this.velocity.z + this._spread(this.velocitySpread.z);
79
+
80
+ // Lifetime
81
+ p.lifetime = this._range(this.lifetime.min, this.lifetime.max);
82
+ p.age = 0;
83
+ p.alive = true;
84
+
85
+ // Size
86
+ p.size = this._range(this.size.min, this.size.max);
87
+
88
+ // Color (copy to avoid shared reference)
89
+ p.color.r = this.color.r;
90
+ p.color.g = this.color.g;
91
+ p.color.b = this.color.b;
92
+ p.color.a = this.color.a;
93
+
94
+ // Shape
95
+ p.shape = this.shape;
96
+ }
97
+
98
+ /**
99
+ * Update emitter and return number of particles to spawn.
100
+ * @param {number} dt - Delta time in seconds
101
+ * @returns {number} Number of particles to spawn this frame
102
+ */
103
+ update(dt) {
104
+ if (!this.active || this.rate <= 0) return 0;
105
+
106
+ this._timer += dt;
107
+ const interval = 1 / this.rate;
108
+ let count = 0;
109
+
110
+ while (this._timer >= interval) {
111
+ this._timer -= interval;
112
+ count++;
113
+ }
114
+
115
+ return count;
116
+ }
117
+
118
+ /**
119
+ * Reset emission timer.
120
+ */
121
+ reset() {
122
+ this._timer = 0;
123
+ }
124
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Particle System Module
3
+ *
4
+ * High-performance particle system with optional Camera3D support.
5
+ *
6
+ * @module particle
7
+ */
8
+ export { Particle } from "./particle.js";
9
+ export { ParticleEmitter } from "./emitter.js";
10
+ export { ParticleSystem } from "./particle-system.js";
11
+ export { Updaters } from "./updaters.js";