@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,537 @@
1
+ import { Shape } from "./shape.js";
2
+ import { Painter } from "../painter/painter.js";
3
+ import { WebGLRenderer } from "../webgl/webgl-renderer.js";
4
+ import { SPHERE_SHADERS } from "../webgl/shaders/sphere-shaders.js";
5
+
6
+ /**
7
+ * Sphere3D - A true 3D sphere that integrates with Camera3D
8
+ *
9
+ * Unlike the 2D Sphere class which uses isometric projection,
10
+ * this sphere works with Scene3D and Camera3D to provide true
11
+ * 3D rotation and perspective projection.
12
+ *
13
+ * Features:
14
+ * - Integrates with Camera3D rotation state
15
+ * - Supports solid colors and gradients
16
+ * - Debug wireframe mode
17
+ * - Depth-sorted face rendering
18
+ * - Surface normal-based lighting
19
+ * - Optional WebGL shader rendering for advanced effects
20
+ *
21
+ * @example
22
+ * // Basic sphere with Canvas 2D rendering
23
+ * const sphere = new Sphere3D(50, {
24
+ * color: "#FFD700",
25
+ * camera: this.camera,
26
+ * });
27
+ *
28
+ * @example
29
+ * // Sphere with WebGL shader for star effect
30
+ * const star = new Sphere3D(50, {
31
+ * camera: this.camera,
32
+ * useShader: true,
33
+ * shaderType: "star",
34
+ * shaderUniforms: {
35
+ * uStarColor: [1.0, 0.9, 0.5],
36
+ * uTemperature: 5778,
37
+ * uActivityLevel: 0.5,
38
+ * },
39
+ * });
40
+ */
41
+ export class Sphere3D extends Shape {
42
+ // Shared WebGL renderer for all shader-enabled spheres
43
+ static _glRenderer = null;
44
+ static _glRendererSize = { width: 0, height: 0 };
45
+
46
+ /**
47
+ * Get or create shared WebGL renderer
48
+ * @param {number} width - Required width
49
+ * @param {number} height - Required height
50
+ * @returns {WebGLRenderer|null}
51
+ * @private
52
+ */
53
+ static _getGLRenderer(width, height) {
54
+ // Create or resize renderer as needed
55
+ if (!Sphere3D._glRenderer) {
56
+ Sphere3D._glRenderer = new WebGLRenderer(width, height);
57
+ Sphere3D._glRendererSize = { width, height };
58
+ } else if (
59
+ Sphere3D._glRendererSize.width !== width ||
60
+ Sphere3D._glRendererSize.height !== height
61
+ ) {
62
+ Sphere3D._glRenderer.resize(width, height);
63
+ Sphere3D._glRendererSize = { width, height };
64
+ }
65
+ return Sphere3D._glRenderer;
66
+ }
67
+
68
+ /**
69
+ * Create a 3D sphere
70
+ * @param {number} radius - Sphere radius
71
+ * @param {object} options - Configuration options
72
+ * @param {string|CanvasGradient} [options.color] - Fill color or gradient
73
+ * @param {Camera3D} [options.camera] - Camera for rotation (optional, can be set later)
74
+ * @param {boolean} [options.debug=false] - Show wireframe
75
+ * @param {number} [options.segments=20] - Number of latitude/longitude segments
76
+ * @param {string} [options.stroke] - Wireframe line color
77
+ * @param {number} [options.lineWidth=1] - Wireframe line width
78
+ * @param {boolean} [options.useShader=false] - Use WebGL shader rendering
79
+ * @param {string} [options.shaderType='star'] - Shader type: 'star', 'blackHole', 'rockyPlanet', 'gasGiant'
80
+ * @param {Object} [options.shaderUniforms={}] - Custom shader uniforms
81
+ */
82
+ constructor(radius, options = {}) {
83
+ super(options);
84
+
85
+ this.radius = radius;
86
+ this.camera = options.camera ?? null;
87
+ this.debug = options.debug ?? false;
88
+ this.segments = options.segments ?? 20;
89
+
90
+ // WebGL shader options
91
+ this.useShader = options.useShader ?? false;
92
+ this.shaderType = options.shaderType ?? "star";
93
+ this.shaderUniforms = options.shaderUniforms ?? {};
94
+ this._shaderInitialized = false;
95
+
96
+ // Generate sphere geometry (for Canvas 2D fallback)
97
+ this._generateGeometry();
98
+ }
99
+
100
+ /**
101
+ * Set or update the camera reference
102
+ * @param {Camera3D} camera - Camera instance
103
+ */
104
+ setCamera(camera) {
105
+ this.camera = camera;
106
+ return this;
107
+ }
108
+
109
+ /**
110
+ * Update shader uniforms dynamically
111
+ * @param {Object} uniforms - Uniform name -> value pairs
112
+ */
113
+ setShaderUniforms(uniforms) {
114
+ Object.assign(this.shaderUniforms, uniforms);
115
+ return this;
116
+ }
117
+
118
+ /**
119
+ * Get fragment shader source for the current shader type
120
+ * @returns {string}
121
+ * @private
122
+ */
123
+ _getFragmentShader() {
124
+ switch (this.shaderType) {
125
+ case "star":
126
+ return SPHERE_SHADERS.star;
127
+ case "blackHole":
128
+ return SPHERE_SHADERS.blackHole;
129
+ case "rockyPlanet":
130
+ return SPHERE_SHADERS.rockyPlanet;
131
+ case "gasGiant":
132
+ return SPHERE_SHADERS.gasGiant;
133
+ default:
134
+ return SPHERE_SHADERS.star;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Initialize or update the WebGL shader
140
+ * @param {number} renderWidth - Render width
141
+ * @param {number} renderHeight - Render height
142
+ * @private
143
+ */
144
+ _initShader(renderWidth, renderHeight) {
145
+ const gl = Sphere3D._getGLRenderer(renderWidth, renderHeight);
146
+ if (!gl || !gl.isAvailable()) {
147
+ this.useShader = false; // Fallback to Canvas 2D
148
+ return;
149
+ }
150
+
151
+ // Initialize shader program
152
+ const programName = `sphere_${this.shaderType}`;
153
+ gl.useProgram(programName, SPHERE_SHADERS.vertex, this._getFragmentShader());
154
+ this._shaderInitialized = true;
155
+ }
156
+
157
+ /**
158
+ * Render using WebGL shader
159
+ * @param {CanvasRenderingContext2D} ctx - 2D context to composite onto
160
+ * @param {number} screenX - Screen X position
161
+ * @param {number} screenY - Screen Y position
162
+ * @param {number} screenRadius - Radius on screen
163
+ * @private
164
+ */
165
+ _renderWithShader(ctx, screenX, screenY, screenRadius) {
166
+ // Calculate render size (larger for glow effects and to prevent clipping)
167
+ // Account for tidal stretch - stretched stars extend beyond normal radius
168
+ const tidalStretch = this.shaderUniforms?.uTidalStretch ?? 0;
169
+ const stretchMultiplier = 1 + tidalStretch; // e.g., 1.8 stretch = 2.8x multiplier
170
+ const padding = screenRadius * stretchMultiplier; // Dynamic padding based on stretch
171
+ const renderSize = Math.ceil((screenRadius + padding) * 2);
172
+
173
+ const gl = Sphere3D._getGLRenderer(renderSize, renderSize);
174
+ if (!gl || !gl.isAvailable()) {
175
+ return false;
176
+ }
177
+
178
+ // Initialize shader if needed
179
+ if (!this._shaderInitialized) {
180
+ this._initShader(renderSize, renderSize);
181
+ }
182
+
183
+ // Use the program
184
+ const programName = `sphere_${this.shaderType}`;
185
+ gl.useProgram(programName, SPHERE_SHADERS.vertex, this._getFragmentShader());
186
+
187
+ // Clear with transparency
188
+ gl.clear(0, 0, 0, 0);
189
+
190
+ // Calculate the base radius for the shader
191
+ // The camera's visible half-width at z=0 is about 1.25 units (based on FOV)
192
+ // We want the unstretched star to appear at screenRadius size
193
+ // baseRadius = visible_half_width * screenRadius / (renderSize / 2)
194
+ const visibleHalfWidth = 1.25;
195
+ const uBaseRadius = visibleHalfWidth * screenRadius / (renderSize / 2);
196
+
197
+ // Set common uniforms
198
+ gl.setUniforms({
199
+ uTime: performance.now() / 1000,
200
+ uResolution: [renderSize, renderSize],
201
+ uBaseRadius: uBaseRadius,
202
+ uCameraRotation: [
203
+ this.camera?.rotationX ?? 0,
204
+ this.camera?.rotationY ?? 0,
205
+ this.camera?.rotationZ ?? 0,
206
+ ],
207
+ });
208
+
209
+ // Set shader-specific uniforms
210
+ gl.setUniforms(this.shaderUniforms);
211
+
212
+ // Handle color uniforms (convert hex to RGB)
213
+ for (const [name, value] of Object.entries(this.shaderUniforms)) {
214
+ if (typeof value === "string" && value.startsWith("#")) {
215
+ gl.setColorUniform(name, value);
216
+ }
217
+ }
218
+
219
+ // Render
220
+ gl.render();
221
+
222
+ // Composite onto 2D canvas
223
+ // No clip needed - shader renders with proper alpha transparency
224
+ // This allows stretched/elliptical shapes and their glows to render fully
225
+ const drawX = screenX - renderSize / 2;
226
+ const drawY = screenY - renderSize / 2;
227
+
228
+ gl.compositeOnto(ctx, drawX, drawY, renderSize, renderSize);
229
+
230
+ return true;
231
+ }
232
+
233
+ /**
234
+ * Generate sphere vertices and faces using UV parameterization
235
+ * @private
236
+ */
237
+ _generateGeometry() {
238
+ this.vertices = [];
239
+ this.faces = [];
240
+
241
+ const latSegments = this.segments;
242
+ const lonSegments = this.segments * 2;
243
+
244
+ // Generate vertices
245
+ for (let lat = 0; lat <= latSegments; lat++) {
246
+ const theta = (lat * Math.PI) / latSegments; // 0 to PI
247
+ const sinTheta = Math.sin(theta);
248
+ const cosTheta = Math.cos(theta);
249
+
250
+ for (let lon = 0; lon <= lonSegments; lon++) {
251
+ const phi = (lon * 2 * Math.PI) / lonSegments; // 0 to 2PI
252
+ const sinPhi = Math.sin(phi);
253
+ const cosPhi = Math.cos(phi);
254
+
255
+ // Spherical to Cartesian coordinates
256
+ const x = this.radius * sinTheta * cosPhi;
257
+ const y = this.radius * cosTheta;
258
+ const z = this.radius * sinTheta * sinPhi;
259
+
260
+ // Store vertex with its normal (normalized position vector)
261
+ this.vertices.push({
262
+ x,
263
+ y,
264
+ z,
265
+ nx: sinTheta * cosPhi,
266
+ ny: cosTheta,
267
+ nz: sinTheta * sinPhi,
268
+ });
269
+ }
270
+ }
271
+
272
+ // Generate faces (quads split into triangles)
273
+ for (let lat = 0; lat < latSegments; lat++) {
274
+ for (let lon = 0; lon < lonSegments; lon++) {
275
+ const first = lat * (lonSegments + 1) + lon;
276
+ const second = first + lonSegments + 1;
277
+
278
+ // Two triangles per quad
279
+ this.faces.push([first, second, first + 1]);
280
+ this.faces.push([second, second + 1, first + 1]);
281
+ }
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Calculate lighting intensity based on surface normal
287
+ * @param {number} nx - Normal x component
288
+ * @param {number} ny - Normal y component
289
+ * @param {number} nz - Normal z component
290
+ * @returns {number} Intensity 0-1
291
+ * @private
292
+ */
293
+ _calculateLighting(nx, ny, nz) {
294
+ // Simple directional light from top-right-front
295
+ const lightX = 0.5;
296
+ const lightY = 0.7;
297
+ const lightZ = 0.5;
298
+ const lightLen = Math.sqrt(
299
+ lightX * lightX + lightY * lightY + lightZ * lightZ
300
+ );
301
+
302
+ // Normalized light direction
303
+ const lx = lightX / lightLen;
304
+ const ly = lightY / lightLen;
305
+ const lz = lightZ / lightLen;
306
+
307
+ // Dot product for diffuse lighting
308
+ let intensity = nx * lx + ny * ly + nz * lz;
309
+
310
+ // Clamp and add ambient light
311
+ intensity = Math.max(0, intensity) * 0.7 + 0.3;
312
+
313
+ return intensity;
314
+ }
315
+
316
+ /**
317
+ * Apply lighting to a color
318
+ * @param {string} color - Base color (hex format)
319
+ * @param {number} intensity - Light intensity 0-1
320
+ * @returns {string} RGB color string
321
+ * @private
322
+ */
323
+ _applyLighting(color, intensity) {
324
+ // If it's a gradient or non-hex color, return as-is
325
+ if (!color || typeof color !== "string" || !color.startsWith("#")) {
326
+ return color;
327
+ }
328
+
329
+ // Parse hex color
330
+ const hex = color.replace("#", "");
331
+ const r = parseInt(hex.substring(0, 2), 16);
332
+ const g = parseInt(hex.substring(2, 4), 16);
333
+ const b = parseInt(hex.substring(4, 6), 16);
334
+
335
+ // Apply intensity
336
+ const lr = Math.round(r * intensity);
337
+ const lg = Math.round(g * intensity);
338
+ const lb = Math.round(b * intensity);
339
+
340
+ return `rgb(${lr}, ${lg}, ${lb})`;
341
+ }
342
+
343
+ /**
344
+ * Main render method
345
+ */
346
+ draw() {
347
+ super.draw();
348
+
349
+ if (!this.camera) {
350
+ // Fallback: draw a simple circle if no camera
351
+ if (this.color) {
352
+ Painter.shapes.fillCircle(0, 0, this.radius, this.color);
353
+ }
354
+ if (this.debug && this.stroke) {
355
+ Painter.shapes.strokeCircle(0, 0, this.radius, this.stroke, this.lineWidth);
356
+ }
357
+ return;
358
+ }
359
+
360
+ // WebGL shader rendering path
361
+ if (this.useShader && !this.debug) {
362
+ // Project sphere center to get screen position and scale
363
+ const projected = this.camera.project(
364
+ this.x || 0,
365
+ this.y || 0,
366
+ this.z || 0
367
+ );
368
+
369
+ // Calculate screen radius based on perspective
370
+ const scale = this.camera.perspective / (this.camera.perspective + projected.z);
371
+ const screenRadius = this.radius * scale;
372
+
373
+ // Get the current canvas transform to find the scene center
374
+ // The Scene3D translates to (game.width/2, game.height/2)
375
+ // We need absolute screen coordinates for WebGL compositing
376
+ const ctx = Painter.ctx;
377
+ const transform = ctx.getTransform();
378
+ const sceneX = transform.e; // Translation X (scene center)
379
+ const sceneY = transform.f; // Translation Y (scene center)
380
+
381
+ // Render with shader at absolute screen position
382
+ // Reset transform temporarily for compositing
383
+ ctx.save();
384
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
385
+ const success = this._renderWithShader(
386
+ ctx,
387
+ sceneX + projected.x,
388
+ sceneY + projected.y,
389
+ screenRadius
390
+ );
391
+ ctx.restore();
392
+
393
+ if (success) {
394
+ return; // Successfully rendered with shader
395
+ }
396
+ // Fall through to Canvas 2D if shader failed
397
+ }
398
+
399
+ // Project all vertices and normals through the camera
400
+ // Add position offset so sphere appears at correct world position
401
+ const projectedVertices = this.vertices.map((v) => {
402
+ const projected = this.camera.project(
403
+ v.x + (this.x || 0),
404
+ v.y + (this.y || 0),
405
+ v.z + (this.z || 0)
406
+ );
407
+
408
+ // Rotate normals using the same rotation sequence as Camera3D.project
409
+ // (Z, then Y, then X)
410
+ let nx = v.nx;
411
+ let ny = v.ny;
412
+ let nz = v.nz;
413
+
414
+ // Rotate around Z axis (roll)
415
+ if (this.camera.rotationZ !== 0) {
416
+ const cosZ = Math.cos(this.camera.rotationZ);
417
+ const sinZ = Math.sin(this.camera.rotationZ);
418
+ const nx0 = nx;
419
+ const ny0 = ny;
420
+ nx = nx0 * cosZ - ny0 * sinZ;
421
+ ny = nx0 * sinZ + ny0 * cosZ;
422
+ }
423
+
424
+ // Rotate around Y axis (horizontal spin)
425
+ const cosY = Math.cos(this.camera.rotationY);
426
+ const sinY = Math.sin(this.camera.rotationY);
427
+ const nx1 = nx * cosY - nz * sinY;
428
+ const nz1 = nx * sinY + nz * cosY;
429
+
430
+ // Rotate around X axis (vertical tilt)
431
+ const cosX = Math.cos(this.camera.rotationX);
432
+ const sinX = Math.sin(this.camera.rotationX);
433
+ const ny1 = ny * cosX - nz1 * sinX;
434
+ const nz2 = ny * sinX + nz1 * cosX;
435
+
436
+ return {
437
+ ...projected,
438
+ nx: nx1,
439
+ ny: ny1,
440
+ nz: nz2,
441
+ };
442
+ });
443
+
444
+ if (this.debug) {
445
+ this.trace("Sphere3D.draw: projected vertices", projectedVertices.length);
446
+ }
447
+
448
+ // Build face list with depth and lighting
449
+ const renderFaces = [];
450
+
451
+ for (const face of this.faces) {
452
+ const v0 = projectedVertices[face[0]];
453
+ const v1 = projectedVertices[face[1]];
454
+ const v2 = projectedVertices[face[2]];
455
+
456
+ // Skip if any vertex is behind camera
457
+ if (
458
+ v0.z < -this.camera.perspective + 10 ||
459
+ v1.z < -this.camera.perspective + 10 ||
460
+ v2.z < -this.camera.perspective + 10
461
+ ) {
462
+ continue;
463
+ }
464
+
465
+ // Calculate average depth for sorting
466
+ const avgZ = (v0.z + v1.z + v2.z) / 3;
467
+
468
+ // Calculate average normal for lighting and backface culling
469
+ const avgNx = (v0.nx + v1.nx + v2.nx) / 3;
470
+ const avgNy = (v0.ny + v1.ny + v2.ny) / 3;
471
+ const avgNz = (v0.nz + v1.nz + v2.nz) / 3;
472
+
473
+ // Backface culling: skip faces pointing away from camera
474
+ // In camera space after rotations, Z points into the screen (away from user).
475
+ // A face is visible if its view-space normal points towards the user (negative Z).
476
+ // Wait, Camera3D.project uses z2 = y * sinX + z1 * cosX; and scale = perspective / (perspective + z2)
477
+ // If z2 is positive, it's further away.
478
+ // So if normal.z is positive, it's pointing away from the user.
479
+ if (avgNz > 0.1) {
480
+ continue;
481
+ }
482
+
483
+ const intensity = this._calculateLighting(avgNx, avgNy, avgNz);
484
+
485
+ renderFaces.push({
486
+ vertices: [v0, v1, v2],
487
+ avgZ,
488
+ intensity,
489
+ });
490
+ }
491
+
492
+ // Sort back to front
493
+ renderFaces.sort((a, b) => b.avgZ - a.avgZ);
494
+
495
+ // Render faces
496
+ for (const face of renderFaces) {
497
+ const points = face.vertices.map((v) => ({ x: v.x, y: v.y }));
498
+
499
+ if (this.debug) {
500
+ // Wireframe mode
501
+ Painter.ctx.beginPath();
502
+ Painter.ctx.moveTo(points[0].x, points[0].y);
503
+ Painter.ctx.lineTo(points[1].x, points[1].y);
504
+ Painter.ctx.lineTo(points[2].x, points[2].y);
505
+ Painter.ctx.closePath();
506
+
507
+ if (this.stroke) {
508
+ Painter.ctx.strokeStyle = this.stroke;
509
+ Painter.ctx.lineWidth = this.lineWidth ?? 1;
510
+ Painter.ctx.stroke();
511
+ }
512
+ } else {
513
+ // Filled mode with lighting
514
+ if (this.color) {
515
+ const faceColor = this._applyLighting(this.color, face.intensity);
516
+
517
+ Painter.ctx.beginPath();
518
+ Painter.ctx.moveTo(points[0].x, points[0].y);
519
+ Painter.ctx.lineTo(points[1].x, points[1].y);
520
+ Painter.ctx.lineTo(points[2].x, points[2].y);
521
+ Painter.ctx.closePath();
522
+
523
+ Painter.ctx.fillStyle = faceColor;
524
+ Painter.ctx.fill();
525
+ }
526
+ }
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Calculate bounding box
532
+ */
533
+ calculateBounds() {
534
+ const size = this.radius * 2;
535
+ return { x: this.x, y: this.y, width: size, height: size };
536
+ }
537
+ }
@@ -0,0 +1,15 @@
1
+ import { Rectangle } from "./rect.js";
2
+ /**
3
+ * Square - A shortcut for creating rectangles with equal width/height.
4
+ */
5
+ export class Square extends Rectangle {
6
+ /**
7
+ * @param {number} size - Side length of the square
8
+ * @param {Object} [options] - Shape rendering options
9
+ */
10
+ constructor(size, options = {}) {
11
+ super(options);
12
+ this.width = size;
13
+ this.height = size;
14
+ }
15
+ }
@@ -0,0 +1,99 @@
1
+ import { Shape } from "./shape.js";
2
+ import { Painter } from "../painter/painter.js";
3
+
4
+ export class Star extends Shape {
5
+ constructor(radius = 40, spikes = 5, inset = 0.5, options = {}) {
6
+ super(options);
7
+ this.radius = radius;
8
+ this.spikes = spikes;
9
+ this.inset = inset;
10
+ }
11
+
12
+ draw() {
13
+ super.draw();
14
+ const step = Math.PI / this.spikes;
15
+ const rotationOffset = -Math.PI / 2;
16
+ // Render
17
+ Painter.lines.beginPath();
18
+ // Draw the star shape
19
+ for (let i = 0; i < this.spikes * 2; i++) {
20
+ const isOuter = i % 2 === 0;
21
+ const r = isOuter ? this.radius : this.radius * this.inset;
22
+ const angle = i * step + rotationOffset;
23
+ const x = Math.cos(angle) * r;
24
+ const y = Math.sin(angle) * r;
25
+ if (i === 0) {
26
+ Painter.lines.moveTo(x, y);
27
+ } else {
28
+ Painter.lines.lineTo(x, y);
29
+ }
30
+ }
31
+ // Close the path
32
+ Painter.lines.closePath();
33
+ // Fill
34
+ if (this.color) {
35
+ Painter.colors.fill(this.color);
36
+ }
37
+ // Stroke
38
+ if (this.stroke) {
39
+ Painter.colors.stroke(this.stroke, this.lineWidth);
40
+ }
41
+ }
42
+
43
+ /* draw() {
44
+ // First apply transforms as usual
45
+ super.draw();
46
+
47
+ // Get the current transform matrix to preserve positioning
48
+ const transform = Painter.ctx.getTransform();
49
+
50
+ // Create a temporary off-screen canvas sized to fit the star
51
+ const tempCanvas = document.createElement('canvas');
52
+ const padding = Math.ceil(this.radius * 0.1) + 2; // Small padding
53
+ tempCanvas.width = this.radius * 2 + padding * 2;
54
+ tempCanvas.height = this.radius * 2 + padding * 2;
55
+ const tempCtx = tempCanvas.getContext('2d');
56
+
57
+ // Center the drawing in the temp canvas
58
+ tempCtx.translate(tempCanvas.width/2, tempCanvas.height/2);
59
+
60
+ // Draw the star with clean state
61
+ tempCtx.beginPath();
62
+ const step = Math.PI / this.spikes;
63
+ const rotationOffset = -Math.PI / 2;
64
+
65
+ for (let i = 0; i < this.spikes * 2; i++) {
66
+ const isOuter = i % 2 === 0;
67
+ const r = isOuter ? this.radius : this.radius * this.inset;
68
+ const angle = i * step + rotationOffset;
69
+ const x = Math.cos(angle) * r;
70
+ const y = Math.sin(angle) * r;
71
+
72
+ if (i === 0) {
73
+ tempCtx.moveTo(x, y);
74
+ } else {
75
+ tempCtx.lineTo(x, y);
76
+ }
77
+ }
78
+
79
+ tempCtx.closePath();
80
+
81
+ // Fill with explicit color
82
+ tempCtx.fillStyle = this.color || 'white';
83
+ tempCtx.fill();
84
+
85
+ // Stroke if needed
86
+ if (this.stroke) {
87
+ tempCtx.strokeStyle = this.stroke;
88
+ tempCtx.lineWidth = this.lineWidth;
89
+ tempCtx.stroke();
90
+ }
91
+
92
+ // Now draw this to the main canvas at the correct position
93
+ Painter.ctx.drawImage(
94
+ tempCanvas,
95
+ -tempCanvas.width/2,
96
+ -tempCanvas.height/2
97
+ );
98
+ } */
99
+ }