@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.
- package/.github/workflows/release.yaml +70 -0
- package/.jshintrc +4 -0
- package/.vscode/settings.json +22 -0
- package/CLAUDE.md +310 -0
- package/blackhole.jpg +0 -0
- package/demo.png +0 -0
- package/demos/CNAME +1 -0
- package/demos/animations.html +31 -0
- package/demos/basic.html +38 -0
- package/demos/baskara.html +31 -0
- package/demos/bezier.html +35 -0
- package/demos/beziersignature.html +29 -0
- package/demos/blackhole.html +28 -0
- package/demos/blob.html +35 -0
- package/demos/demos.css +289 -0
- package/demos/easing.html +28 -0
- package/demos/events.html +195 -0
- package/demos/fluent.html +647 -0
- package/demos/fractals.html +36 -0
- package/demos/genart.html +26 -0
- package/demos/gendream.html +26 -0
- package/demos/group.html +36 -0
- package/demos/home.html +587 -0
- package/demos/index.html +364 -0
- package/demos/isometric.html +34 -0
- package/demos/js/animations.js +452 -0
- package/demos/js/basic.js +204 -0
- package/demos/js/baskara.js +751 -0
- package/demos/js/bezier.js +692 -0
- package/demos/js/beziersignature.js +241 -0
- package/demos/js/blackhole/accretiondisk.obj.js +379 -0
- package/demos/js/blackhole/blackhole.obj.js +318 -0
- package/demos/js/blackhole/index.js +409 -0
- package/demos/js/blackhole/particle.js +56 -0
- package/demos/js/blackhole/starfield.obj.js +218 -0
- package/demos/js/blob.js +2263 -0
- package/demos/js/easing.js +477 -0
- package/demos/js/fluent.js +183 -0
- package/demos/js/fractals.js +931 -0
- package/demos/js/fractalworker.js +93 -0
- package/demos/js/genart.js +268 -0
- package/demos/js/gendream.js +209 -0
- package/demos/js/group.js +140 -0
- package/demos/js/info-toggle.js +25 -0
- package/demos/js/isometric.js +863 -0
- package/demos/js/kerr.js +1556 -0
- package/demos/js/lavalamp.js +590 -0
- package/demos/js/layout.js +354 -0
- package/demos/js/mondrian.js +285 -0
- package/demos/js/opacity.js +275 -0
- package/demos/js/painter.js +484 -0
- package/demos/js/particles-showcase.js +514 -0
- package/demos/js/particles.js +299 -0
- package/demos/js/patterns.js +397 -0
- package/demos/js/penrose/artifact.js +69 -0
- package/demos/js/penrose/blackhole.js +121 -0
- package/demos/js/penrose/constants.js +73 -0
- package/demos/js/penrose/game.js +943 -0
- package/demos/js/penrose/lore.js +278 -0
- package/demos/js/penrose/penrosescene.js +892 -0
- package/demos/js/penrose/ship.js +216 -0
- package/demos/js/penrose/sounds.js +211 -0
- package/demos/js/penrose/voidparticle.js +55 -0
- package/demos/js/penrose/voidscene.js +258 -0
- package/demos/js/penrose/voidship.js +144 -0
- package/demos/js/penrose/wormhole.js +46 -0
- package/demos/js/pipeline.js +555 -0
- package/demos/js/scene.js +304 -0
- package/demos/js/scenes.js +320 -0
- package/demos/js/schrodinger.js +410 -0
- package/demos/js/schwarzschild.js +1023 -0
- package/demos/js/shapes.js +628 -0
- package/demos/js/space/alien.js +171 -0
- package/demos/js/space/boom.js +98 -0
- package/demos/js/space/boss.js +353 -0
- package/demos/js/space/buff.js +73 -0
- package/demos/js/space/bullet.js +102 -0
- package/demos/js/space/constants.js +85 -0
- package/demos/js/space/game.js +1884 -0
- package/demos/js/space/hud.js +112 -0
- package/demos/js/space/laserbeam.js +179 -0
- package/demos/js/space/lightning.js +277 -0
- package/demos/js/space/minion.js +192 -0
- package/demos/js/space/missile.js +212 -0
- package/demos/js/space/player.js +430 -0
- package/demos/js/space/powerup.js +90 -0
- package/demos/js/space/starfield.js +58 -0
- package/demos/js/space/starpower.js +90 -0
- package/demos/js/spacetime.js +559 -0
- package/demos/js/svgtween.js +204 -0
- package/demos/js/tde/accretiondisk.js +418 -0
- package/demos/js/tde/blackhole.js +219 -0
- package/demos/js/tde/blackholescene.js +209 -0
- package/demos/js/tde/config.js +59 -0
- package/demos/js/tde/index.js +695 -0
- package/demos/js/tde/jets.js +290 -0
- package/demos/js/tde/lensedstarfield.js +147 -0
- package/demos/js/tde/tdestar.js +317 -0
- package/demos/js/tde/tidalstream.js +356 -0
- package/demos/js/tde_old/blackhole.obj.js +354 -0
- package/demos/js/tde_old/debris.obj.js +791 -0
- package/demos/js/tde_old/flare.obj.js +239 -0
- package/demos/js/tde_old/index.js +448 -0
- package/demos/js/tde_old/star.obj.js +812 -0
- package/demos/js/tiles.js +312 -0
- package/demos/js/tweendemo.js +79 -0
- package/demos/js/visibility.js +102 -0
- package/demos/kerr.html +28 -0
- package/demos/lavalamp.html +27 -0
- package/demos/layouts.html +37 -0
- package/demos/logo.svg +4 -0
- package/demos/loop.html +84 -0
- package/demos/mondrian.html +32 -0
- package/demos/og_image.png +0 -0
- package/demos/opacity.html +36 -0
- package/demos/painter.html +39 -0
- package/demos/particles-showcase.html +28 -0
- package/demos/particles.html +24 -0
- package/demos/patterns.html +33 -0
- package/demos/penrose-game.html +31 -0
- package/demos/pipeline.html +737 -0
- package/demos/scene.html +33 -0
- package/demos/scenes.html +96 -0
- package/demos/schrodinger.html +27 -0
- package/demos/schwarzschild.html +27 -0
- package/demos/shapes.html +16 -0
- package/demos/space.html +85 -0
- package/demos/spacetime.html +27 -0
- package/demos/svgtween.html +29 -0
- package/demos/tde.html +28 -0
- package/demos/tiles.html +28 -0
- package/demos/transforms.html +400 -0
- package/demos/tween.html +45 -0
- package/demos/visibility.html +33 -0
- package/disk_example.png +0 -0
- package/docs/README.md +222 -0
- package/docs/concepts/architecture-overview.md +204 -0
- package/docs/concepts/lifecycle.md +255 -0
- package/docs/concepts/rendering-pipeline.md +279 -0
- package/docs/concepts/tde-zorder.md +106 -0
- package/docs/concepts/two-layer-architecture.md +229 -0
- package/docs/getting-started/first-game.md +354 -0
- package/docs/getting-started/hello-world.md +269 -0
- package/docs/getting-started/installation.md +157 -0
- package/docs/modules/collision/README.md +453 -0
- package/docs/modules/fluent/README.md +1075 -0
- package/docs/modules/game/README.md +303 -0
- package/docs/modules/isometric-camera.md +210 -0
- package/docs/modules/isometric.md +275 -0
- package/docs/modules/painter/README.md +328 -0
- package/docs/modules/particle/README.md +559 -0
- package/docs/modules/shapes/README.md +221 -0
- package/docs/modules/shapes/base/euclidian.md +123 -0
- package/docs/modules/shapes/base/geometry2d.md +204 -0
- package/docs/modules/shapes/base/renderable.md +215 -0
- package/docs/modules/shapes/base/shape.md +262 -0
- package/docs/modules/shapes/base/transformable.md +243 -0
- package/docs/modules/shapes/hierarchy.md +218 -0
- package/docs/modules/state/README.md +577 -0
- package/docs/modules/util/README.md +99 -0
- package/docs/modules/util/camera3d.md +412 -0
- package/docs/modules/util/scene3d.md +395 -0
- package/index.html +17 -0
- package/jsdoc.json +50 -0
- package/package.json +55 -0
- package/readme.md +599 -0
- package/scripts/build-demo.js +69 -0
- package/scripts/bundle4llm.js +276 -0
- package/scripts/clearconsole.js +48 -0
- package/src/collision/collision-system.js +332 -0
- package/src/collision/collision.js +303 -0
- package/src/collision/index.js +10 -0
- package/src/fluent/fluent-game.js +430 -0
- package/src/fluent/fluent-go.js +1060 -0
- package/src/fluent/fluent-layer.js +152 -0
- package/src/fluent/fluent-scene.js +291 -0
- package/src/fluent/index.js +98 -0
- package/src/fluent/sketch.js +380 -0
- package/src/game/game.js +467 -0
- package/src/game/index.js +49 -0
- package/src/game/objects/go.js +220 -0
- package/src/game/objects/imagego.js +30 -0
- package/src/game/objects/index.js +54 -0
- package/src/game/objects/isometric-scene.js +260 -0
- package/src/game/objects/layoutscene.js +549 -0
- package/src/game/objects/scene.js +175 -0
- package/src/game/objects/scene3d.js +118 -0
- package/src/game/objects/text.js +221 -0
- package/src/game/objects/wrapper.js +232 -0
- package/src/game/pipeline.js +243 -0
- package/src/game/ui/button.js +396 -0
- package/src/game/ui/cursor.js +93 -0
- package/src/game/ui/fps.js +91 -0
- package/src/game/ui/index.js +5 -0
- package/src/game/ui/togglebutton.js +93 -0
- package/src/game/ui/tooltip.js +249 -0
- package/src/index.js +25 -0
- package/src/io/events.js +20 -0
- package/src/io/index.js +86 -0
- package/src/io/input.js +70 -0
- package/src/io/keys.js +152 -0
- package/src/io/mouse.js +61 -0
- package/src/io/touch.js +39 -0
- package/src/logger/debugtab.js +138 -0
- package/src/logger/index.js +3 -0
- package/src/logger/loggable.js +47 -0
- package/src/logger/logger.js +113 -0
- package/src/math/complex.js +37 -0
- package/src/math/constants.js +1 -0
- package/src/math/fractal.js +1271 -0
- package/src/math/gr.js +201 -0
- package/src/math/heat.js +202 -0
- package/src/math/index.js +12 -0
- package/src/math/noise.js +433 -0
- package/src/math/orbital.js +191 -0
- package/src/math/patterns.js +1339 -0
- package/src/math/penrose.js +259 -0
- package/src/math/quantum.js +115 -0
- package/src/math/random.js +195 -0
- package/src/math/tensor.js +1009 -0
- package/src/mixins/anchor.js +131 -0
- package/src/mixins/draggable.js +72 -0
- package/src/mixins/index.js +2 -0
- package/src/motion/bezier.js +132 -0
- package/src/motion/bounce.js +58 -0
- package/src/motion/easing.js +349 -0
- package/src/motion/float.js +130 -0
- package/src/motion/follow.js +125 -0
- package/src/motion/hop.js +52 -0
- package/src/motion/index.js +82 -0
- package/src/motion/motion.js +1124 -0
- package/src/motion/orbit.js +49 -0
- package/src/motion/oscillate.js +39 -0
- package/src/motion/parabolic.js +141 -0
- package/src/motion/patrol.js +147 -0
- package/src/motion/pendulum.js +48 -0
- package/src/motion/pulse.js +88 -0
- package/src/motion/shake.js +83 -0
- package/src/motion/spiral.js +144 -0
- package/src/motion/spring.js +150 -0
- package/src/motion/swing.js +47 -0
- package/src/motion/tween.js +92 -0
- package/src/motion/tweenetik.js +139 -0
- package/src/motion/waypoint.js +210 -0
- package/src/painter/index.js +8 -0
- package/src/painter/painter.colors.js +331 -0
- package/src/painter/painter.effects.js +230 -0
- package/src/painter/painter.img.js +229 -0
- package/src/painter/painter.js +295 -0
- package/src/painter/painter.lines.js +189 -0
- package/src/painter/painter.opacity.js +41 -0
- package/src/painter/painter.shapes.js +277 -0
- package/src/painter/painter.text.js +273 -0
- package/src/particle/emitter.js +124 -0
- package/src/particle/index.js +11 -0
- package/src/particle/particle-system.js +322 -0
- package/src/particle/particle.js +71 -0
- package/src/particle/updaters.js +170 -0
- package/src/shapes/arc.js +43 -0
- package/src/shapes/arrow.js +33 -0
- package/src/shapes/bezier.js +42 -0
- package/src/shapes/circle.js +62 -0
- package/src/shapes/clouds.js +56 -0
- package/src/shapes/cone.js +219 -0
- package/src/shapes/cross.js +70 -0
- package/src/shapes/cube.js +244 -0
- package/src/shapes/cylinder.js +254 -0
- package/src/shapes/diamond.js +48 -0
- package/src/shapes/euclidian.js +111 -0
- package/src/shapes/figure.js +115 -0
- package/src/shapes/geometry.js +220 -0
- package/src/shapes/group.js +375 -0
- package/src/shapes/heart.js +42 -0
- package/src/shapes/hexagon.js +26 -0
- package/src/shapes/image.js +192 -0
- package/src/shapes/index.js +111 -0
- package/src/shapes/line.js +29 -0
- package/src/shapes/pattern.js +90 -0
- package/src/shapes/pin.js +44 -0
- package/src/shapes/poly.js +31 -0
- package/src/shapes/prism.js +226 -0
- package/src/shapes/rect.js +35 -0
- package/src/shapes/renderable.js +333 -0
- package/src/shapes/ring.js +26 -0
- package/src/shapes/roundrect.js +95 -0
- package/src/shapes/shape.js +117 -0
- package/src/shapes/slice.js +26 -0
- package/src/shapes/sphere.js +314 -0
- package/src/shapes/sphere3d.js +537 -0
- package/src/shapes/square.js +15 -0
- package/src/shapes/star.js +99 -0
- package/src/shapes/svg.js +408 -0
- package/src/shapes/text.js +553 -0
- package/src/shapes/traceable.js +83 -0
- package/src/shapes/transform.js +357 -0
- package/src/shapes/transformable.js +172 -0
- package/src/shapes/triangle.js +26 -0
- package/src/sound/index.js +17 -0
- package/src/sound/sound.js +473 -0
- package/src/sound/synth.analyzer.js +149 -0
- package/src/sound/synth.effects.js +207 -0
- package/src/sound/synth.envelope.js +59 -0
- package/src/sound/synth.js +229 -0
- package/src/sound/synth.musical.js +160 -0
- package/src/sound/synth.noise.js +85 -0
- package/src/sound/synth.oscillators.js +293 -0
- package/src/state/index.js +10 -0
- package/src/state/state-machine.js +371 -0
- package/src/util/camera3d.js +438 -0
- package/src/util/index.js +6 -0
- package/src/util/isometric-camera.js +235 -0
- package/src/util/layout.js +317 -0
- package/src/util/position.js +147 -0
- package/src/util/tasks.js +47 -0
- package/src/util/zindex.js +287 -0
- package/src/webgl/index.js +9 -0
- package/src/webgl/shaders/sphere-shaders.js +994 -0
- package/src/webgl/webgl-renderer.js +388 -0
- package/tde.png +0 -0
- package/test/math/orbital.test.js +61 -0
- package/test/math/tensor.test.js +114 -0
- package/test/particle/emitter.test.js +204 -0
- package/test/particle/particle-system.test.js +310 -0
- package/test/particle/particle.test.js +116 -0
- package/test/particle/updaters.test.js +386 -0
- package/test/setup.js +120 -0
- package/test/shapes/euclidian.test.js +44 -0
- package/test/shapes/geometry.test.js +86 -0
- package/test/shapes/group.test.js +86 -0
- package/test/shapes/rectangle.test.js +64 -0
- package/test/shapes/transform.test.js +379 -0
- package/test/util/camera3d.test.js +428 -0
- package/test/util/scene3d.test.js +352 -0
- package/types/collision.d.ts +249 -0
- package/types/common.d.ts +155 -0
- package/types/game.d.ts +497 -0
- package/types/index.d.ts +309 -0
- package/types/io.d.ts +188 -0
- package/types/logger.d.ts +127 -0
- package/types/math.d.ts +268 -0
- package/types/mixins.d.ts +92 -0
- package/types/motion.d.ts +678 -0
- package/types/painter.d.ts +378 -0
- package/types/shapes.d.ts +864 -0
- package/types/sound.d.ts +672 -0
- package/types/state.d.ts +251 -0
- package/types/util.d.ts +253 -0
- package/vite.config.js +50 -0
- 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,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
|
+
}
|