How to Implement Drag and Drop in Pixi.js

Implementing drag-and-drop functionality in Pixi.js involves enabling interactivity on a display object, listening to pointer events, and updating the object’s coordinates based on pointer movement. This article provides a step-by-step guide and a complete code example to create a smooth, responsive drag-and-drop interaction that handles multi-touch inputs and prevents the object from snapping to the cursor’s center point.

Step 1: Enable Interactivity on the Object

By default, Pixi.js display objects (like Sprites or Graphics) do not respond to user input. To make an object draggable, you must set its eventMode property to 'static' (or 'dynamic' if it moves independently) and change the cursor to a pointer for better user feedback.

const sprite = PIXI.Sprite.from('path/to/image.png');

// Enable interaction
sprite.eventMode = 'static';
sprite.cursor = 'pointer';

Step 2: Handle Pointer Events

To create a natural dragging experience, you need to listen to three main pointer events: * pointerdown: Triggered when the user clicks or touches the object. * pointermove: Triggered when the pointer moves. * pointerup / pointerupoutside: Triggered when the user releases the click/touch, even if the pointer is no longer directly over the object.

sprite.on('pointerdown', onDragStart);

Step 3: Write the Drag Event Handlers

The event handlers manage the dragging state. To prevent the object’s center or anchor point from “snapping” to the mouse cursor when clicked, calculate the offset between the pointer’s click position and the object’s origin.

function onDragStart(event) {
    // Store a reference to the event data to track this specific pointer (supports multi-touch)
    this.data = event.data;
    this.alpha = 0.5; // Visually indicate dragging

    // Get the pointer's position relative to the object's parent container
    const newPosition = this.data.getLocalPosition(this.parent);
    
    // Calculate and store the offset
    this.offsetX = newPosition.x - this.x;
    this.offsetY = newPosition.y - this.y;

    // Listen for movement and release on the parent container for smoother tracking
    this.parent.on('pointermove', onDragMove, this);
    this.parent.once('pointerup', onDragEnd, this);
    this.parent.once('pointerupoutside', onDragEnd, this);
}

function onDragMove(event) {
    if (this.data) {
        // Retrieve the updated pointer position
        const newPosition = this.data.getLocalPosition(this.parent);
        
        // Update the object's coordinates while maintaining the initial click offset
        this.x = newPosition.x - this.offsetX;
        this.y = newPosition.y - this.offsetY;
    }
}

function onDragEnd() {
    if (this.data) {
        this.alpha = 1; // Restore original opacity
        
        // Remove tracking listeners from the parent container
        this.parent.off('pointermove', onDragMove, this);
        this.parent.off('pointerup', onDragEnd, this);
        this.parent.off('pointerupoutside', onDragEnd, this);
        
        this.data = null; // Clear the pointer reference
    }
}

Why This Method is Smooth and Robust

  1. Offset Calculations: Subtracting this.offsetX and this.offsetY ensures the object moves smoothly from the exact pixel where the user grabbed it, rather than snapping its anchor point directly to the mouse cursor.
  2. Parent-Level Event Binding: Binding the pointermove and pointerup events to this.parent (or the stage) instead of the sprite itself ensures that even if the user moves the mouse very quickly and temporarily leaves the bounds of the sprite, the drag action is not interrupted.
  3. Multi-Touch Compatibility: Storing event.data allows Pixi.js to track individual touch points independently, enabling multiple objects to be dragged simultaneously on touch screens.