@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,438 @@
1
+ /**
2
+ * Camera3D - Pseudo-3D projection and mouse-controlled rotation
3
+ *
4
+ * Provides 3D to 2D projection with perspective, rotation controls,
5
+ * and interactive mouse/touch rotation for 2D canvas applications.
6
+ *
7
+ * Supports inertia for smooth, physics-based camera movement.
8
+ *
9
+ * @example
10
+ * // Create camera with initial settings
11
+ * const camera = new Camera3D({
12
+ * rotationX: 0.3,
13
+ * rotationY: -0.4,
14
+ * perspective: 800,
15
+ * inertia: true, // Enable inertia
16
+ * friction: 0.92 // Velocity decay (0.9 = fast stop, 0.98 = slow drift)
17
+ * });
18
+ *
19
+ * // Enable mouse drag rotation
20
+ * camera.enableMouseControl(canvas);
21
+ *
22
+ * // In render loop - project 3D points to 2D
23
+ * const { x, y, scale, z } = camera.project(x3d, y3d, z3d);
24
+ *
25
+ * // Draw at projected position (centered on screen)
26
+ * ctx.fillRect(centerX + x, centerY + y, 10 * scale, 10 * scale);
27
+ */
28
+ export class Camera3D {
29
+ /**
30
+ * Create a new Camera3D instance
31
+ * @param {object} options - Configuration options
32
+ * @param {number} [options.rotationX=0] - Initial X rotation (tilt up/down) in radians
33
+ * @param {number} [options.rotationY=0] - Initial Y rotation (spin left/right) in radians
34
+ * @param {number} [options.rotationZ=0] - Initial Z rotation (roll) in radians
35
+ * @param {number} [options.perspective=800] - Perspective distance (higher = less distortion)
36
+ * @param {number} [options.sensitivity=0.005] - Mouse drag sensitivity
37
+ * @param {number} [options.minRotationX=-1.5] - Minimum X rotation limit
38
+ * @param {number} [options.maxRotationX=1.5] - Maximum X rotation limit
39
+ * @param {boolean} [options.clampX=true] - Whether to clamp X rotation
40
+ * @param {boolean} [options.autoRotate=false] - Enable auto-rotation
41
+ * @param {number} [options.autoRotateSpeed=0.5] - Auto-rotation speed (radians per second)
42
+ * @param {boolean} [options.inertia=false] - Enable inertia (momentum after release)
43
+ * @param {number} [options.friction=0.92] - Velocity decay per frame (0.9 = fast stop, 0.98 = slow drift)
44
+ * @param {number} [options.velocityScale=1.0] - Multiplier for initial throw velocity
45
+ */
46
+ constructor(options = {}) {
47
+ // Rotation state
48
+ this.rotationX = options.rotationX ?? 0;
49
+ this.rotationY = options.rotationY ?? 0;
50
+ this.rotationZ = options.rotationZ ?? 0;
51
+
52
+ // Store initial values for reset
53
+ this._initialRotationX = this.rotationX;
54
+ this._initialRotationY = this.rotationY;
55
+ this._initialRotationZ = this.rotationZ;
56
+
57
+ // Perspective
58
+ this.perspective = options.perspective ?? 800;
59
+
60
+ // Mouse control settings
61
+ this.sensitivity = options.sensitivity ?? 0.005;
62
+ this.minRotationX = options.minRotationX ?? -1.5;
63
+ this.maxRotationX = options.maxRotationX ?? 1.5;
64
+ this.clampX = options.clampX ?? true;
65
+
66
+ // Auto-rotate
67
+ this.autoRotate = options.autoRotate ?? false;
68
+ this.autoRotateSpeed = options.autoRotateSpeed ?? 0.5;
69
+ this.autoRotateAxis = options.autoRotateAxis ?? 'y'; // 'x', 'y', or 'z'
70
+
71
+ // Inertia settings
72
+ this.inertia = options.inertia ?? false;
73
+ this.friction = options.friction ?? 0.92;
74
+ this.velocityScale = options.velocityScale ?? 1.0;
75
+
76
+ // Velocity state for inertia
77
+ this._velocityX = 0;
78
+ this._velocityY = 0;
79
+ this._lastDeltaX = 0;
80
+ this._lastDeltaY = 0;
81
+ this._lastMoveTime = 0;
82
+
83
+ // Internal state for mouse control
84
+ this._isDragging = false;
85
+ this._lastMouseX = 0;
86
+ this._lastMouseY = 0;
87
+ this._canvas = null;
88
+ this._boundHandlers = null;
89
+ }
90
+
91
+ /**
92
+ * Project a 3D point to 2D screen coordinates
93
+ * @param {number} x - X coordinate in 3D space
94
+ * @param {number} y - Y coordinate in 3D space
95
+ * @param {number} z - Z coordinate in 3D space
96
+ * @returns {{x: number, y: number, z: number, scale: number}} Projected 2D coordinates and depth info
97
+ */
98
+ project(x, y, z) {
99
+ // Rotate around Z axis (roll)
100
+ if (this.rotationZ !== 0) {
101
+ const cosZ = Math.cos(this.rotationZ);
102
+ const sinZ = Math.sin(this.rotationZ);
103
+ const x0 = x;
104
+ const y0 = y;
105
+ x = x0 * cosZ - y0 * sinZ;
106
+ y = x0 * sinZ + y0 * cosZ;
107
+ }
108
+
109
+ // Rotate around Y axis (horizontal spin)
110
+ const cosY = Math.cos(this.rotationY);
111
+ const sinY = Math.sin(this.rotationY);
112
+ const x1 = x * cosY - z * sinY;
113
+ const z1 = x * sinY + z * cosY;
114
+
115
+ // Rotate around X axis (vertical tilt)
116
+ const cosX = Math.cos(this.rotationX);
117
+ const sinX = Math.sin(this.rotationX);
118
+ const y1 = y * cosX - z1 * sinX;
119
+ const z2 = y * sinX + z1 * cosX;
120
+
121
+ // Perspective projection
122
+ const scale = this.perspective / (this.perspective + z2);
123
+ const screenX = x1 * scale;
124
+ const screenY = y1 * scale;
125
+
126
+ return {
127
+ x: screenX,
128
+ y: screenY,
129
+ z: z2, // Depth (for sorting)
130
+ scale: scale // Scale factor (for size adjustment)
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Project multiple points at once
136
+ * @param {Array<{x: number, y: number, z: number}>} points - Array of 3D points
137
+ * @returns {Array<{x: number, y: number, z: number, scale: number}>} Array of projected points
138
+ */
139
+ projectAll(points) {
140
+ return points.map(p => this.project(p.x, p.y, p.z));
141
+ }
142
+
143
+ /**
144
+ * Update camera for auto-rotation and inertia (call in update loop)
145
+ * @param {number} dt - Delta time in seconds
146
+ */
147
+ update(dt) {
148
+ // Apply inertia when not dragging
149
+ if (this.inertia && !this._isDragging) {
150
+ // Apply velocity to rotation
151
+ if (Math.abs(this._velocityX) > 0.0001 || Math.abs(this._velocityY) > 0.0001) {
152
+ this.rotationY += this._velocityY;
153
+ this.rotationX += this._velocityX;
154
+
155
+ // Clamp X rotation
156
+ if (this.clampX) {
157
+ this.rotationX = Math.max(this.minRotationX, Math.min(this.maxRotationX, this.rotationX));
158
+ }
159
+
160
+ // Apply friction (exponential decay)
161
+ this._velocityX *= this.friction;
162
+ this._velocityY *= this.friction;
163
+
164
+ // Stop if velocity is negligible
165
+ if (Math.abs(this._velocityX) < 0.0001) this._velocityX = 0;
166
+ if (Math.abs(this._velocityY) < 0.0001) this._velocityY = 0;
167
+ }
168
+ }
169
+
170
+ // Auto-rotate when not dragging and no significant velocity
171
+ if (this.autoRotate && !this._isDragging) {
172
+ const hasVelocity = Math.abs(this._velocityX) > 0.001 || Math.abs(this._velocityY) > 0.001;
173
+ if (!hasVelocity) {
174
+ const delta = this.autoRotateSpeed * dt;
175
+ switch (this.autoRotateAxis) {
176
+ case 'x':
177
+ this.rotationX += delta;
178
+ break;
179
+ case 'y':
180
+ this.rotationY += delta;
181
+ break;
182
+ case 'z':
183
+ this.rotationZ += delta;
184
+ break;
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Enable mouse/touch drag rotation on a canvas
192
+ * @param {HTMLCanvasElement} canvas - The canvas element to attach controls to
193
+ * @param {object} [options] - Control options
194
+ * @param {boolean} [options.invertX=false] - Invert horizontal rotation
195
+ * @param {boolean} [options.invertY=false] - Invert vertical rotation
196
+ * @returns {Camera3D} Returns this for chaining
197
+ */
198
+ enableMouseControl(canvas, options = {}) {
199
+ if (this._canvas) {
200
+ this.disableMouseControl();
201
+ }
202
+
203
+ this._canvas = canvas;
204
+ const invertX = options.invertX ? -1 : 1;
205
+ const invertY = options.invertY ? -1 : 1;
206
+
207
+ // Create bound handlers so we can remove them later
208
+ this._boundHandlers = {
209
+ mousedown: (e) => {
210
+ this._isDragging = true;
211
+ this._lastMouseX = e.clientX;
212
+ this._lastMouseY = e.clientY;
213
+ this._lastMoveTime = performance.now();
214
+ // Stop any existing inertia
215
+ this._velocityX = 0;
216
+ this._velocityY = 0;
217
+ },
218
+
219
+ mousemove: (e) => {
220
+ if (!this._isDragging) return;
221
+
222
+ const deltaX = e.clientX - this._lastMouseX;
223
+ const deltaY = e.clientY - this._lastMouseY;
224
+
225
+ const scaledDeltaX = deltaX * this.sensitivity * invertX;
226
+ const scaledDeltaY = deltaY * this.sensitivity * invertY;
227
+
228
+ this.rotationY += scaledDeltaX;
229
+ this.rotationX += scaledDeltaY;
230
+
231
+ if (this.clampX) {
232
+ this.rotationX = Math.max(this.minRotationX, Math.min(this.maxRotationX, this.rotationX));
233
+ }
234
+
235
+ // Track velocity for inertia (store last delta)
236
+ if (this.inertia) {
237
+ this._lastDeltaX = scaledDeltaY; // X rotation from Y mouse movement
238
+ this._lastDeltaY = scaledDeltaX; // Y rotation from X mouse movement
239
+ this._lastMoveTime = performance.now();
240
+ }
241
+
242
+ this._lastMouseX = e.clientX;
243
+ this._lastMouseY = e.clientY;
244
+ },
245
+
246
+ mouseup: () => {
247
+ // Transfer last delta to velocity for inertia throw
248
+ if (this.inertia && this._isDragging) {
249
+ const timeSinceMove = performance.now() - this._lastMoveTime;
250
+ // Only apply inertia if the release was quick (within 50ms of last move)
251
+ if (timeSinceMove < 50) {
252
+ this._velocityX = this._lastDeltaX * this.velocityScale;
253
+ this._velocityY = this._lastDeltaY * this.velocityScale;
254
+ }
255
+ }
256
+ this._isDragging = false;
257
+ },
258
+
259
+ mouseleave: () => {
260
+ // Apply inertia on mouseleave too
261
+ if (this.inertia && this._isDragging) {
262
+ const timeSinceMove = performance.now() - this._lastMoveTime;
263
+ if (timeSinceMove < 50) {
264
+ this._velocityX = this._lastDeltaX * this.velocityScale;
265
+ this._velocityY = this._lastDeltaY * this.velocityScale;
266
+ }
267
+ }
268
+ this._isDragging = false;
269
+ },
270
+
271
+ touchstart: (e) => {
272
+ if (e.touches.length === 1) {
273
+ this._isDragging = true;
274
+ this._lastMouseX = e.touches[0].clientX;
275
+ this._lastMouseY = e.touches[0].clientY;
276
+ this._lastMoveTime = performance.now();
277
+ // Stop any existing inertia
278
+ this._velocityX = 0;
279
+ this._velocityY = 0;
280
+ }
281
+ },
282
+
283
+ touchmove: (e) => {
284
+ if (!this._isDragging || e.touches.length !== 1) return;
285
+ e.preventDefault();
286
+
287
+ const deltaX = e.touches[0].clientX - this._lastMouseX;
288
+ const deltaY = e.touches[0].clientY - this._lastMouseY;
289
+
290
+ const scaledDeltaX = deltaX * this.sensitivity * invertX;
291
+ const scaledDeltaY = deltaY * this.sensitivity * invertY;
292
+
293
+ this.rotationY += scaledDeltaX;
294
+ this.rotationX += scaledDeltaY;
295
+
296
+ if (this.clampX) {
297
+ this.rotationX = Math.max(this.minRotationX, Math.min(this.maxRotationX, this.rotationX));
298
+ }
299
+
300
+ // Track velocity for inertia
301
+ if (this.inertia) {
302
+ this._lastDeltaX = scaledDeltaY;
303
+ this._lastDeltaY = scaledDeltaX;
304
+ this._lastMoveTime = performance.now();
305
+ }
306
+
307
+ this._lastMouseX = e.touches[0].clientX;
308
+ this._lastMouseY = e.touches[0].clientY;
309
+ },
310
+
311
+ touchend: () => {
312
+ // Transfer last delta to velocity for inertia throw
313
+ if (this.inertia && this._isDragging) {
314
+ const timeSinceMove = performance.now() - this._lastMoveTime;
315
+ if (timeSinceMove < 50) {
316
+ this._velocityX = this._lastDeltaX * this.velocityScale;
317
+ this._velocityY = this._lastDeltaY * this.velocityScale;
318
+ }
319
+ }
320
+ this._isDragging = false;
321
+ },
322
+
323
+ dblclick: () => {
324
+ this.reset();
325
+ }
326
+ };
327
+
328
+ // Attach all event listeners
329
+ canvas.addEventListener('mousedown', this._boundHandlers.mousedown);
330
+ canvas.addEventListener('mousemove', this._boundHandlers.mousemove);
331
+ canvas.addEventListener('mouseup', this._boundHandlers.mouseup);
332
+ canvas.addEventListener('mouseleave', this._boundHandlers.mouseleave);
333
+ canvas.addEventListener('touchstart', this._boundHandlers.touchstart);
334
+ canvas.addEventListener('touchmove', this._boundHandlers.touchmove, { passive: false });
335
+ canvas.addEventListener('touchend', this._boundHandlers.touchend);
336
+ canvas.addEventListener('dblclick', this._boundHandlers.dblclick);
337
+
338
+ return this;
339
+ }
340
+
341
+ /**
342
+ * Disable mouse/touch controls
343
+ * @returns {Camera3D} Returns this for chaining
344
+ */
345
+ disableMouseControl() {
346
+ if (this._canvas && this._boundHandlers) {
347
+ this._canvas.removeEventListener('mousedown', this._boundHandlers.mousedown);
348
+ this._canvas.removeEventListener('mousemove', this._boundHandlers.mousemove);
349
+ this._canvas.removeEventListener('mouseup', this._boundHandlers.mouseup);
350
+ this._canvas.removeEventListener('mouseleave', this._boundHandlers.mouseleave);
351
+ this._canvas.removeEventListener('touchstart', this._boundHandlers.touchstart);
352
+ this._canvas.removeEventListener('touchmove', this._boundHandlers.touchmove);
353
+ this._canvas.removeEventListener('touchend', this._boundHandlers.touchend);
354
+ this._canvas.removeEventListener('dblclick', this._boundHandlers.dblclick);
355
+ }
356
+
357
+ this._canvas = null;
358
+ this._boundHandlers = null;
359
+ return this;
360
+ }
361
+
362
+ /**
363
+ * Reset rotation to initial values and stop inertia
364
+ * @returns {Camera3D} Returns this for chaining
365
+ */
366
+ reset() {
367
+ this.rotationX = this._initialRotationX;
368
+ this.rotationY = this._initialRotationY;
369
+ this.rotationZ = this._initialRotationZ;
370
+ this._velocityX = 0;
371
+ this._velocityY = 0;
372
+ return this;
373
+ }
374
+
375
+ /**
376
+ * Stop any inertia motion immediately
377
+ * @returns {Camera3D} Returns this for chaining
378
+ */
379
+ stopInertia() {
380
+ this._velocityX = 0;
381
+ this._velocityY = 0;
382
+ return this;
383
+ }
384
+
385
+ /**
386
+ * Set rotation angles
387
+ * @param {number} x - X rotation in radians
388
+ * @param {number} y - Y rotation in radians
389
+ * @param {number} [z=0] - Z rotation in radians
390
+ * @returns {Camera3D} Returns this for chaining
391
+ */
392
+ setRotation(x, y, z = 0) {
393
+ this.rotationX = x;
394
+ this.rotationY = y;
395
+ this.rotationZ = z;
396
+ return this;
397
+ }
398
+
399
+ /**
400
+ * Add to current rotation
401
+ * @param {number} dx - Delta X rotation in radians
402
+ * @param {number} dy - Delta Y rotation in radians
403
+ * @param {number} [dz=0] - Delta Z rotation in radians
404
+ * @returns {Camera3D} Returns this for chaining
405
+ */
406
+ rotate(dx, dy, dz = 0) {
407
+ this.rotationX += dx;
408
+ this.rotationY += dy;
409
+ this.rotationZ += dz;
410
+
411
+ if (this.clampX) {
412
+ this.rotationX = Math.max(this.minRotationX, Math.min(this.maxRotationX, this.rotationX));
413
+ }
414
+
415
+ return this;
416
+ }
417
+
418
+ /**
419
+ * Check if currently being dragged by user
420
+ * @returns {boolean} True if user is dragging
421
+ */
422
+ isDragging() {
423
+ return this._isDragging;
424
+ }
425
+
426
+ /**
427
+ * Look at a specific point (sets rotation to face that direction)
428
+ * @param {number} x - Target X
429
+ * @param {number} y - Target Y
430
+ * @param {number} z - Target Z
431
+ * @returns {Camera3D} Returns this for chaining
432
+ */
433
+ lookAt(x, y, z) {
434
+ this.rotationY = Math.atan2(x, z);
435
+ this.rotationX = Math.atan2(y, Math.sqrt(x * x + z * z));
436
+ return this;
437
+ }
438
+ }
@@ -0,0 +1,6 @@
1
+ export { ZOrderedCollection } from "./zindex";
2
+ export * from "./position";
3
+ export * from "./layout";
4
+ export { TaskManager } from "./tasks";
5
+ export { Camera3D } from "./camera3d";
6
+ export { IsometricCamera } from "./isometric-camera";
@@ -0,0 +1,235 @@
1
+ /**
2
+ * IsometricCamera - Camera for isometric views with step-based rotation
3
+ *
4
+ * Unlike Camera3D which provides free 3D rotation, IsometricCamera is designed
5
+ * for isometric games where the view rotates in fixed increments (45° or 90°).
6
+ *
7
+ * Features:
8
+ * - Rotate view in fixed steps (default 45°)
9
+ * - Smooth animated transitions between angles
10
+ * - Maintains isometric perspective (no distortion)
11
+ * - Easy integration with IsometricScene
12
+ *
13
+ * @example
14
+ * // Create camera with 45° rotation steps
15
+ * const camera = new IsometricCamera({
16
+ * rotationStep: Math.PI / 4, // 45 degrees
17
+ * animationDuration: 0.5 // 500ms transition
18
+ * });
19
+ *
20
+ * // Rotate the view
21
+ * camera.rotateRight(); // Animate to next 45° position
22
+ * camera.rotateLeft(); // Animate to previous 45° position
23
+ *
24
+ * // In update loop
25
+ * camera.update(dt);
26
+ *
27
+ * // Use angle in projection
28
+ * const projected = scene.toIsometric(x, y, z, camera.angle);
29
+ */
30
+ export class IsometricCamera {
31
+ /**
32
+ * Create an IsometricCamera instance
33
+ * @param {object} options - Configuration options
34
+ * @param {number} [options.angle=0] - Initial viewing angle in radians
35
+ * @param {number} [options.rotationStep=Math.PI/2] - Rotation step size (default 90°)
36
+ * @param {number} [options.animationDuration=0.4] - Transition duration in seconds
37
+ * @param {string} [options.easing='easeInOutCubic'] - Easing function name
38
+ */
39
+ constructor(options = {}) {
40
+ /** Current viewing angle in radians */
41
+ this.angle = options.angle ?? 0;
42
+
43
+ /** Target angle for animation */
44
+ this._targetAngle = this.angle;
45
+
46
+ /** Rotation step size in radians (default 90°) */
47
+ this.rotationStep = options.rotationStep ?? Math.PI / 2;
48
+
49
+ /** Animation duration in seconds */
50
+ this.animationDuration = options.animationDuration ?? 0.4;
51
+
52
+ /** Easing function type */
53
+ this.easingType = options.easing ?? 'easeInOutCubic';
54
+
55
+ /** Animation state */
56
+ this._animating = false;
57
+ this._animationProgress = 0;
58
+ this._startAngle = 0;
59
+
60
+ /** Callbacks */
61
+ this._onRotationStart = null;
62
+ this._onRotationEnd = null;
63
+ }
64
+
65
+ /**
66
+ * Rotate view to the right (clockwise) by one step
67
+ * @returns {IsometricCamera} this for chaining
68
+ */
69
+ rotateRight() {
70
+ if (this._animating) return this;
71
+ this._startRotation(this._targetAngle + this.rotationStep);
72
+ return this;
73
+ }
74
+
75
+ /**
76
+ * Rotate view to the left (counter-clockwise) by one step
77
+ * @returns {IsometricCamera} this for chaining
78
+ */
79
+ rotateLeft() {
80
+ if (this._animating) return this;
81
+ this._startRotation(this._targetAngle - this.rotationStep);
82
+ return this;
83
+ }
84
+
85
+ /**
86
+ * Rotate to a specific angle (animated)
87
+ * @param {number} angle - Target angle in radians
88
+ * @returns {IsometricCamera} this for chaining
89
+ */
90
+ rotateTo(angle) {
91
+ if (this._animating) return this;
92
+ this._startRotation(angle);
93
+ return this;
94
+ }
95
+
96
+ /**
97
+ * Rotate to a specific angle immediately (no animation)
98
+ * @param {number} angle - Target angle in radians
99
+ * @returns {IsometricCamera} this for chaining
100
+ */
101
+ setAngle(angle) {
102
+ this.angle = angle;
103
+ this._targetAngle = angle;
104
+ this._animating = false;
105
+ return this;
106
+ }
107
+
108
+ /**
109
+ * Start rotation animation
110
+ * @param {number} targetAngle - Target angle
111
+ * @private
112
+ */
113
+ _startRotation(targetAngle) {
114
+ this._startAngle = this.angle;
115
+ this._targetAngle = targetAngle;
116
+ this._animationProgress = 0;
117
+ this._animating = true;
118
+
119
+ if (this._onRotationStart) {
120
+ this._onRotationStart(this._startAngle, this._targetAngle);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Update camera animation (call each frame)
126
+ * @param {number} dt - Delta time in seconds
127
+ */
128
+ update(dt) {
129
+ if (!this._animating) return;
130
+
131
+ // Advance animation progress
132
+ this._animationProgress += dt / this.animationDuration;
133
+
134
+ if (this._animationProgress >= 1) {
135
+ // Animation complete
136
+ this._animationProgress = 1;
137
+ this.angle = this._targetAngle;
138
+ this._animating = false;
139
+
140
+ if (this._onRotationEnd) {
141
+ this._onRotationEnd(this.angle);
142
+ }
143
+ } else {
144
+ // Interpolate angle using easing
145
+ const t = this._ease(this._animationProgress);
146
+ this.angle = this._startAngle + (this._targetAngle - this._startAngle) * t;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Apply easing function to progress value
152
+ * @param {number} t - Progress 0-1
153
+ * @returns {number} Eased value 0-1
154
+ * @private
155
+ */
156
+ _ease(t) {
157
+ switch (this.easingType) {
158
+ case 'linear':
159
+ return t;
160
+ case 'easeInQuad':
161
+ return t * t;
162
+ case 'easeOutQuad':
163
+ return t * (2 - t);
164
+ case 'easeInOutQuad':
165
+ return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
166
+ case 'easeInCubic':
167
+ return t * t * t;
168
+ case 'easeOutCubic':
169
+ return (--t) * t * t + 1;
170
+ case 'easeInOutCubic':
171
+ return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
172
+ case 'easeOutBack':
173
+ const c1 = 1.70158;
174
+ const c3 = c1 + 1;
175
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
176
+ default:
177
+ return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Check if camera is currently animating
183
+ * @returns {boolean} True if animating
184
+ */
185
+ isAnimating() {
186
+ return this._animating;
187
+ }
188
+
189
+ /**
190
+ * Get the current angle in degrees
191
+ * @returns {number} Angle in degrees
192
+ */
193
+ getAngleDegrees() {
194
+ return (this.angle * 180 / Math.PI) % 360;
195
+ }
196
+
197
+ /**
198
+ * Get normalized angle (0 to 2π)
199
+ * @returns {number} Normalized angle in radians
200
+ */
201
+ getNormalizedAngle() {
202
+ let normalized = this.angle % (Math.PI * 2);
203
+ if (normalized < 0) normalized += Math.PI * 2;
204
+ return normalized;
205
+ }
206
+
207
+ /**
208
+ * Set callback for rotation start
209
+ * @param {Function} callback - Called when rotation starts (startAngle, targetAngle)
210
+ * @returns {IsometricCamera} this for chaining
211
+ */
212
+ onRotationStart(callback) {
213
+ this._onRotationStart = callback;
214
+ return this;
215
+ }
216
+
217
+ /**
218
+ * Set callback for rotation end
219
+ * @param {Function} callback - Called when rotation completes (finalAngle)
220
+ * @returns {IsometricCamera} this for chaining
221
+ */
222
+ onRotationEnd(callback) {
223
+ this._onRotationEnd = callback;
224
+ return this;
225
+ }
226
+
227
+ /**
228
+ * Reset camera to initial angle
229
+ * @returns {IsometricCamera} this for chaining
230
+ */
231
+ reset() {
232
+ this.setAngle(0);
233
+ return this;
234
+ }
235
+ }