Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions DRAG_OUTSIDE_EXAMPLE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# dragOutside Feature Example

The `dragOutside` option allows dragging to continue even when the mouse/pointer leaves the canvas or browser window.

## Basic Usage

```javascript
import { Viewport } from 'pixi-viewport';

const viewport = new Viewport({
screenWidth: window.innerWidth,
screenHeight: window.innerHeight,
worldWidth: 2000,
worldHeight: 2000,
events: app.renderer.events
});

// Enable dragOutside
viewport.drag({
dragOutside: true
});
```

## Options

```javascript
viewport.drag({
dragOutside: true, // Enable dragging outside canvas (default: false)
clampWheel: false, // Other options work as usual
mouseButtons: 'all' // Still respects button configuration
});
```

## How it Works

When `dragOutside: true` is enabled:

1. **Normal behavior inside canvas**: Works exactly like before
2. **Outside canvas**: Uses document-level event listeners to track mouse movement
3. **Cross-window dragging**: Continues panning even when mouse leaves browser window
4. **Proper cleanup**: Automatically removes document listeners when drag ends

## Benefits

- **Better UX**: No more frustrating drag interruptions
- **Small canvases**: Enables large mouse movements for fine control
- **Interactive overlays**: Doesn't stop when mouse moves over UI elements
- **Backward compatible**: Default `false` maintains existing behavior

## Demo

The development server demo at `http://localhost:5175` shows `dragOutside` in action.
Look for the green indicator in the top-right corner!

## Browser Support

Works in all modern browsers that support pointer events (IE11+, all evergreen browsers).
3 changes: 2 additions & 1 deletion docs/src/code.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function viewport() {
stopPropagation: true
}))
_viewport
.drag({ clampWheel: false })
.drag({ clampWheel: false, dragOutside: true })
.wheel({ smooth: 3, trackpadPinch: true, wheelZoom: false, })
.pinch()
.decelerate()
Expand Down Expand Up @@ -192,6 +192,7 @@ function API() {
button.style.backgroundImage = 'linear-gradient(to bottom, #3498db, #2980b9)'
button.style.padding = '10px 20px 10px 20px'
clicked(button, () => window.location.href = 'https://davidfig.github.io/pixi-viewport/jsdoc/')

}

window.onload = function () {
Expand Down
31 changes: 28 additions & 3 deletions docs/src/gui.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export function gui(viewport, drawWorld, target) {
_world = _gui.addFolder('world')
options = {
testDirty: false,
drag: true,
drag: {
drag: true,
dragOutside: true
},
clampZoom: {
clampZoom: false,
minWidth: 1000,
Expand Down Expand Up @@ -119,15 +122,37 @@ function guiWorld() {
}

function guiDrag() {
_gui.add(options, 'drag').onChange(
function change() {
_viewport.drag({
clampWheel: true,
dragOutside: options.drag.dragOutside
})
}

function add() {
dragOutside = drag.add(options.drag, 'dragOutside').onChange(change)
}

let dragOutside
const drag = _gui.addFolder('drag')
drag.add(options.drag, 'drag').onChange(
function (value) {
if (value) {
_viewport.drag({ clampWheel: true })
change()
add()
}
else {
_viewport.plugins.remove('drag')
if (dragOutside) {
drag.remove(dragOutside)
dragOutside = null
}
}
})
if (options.drag.drag) {
add()
drag.open()
}
}

function guiClamp() {
Expand Down
138 changes: 132 additions & 6 deletions src/plugins/Drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ export interface IDragOptions
* @default false
*/
wheelSwapAxes?: boolean;

/**
* Continue dragging when mouse/pointer leaves the canvas/window.
* Uses document-level event listeners to track mouse movement outside the viewport.
*
* @default false
*/
dragOutside?: boolean;
}

const DEFAULT_DRAG_OPTIONS: Required<IDragOptions> = {
Expand All @@ -118,6 +126,7 @@ const DEFAULT_DRAG_OPTIONS: Required<IDragOptions> = {
ignoreKeyToPressOnTouch: false,
lineHeight: 20,
wheelSwapAxes: false,
dragOutside: false,
};

/**
Expand Down Expand Up @@ -166,6 +175,15 @@ export class Drag extends Plugin
handler: (e: any) => void;
}> = [];

/** Tracks whether we're in dragOutside mode */
private isDragOutside = false;

/** Initial viewport position when dragOutside starts */
private dragOutsideStartPosition?: PointData;

/** Initial DOM coordinates when dragOutside starts */
private dragOutsideStartMouse?: PointData;

/**
* This is called by {@link Viewport.drag}.
*/
Expand Down Expand Up @@ -193,6 +211,11 @@ export class Drag extends Plugin
{
this.handleKeyPresses(this.options.keyToPress);
}

if (this.options.dragOutside)
{
this.setupDragOutside();
}
}

/**
Expand Down Expand Up @@ -232,6 +255,73 @@ export class Drag extends Plugin
this.windowEventHandlers.push({ event, handler });
}

/**
* Sets up document-level event handlers for dragOutside functionality
*/
private setupDragOutside(): void
{
const documentMoveHandler = (e: PointerEvent) =>
{
if (this.isDragOutside && this.dragOutsideStartMouse && this.dragOutsideStartPosition && this.current !== undefined)
{
// Calculate movement delta from the original drag start position (in DOM coordinates)
const deltaX = e.clientX - this.dragOutsideStartMouse.x;
const deltaY = e.clientY - this.dragOutsideStartMouse.y;


if (this.xDirection)
{
this.parent.x = this.dragOutsideStartPosition.x + deltaX * this.options.factor;
}
if (this.yDirection)
{
this.parent.y = this.dragOutsideStartPosition.y + deltaY * this.options.factor;
}

if (!this.moved)
{
this.parent.emit('drag-start', {
event: e as any,
screen: new Point(this.dragOutsideStartMouse.x, this.dragOutsideStartMouse.y),
world: this.parent.toWorld(new Point(this.dragOutsideStartMouse.x, this.dragOutsideStartMouse.y)),
viewport: this.parent,
});
this.moved = true;
}

this.parent.emit('moved', { viewport: this.parent, type: 'drag' });
}
};

const documentUpHandler = (e: PointerEvent) =>
{
if (this.isDragOutside)
{
this.isDragOutside = false;
this.dragOutsideStartPosition = undefined;
this.dragOutsideStartMouse = undefined;

if (this.moved)
{
const screen = new Point(e.clientX, e.clientY);
this.parent.emit('drag-end', {
event: e as any,
screen,
world: this.parent.toWorld(screen),
viewport: this.parent,
});
}

this.last = null;
this.moved = false;
this.current = undefined;
}
};

this.addWindowEventHandler('pointermove', documentMoveHandler);
this.addWindowEventHandler('pointerup', documentUpHandler);
}

public override destroy(): void
{
if (typeof window === 'undefined') return;
Expand Down Expand Up @@ -341,14 +431,32 @@ export class Drag extends Plugin
}
if (this.checkButtons(event) && this.checkKeyPress(event))
{
this.last = { x: event.global.x, y: event.global.y };
(this.parent.parent || this.parent).toLocal(
this.last,
undefined,
this.last,
);
this.current = event.pointerId;

// Setup dragOutside mode if enabled
if (this.options.dragOutside)
{
this.isDragOutside = true;
this.dragOutsideStartPosition = { x: this.parent.x, y: this.parent.y };

// Store DOM coordinates for dragOutside calculation
// Use the original pointer event to get DOM coordinates
const nativeEvent = event.nativeEvent as PointerEvent;
this.dragOutsideStartMouse = { x: nativeEvent.clientX, y: nativeEvent.clientY };

// Still store PIXI coordinates for normal drag functionality
this.last = { x: event.global.x, y: event.global.y };
}
else
{
this.last = { x: event.global.x, y: event.global.y };
(this.parent.parent || this.parent).toLocal(
this.last,
undefined,
this.last,
);
}

return true;
}
this.last = null;
Expand All @@ -369,6 +477,17 @@ export class Drag extends Plugin
}
if (this.last && this.current === event.data.pointerId)
{
// Skip normal drag handling if dragOutside is active (handled by document events)
if (this.options.dragOutside && this.isDragOutside)
{
// Still need to update moved flag for proper event handling
if (!this.moved)
{
this.moved = true;
}
return true; // Return true to indicate drag is handled
}

const x = event.global.x;
const y = event.global.y;
const count = this.parent.input.count();
Expand Down Expand Up @@ -435,6 +554,13 @@ export class Drag extends Plugin
return false;
}

// If dragOutside is active, don't process normal up events
// The document handler will handle the drag end
if (this.options.dragOutside && this.isDragOutside)
{
return true; // Return true to indicate we handled it
}

const touches = this.parent.input.touches;

if (touches.length === 1)
Expand Down
Loading