@@ -102,6 +102,14 @@ export interface IDragOptions
102102 * @default false
103103 */
104104 wheelSwapAxes ?: boolean ;
105+
106+ /**
107+ * Continue dragging when mouse/pointer leaves the canvas/window.
108+ * Uses document-level event listeners to track mouse movement outside the viewport.
109+ *
110+ * @default false
111+ */
112+ dragOutside ?: boolean ;
105113}
106114
107115const DEFAULT_DRAG_OPTIONS : Required < IDragOptions > = {
@@ -118,6 +126,7 @@ const DEFAULT_DRAG_OPTIONS: Required<IDragOptions> = {
118126 ignoreKeyToPressOnTouch : false ,
119127 lineHeight : 20 ,
120128 wheelSwapAxes : false ,
129+ dragOutside : false ,
121130} ;
122131
123132/**
@@ -166,6 +175,15 @@ export class Drag extends Plugin
166175 handler : ( e : any ) => void ;
167176 } > = [ ] ;
168177
178+ /** Tracks whether we're in dragOutside mode */
179+ private isDragOutside = false ;
180+
181+ /** Initial viewport position when dragOutside starts */
182+ private dragOutsideStartPosition ?: PointData ;
183+
184+ /** Initial DOM coordinates when dragOutside starts */
185+ private dragOutsideStartMouse ?: PointData ;
186+
169187 /**
170188 * This is called by {@link Viewport.drag}.
171189 */
@@ -193,6 +211,11 @@ export class Drag extends Plugin
193211 {
194212 this . handleKeyPresses ( this . options . keyToPress ) ;
195213 }
214+
215+ if ( this . options . dragOutside )
216+ {
217+ this . setupDragOutside ( ) ;
218+ }
196219 }
197220
198221 /**
@@ -232,6 +255,73 @@ export class Drag extends Plugin
232255 this . windowEventHandlers . push ( { event, handler } ) ;
233256 }
234257
258+ /**
259+ * Sets up document-level event handlers for dragOutside functionality
260+ */
261+ private setupDragOutside ( ) : void
262+ {
263+ const documentMoveHandler = ( e : PointerEvent ) =>
264+ {
265+ if ( this . isDragOutside && this . dragOutsideStartMouse && this . dragOutsideStartPosition && this . current !== undefined )
266+ {
267+ // Calculate movement delta from the original drag start position (in DOM coordinates)
268+ const deltaX = e . clientX - this . dragOutsideStartMouse . x ;
269+ const deltaY = e . clientY - this . dragOutsideStartMouse . y ;
270+
271+
272+ if ( this . xDirection )
273+ {
274+ this . parent . x = this . dragOutsideStartPosition . x + deltaX * this . options . factor ;
275+ }
276+ if ( this . yDirection )
277+ {
278+ this . parent . y = this . dragOutsideStartPosition . y + deltaY * this . options . factor ;
279+ }
280+
281+ if ( ! this . moved )
282+ {
283+ this . parent . emit ( 'drag-start' , {
284+ event : e as any ,
285+ screen : new Point ( this . dragOutsideStartMouse . x , this . dragOutsideStartMouse . y ) ,
286+ world : this . parent . toWorld ( new Point ( this . dragOutsideStartMouse . x , this . dragOutsideStartMouse . y ) ) ,
287+ viewport : this . parent ,
288+ } ) ;
289+ this . moved = true ;
290+ }
291+
292+ this . parent . emit ( 'moved' , { viewport : this . parent , type : 'drag' } ) ;
293+ }
294+ } ;
295+
296+ const documentUpHandler = ( e : PointerEvent ) =>
297+ {
298+ if ( this . isDragOutside )
299+ {
300+ this . isDragOutside = false ;
301+ this . dragOutsideStartPosition = undefined ;
302+ this . dragOutsideStartMouse = undefined ;
303+
304+ if ( this . moved )
305+ {
306+ const screen = new Point ( e . clientX , e . clientY ) ;
307+ this . parent . emit ( 'drag-end' , {
308+ event : e as any ,
309+ screen,
310+ world : this . parent . toWorld ( screen ) ,
311+ viewport : this . parent ,
312+ } ) ;
313+ }
314+
315+ this . last = null ;
316+ this . moved = false ;
317+ this . current = undefined ;
318+ }
319+ } ;
320+
321+ this . addWindowEventHandler ( 'pointermove' , documentMoveHandler ) ;
322+ this . addWindowEventHandler ( 'pointerup' , documentUpHandler ) ;
323+ }
324+
235325 public override destroy ( ) : void
236326 {
237327 if ( typeof window === 'undefined' ) return ;
@@ -341,14 +431,32 @@ export class Drag extends Plugin
341431 }
342432 if ( this . checkButtons ( event ) && this . checkKeyPress ( event ) )
343433 {
344- this . last = { x : event . global . x , y : event . global . y } ;
345- ( this . parent . parent || this . parent ) . toLocal (
346- this . last ,
347- undefined ,
348- this . last ,
349- ) ;
350434 this . current = event . pointerId ;
351435
436+ // Setup dragOutside mode if enabled
437+ if ( this . options . dragOutside )
438+ {
439+ this . isDragOutside = true ;
440+ this . dragOutsideStartPosition = { x : this . parent . x , y : this . parent . y } ;
441+
442+ // Store DOM coordinates for dragOutside calculation
443+ // Use the original pointer event to get DOM coordinates
444+ const nativeEvent = event . nativeEvent as PointerEvent ;
445+ this . dragOutsideStartMouse = { x : nativeEvent . clientX , y : nativeEvent . clientY } ;
446+
447+ // Still store PIXI coordinates for normal drag functionality
448+ this . last = { x : event . global . x , y : event . global . y } ;
449+ }
450+ else
451+ {
452+ this . last = { x : event . global . x , y : event . global . y } ;
453+ ( this . parent . parent || this . parent ) . toLocal (
454+ this . last ,
455+ undefined ,
456+ this . last ,
457+ ) ;
458+ }
459+
352460 return true ;
353461 }
354462 this . last = null ;
@@ -369,6 +477,17 @@ export class Drag extends Plugin
369477 }
370478 if ( this . last && this . current === event . data . pointerId )
371479 {
480+ // Skip normal drag handling if dragOutside is active (handled by document events)
481+ if ( this . options . dragOutside && this . isDragOutside )
482+ {
483+ // Still need to update moved flag for proper event handling
484+ if ( ! this . moved )
485+ {
486+ this . moved = true ;
487+ }
488+ return true ; // Return true to indicate drag is handled
489+ }
490+
372491 const x = event . global . x ;
373492 const y = event . global . y ;
374493 const count = this . parent . input . count ( ) ;
@@ -435,6 +554,13 @@ export class Drag extends Plugin
435554 return false ;
436555 }
437556
557+ // If dragOutside is active, don't process normal up events
558+ // The document handler will handle the drag end
559+ if ( this . options . dragOutside && this . isDragOutside )
560+ {
561+ return true ; // Return true to indicate we handled it
562+ }
563+
438564 const touches = this . parent . input . touches ;
439565
440566 if ( touches . length === 1 )
0 commit comments