PixiJS 6.1.0 will ship with an experimental UI event infrastructure that provides a much more robust and DOM-compatible solution than the incumbent interaction plugin. This change made it through PixiJS’ RFC 7071 and merged in PR 7213.
I named it the “Federated Events API.” It’s federated because you can create multiple event boundaries and override their logic for parts of your scene. Each event boundary only controls events for the scene below them – not unlike a federation.
Context
I developed the Federated Events API to overcome the two significant limitations in the Interaction API –
- DOM Incompatibility
- Extensibility
Apart from these API-facing issues, we also needed to refactor the implementation to make it more maintainable.
DOM Incompatibility
The Interaction API had a synthetic InteractionEvent
that didn’t overlap with DOM’s PointerEvent
well enough. If your UI shared DOM elements, then event handlers had to still be specific to PixiJS or the DOM.
The Federated Events API brings multiple events that inherit their DOM counterparts. This means your event handlers are agnostic to whether they’re looking at a DOM or PixiJS event. DisplayObject
now also has addEventListener
and removeEventListener
methods.
The semantics of some interaction events diverged from those of the Pointer Events API.
pointerover
andpointerout
events didn’t bubble up to their common ancestor.pointerenter
andpointerleave
events were missing.pointermove
events would fire throughout the scene graph, instead of just on hovered display object.
This gets corrected in this new API!
Another important addition is the capture phase for event propagation. The new API’s event propagation matches that of the DOM.
Extensibility
The Interaction API’s implementation was very brittle, and overriding any detail meant hell. The rigid architecture also means that customizing interaction for a part of your scene was impossible.
This new API lets you override specific details of the event infrastructure. That includes:
- optimizing hit testing (spatial hash acceleration?)
- adding custom events (focus, anyone?)
- modifying event coordinates (handy if you’re using projections)
The API also lets you mount event boundaries at specific parts of your scene graph to override events for display objects underneath it.
Other improvements
wheel
eventsstopImmediatePropagation
&preventDefault
methods on events- Faster hit testing by default; the search terminates once the event’s target is located.
Architecture
The EventSystem
is the main point of contact for federated events. Adding it to your renderer will register the system’s event listeners, and once it renders – the API will propagate FederatedEvent
s to your scene. The EventSystem
‘s job is to normalize native DOM events into FederatedEvent
s and pass them to the rootBoundary
. It’s just a thin wrapper with a bit of configuration & cursor handling on top.
The EventBoundary
object holds the API’s core functionality – taking an upstream event, translating it into a downstream event, and then propagating it. The translation is implemented as an “event mapping” – listeners are registered for handling specific upstream event types and are responsible for translating and propagating the corresponding downstream events. This mapping isn’t always one-to-one; the default mappings are as follows:
pointerdown
→pointerdown
pointermove
→pointerout
,pointerleave
,pointermove
,pointerover
,pointerenter
pointerup
→pointerup
pointerout
→pointerout
,pointerleave
pointerover
→pointerover
,pointerenter
wheel
→wheel
This list doesn’t include the mouse- and touch-specific events that are emitted too.
Federation
An event boundary can search through and propagate events throughout a connected scene graph, which would be connected by the parent-child relationships.
In certain cases, however, you may want to “hide” the implementation scene for an object. @pixi-essentials/svg does this to prevent your scene from being dominated by SVG rendering nodes. Instead of holding the nodes below as children
, you place them in a root
container and render it separately.

// Crude anatomy of a disconnected scene
class HiddenScene {
root: Container;
render(renderer) {
renderer.render(this.root);
}
}
This poses a problem when you want interactivity to still flow through this “point of disconnection”. Here, an additional event boundary that accepts upstream events and propagating them through root
can fix this! See the nested boundary example at the end for how.
Examples
Basic usage
Since the Federated Events API won’t be production-ready until PixiJS 7, it’s not enabled by default. To use it, you’ll have to delete the interaction plugin and install the EventSystem
manually. If you’re using a custom bundle, you can remove the @pixi/interaction module too.
import { EventSystem } from '@pixi/events';
import { Renderer } from '@pixi/core';// or pixi.js
delete Renderer.__plugins.interaction;
// Assuming your renderer is at "app.renderer"
if (!('events' in app.renderer)) {
app.renderer.addSystem(EventSystem, 'events');
}
Clicks
Let’s start with this barebones example – handling clicks on a display object. Just like the Interaction API, you need to mark it interactive
and add a listener.
// Enable interactivity for this specific object. This
// means that an event can be fired with this as a target.
object.interactive = true;
// Listen to clicks on this object!
object.addEventListener('click', function onClick() {
// Make the object bigger each time it's clicked!
object.scale.set(object.scale.x * 1.1);
});
A handy tool for checking handling “double” or even “triple” clicks is the event’s detail
. The event boundary keeps track of how many clicks have been done each within 200ms of each other. For double clicks, it’ll be set to 2. The following example scales the bunny based on this property – you have to click fast to make the bunny larger!
Dragging
Dragging is done slightly differently with the new API – you have to register the pointermove
handler on the stage, not the dragged object. Otherwise, if the pointer moves out of the selected DisplayObject, it’ll stop getting pointermove
events (to emulate the InteractionManager’s behavior – enable moveOnAll
in the root boundary).
The upside is much better performance and mirroring of the DOM’s semantics.
function onDragStart(e) {
selectedTarget = e.target;
// Start listening to dragging on the stage
app.stage.addEventListener('pointermove', onDragMove);
}
function onDragMove(e) {
// Don't use e.target because the pointer might
// move out of the bunny if the user drags fast,
// which would make e.target become the stage.
selectedTarget.parent.toLocal(
e.global, null, selectedTarget.position);
}
Wheel
The wheel
event is available to use just like any other! You can move your display object by the event’s deltaY
to implement scrolling. This example does that for a slider’s handle.
Right now, wheel events are implemented as “passive” listeners. That means you can’t do preventDefault()
to block the browser from scrolling other content; this means you should only use it on fullscreen canvas apps.
slider.addEventListener('wheel', onWheel);
Advanced use-cases
Manual hit-testing
To override a specific part of event handling, you can inherit from EventBoundary
and set the event system’s rootBoundary
!
Here’s an example that uses a SpatialHash
to accelerate hit-testing. A special HashedContainer
holds a spatial hash for its children, and that is used to search through them instead of a brute force loop.
This technique is particularly useful for horizontal scene graphs, where a few containers hold most of the display objects as children.
Nested boundaries
The ultimate example: how you can use a nested EventBoundary
in your scene graph. As mentioned before, this is useful when you have a disconnected scene graph and you want events to propagate over points of disconnection.
To forward events from upstream, you make the “subscene” interactive, listen to all the relevant events, and map them into the event boundary below. The event boundary should be attached to the content of your scene. It’s like implementing a stripped down version of the EventSystem
.
// Override copyMouseData to apply inverse worldTransform
// on global coords
this.boundary.copyMouseData = (from, to) => {
// Apply default implementation first
PIXI.EventBoundary.prototype
.copyMouseData
.call(this.boundary, from, to);
// Then bring global coords into content's world
this.worldTransform.applyInverse(
to.global, to.global);
};
// Propagate these events down into the content's
// scene graph!
[
'pointerdown',
'pointerup',
'pointermove',
'pointerover',
'pointerout',
'wheel',
].forEach((event) => {
this.addEventListener(
event,
(e) => this.boundary.mapEvent(e),
);
});
To make the cursor
on internal objects work too, you should expose the event boundary’s cursor
property on the subscene.
get cursor() {
return this.boundary.cursor;
}