1+ import { MAX_ZOOM , MIN_ZOOM } from "./preferences.js" ;
2+
3+ function clamp ( value , min , max ) {
4+ return Math . max ( min , Math . min ( max , value ) ) ;
5+ }
6+
7+ export class CameraController {
8+ constructor ( options ) {
9+ this . getActiveCamera = options . getActiveCamera ;
10+ this . onCameraChanged = options . onCameraChanged ;
11+ this . canvas = null ;
12+ this . dragging = false ;
13+ this . lastPointer = null ;
14+ }
15+
16+ attachCanvas ( canvas ) {
17+ this . canvas = canvas ;
18+
19+ this . canvas . addEventListener ( "wheel" , event => this . onWheel ( event ) , { passive : false } ) ;
20+ this . canvas . addEventListener ( "pointerdown" , event => this . onPointerDown ( event ) ) ;
21+ this . canvas . addEventListener ( "pointermove" , event => this . onPointerMove ( event ) ) ;
22+ this . canvas . addEventListener ( "pointerup" , event => this . onPointerUp ( event ) ) ;
23+ this . canvas . addEventListener ( "pointercancel" , event => this . onPointerUp ( event ) ) ;
24+ this . canvas . addEventListener ( "dblclick" , event => {
25+ event . preventDefault ( ) ;
26+ this . resetView ( ) ;
27+ } ) ;
28+ }
29+
30+ onWheel ( event ) {
31+ if ( ! this . canvas ) {
32+ return ;
33+ }
34+
35+ event . preventDefault ( ) ;
36+ const pointer = this . getPointerPosition ( event ) ;
37+ if ( ! pointer ) {
38+ return ;
39+ }
40+
41+ const factor = event . deltaY < 0 ? 1.1 : 1 / 1.1 ;
42+ this . zoomAt ( pointer . x , pointer . y , factor ) ;
43+ this . onCameraChanged ( ) ;
44+ }
45+
46+ onPointerDown ( event ) {
47+ if ( ! this . canvas || event . button !== 0 ) {
48+ return ;
49+ }
50+
51+ const pointer = this . getPointerPosition ( event ) ;
52+ if ( ! pointer ) {
53+ return ;
54+ }
55+
56+ this . dragging = true ;
57+ this . lastPointer = pointer ;
58+ this . canvas . classList . add ( "is-panning" ) ;
59+ this . canvas . setPointerCapture ( event . pointerId ) ;
60+ event . preventDefault ( ) ;
61+ }
62+
63+ onPointerMove ( event ) {
64+ if ( ! this . canvas || ! this . dragging ) {
65+ return ;
66+ }
67+
68+ const pointer = this . getPointerPosition ( event ) ;
69+ if ( ! pointer || ! this . lastPointer ) {
70+ return ;
71+ }
72+
73+ const dx = pointer . x - this . lastPointer . x ;
74+ const dy = pointer . y - this . lastPointer . y ;
75+ this . lastPointer = pointer ;
76+ this . panBy ( dx , dy ) ;
77+ event . preventDefault ( ) ;
78+ }
79+
80+ onPointerUp ( event ) {
81+ if ( ! this . canvas ) {
82+ return ;
83+ }
84+
85+ this . dragging = false ;
86+ this . lastPointer = null ;
87+ this . canvas . classList . remove ( "is-panning" ) ;
88+ this . onCameraChanged ( ) ;
89+
90+ if ( this . canvas . hasPointerCapture ( event . pointerId ) ) {
91+ this . canvas . releasePointerCapture ( event . pointerId ) ;
92+ }
93+ }
94+
95+ getPointerPosition ( event ) {
96+ if ( ! this . canvas ) {
97+ return null ;
98+ }
99+
100+ const rect = this . canvas . getBoundingClientRect ( ) ;
101+ return {
102+ x : event . clientX - rect . left ,
103+ y : event . clientY - rect . top
104+ } ;
105+ }
106+
107+ zoomAt ( screenX , screenY , factor ) {
108+ if ( factor === 0 ) {
109+ return ;
110+ }
111+
112+ const camera = this . getActiveCamera ( ) ;
113+ const oldZoom = camera . zoom ;
114+ const newZoom = clamp ( oldZoom * factor , MIN_ZOOM , MAX_ZOOM ) ;
115+
116+ if ( newZoom === oldZoom ) {
117+ return ;
118+ }
119+
120+ const worldX = ( screenX - camera . panX ) / oldZoom ;
121+ const worldY = ( screenY - camera . panY ) / oldZoom ;
122+
123+ camera . zoom = newZoom ;
124+ camera . panX = screenX - worldX * newZoom ;
125+ camera . panY = screenY - worldY * newZoom ;
126+ }
127+
128+ panBy ( dx , dy ) {
129+ const camera = this . getActiveCamera ( ) ;
130+ camera . panX += dx ;
131+ camera . panY += dy ;
132+ }
133+
134+ resetView ( ) {
135+ const camera = this . getActiveCamera ( ) ;
136+ camera . zoom = 1 ;
137+ camera . panX = 0 ;
138+ camera . panY = 0 ;
139+ this . onCameraChanged ( ) ;
140+ }
141+ }
0 commit comments