Pointer tracking

Pointer tracking is an integral part of user interfaces.

Drag & drop, scrolling galleries, and measuring touch scroll speed are some obvious use cases.

In this quick tutorial, we’ll first see how to convert DOM events into streams of values with the listen action.

Then, we’ll look at how to track mouse and touch movement with the pointer action and then use that pointer’s movement to drag a DOM element.

The listen action

Popmotion provides the listen action to add event listeners to the DOM.

import { listen } from 'popmotion';

It accepts event names as a space-delimited string, meaning you can use a single listen call to listen to multiple events:

listen(document, 'mousedown touchstart')
  .start((e) => console.log(e));

Just like the good-old days of jQuery!

As listen is an action, it offers all the same chainable methods.

For example, here’s how you could make a touchmove listener that only fires when there’s more than two touches:

listen(document, 'touchmove')
  .filter(({ touches }) => touches.length >= 2)
  .start((e) => /* This event has more than 2 touches! */);

The pointer action

The pointer action provides a generic interface for interacting with single point mouse and touch inputs (for multitouch, Popmotion offers the multitouch action).

import { pointer } from 'popmotion';

By default, pointer outputs the pointer’s clientX and clientY properties as x and y.

let pointerTracker;

listen(document, 'mousedown touchstart').start(() => {
  pointerTracker = pointer()
    .start(({ x, y }) => console.log(x, y));
});

listen(document, 'mouseup touchend').start(() => {
  if (pointerTracker) pointerTracker.stop();
});

Dragging

The majority of time we actually want to use this movement data to drag or scroll.

Let’s use the styler function again to create a DOM element interface to provide the positional data to.

Look at startTracking function and try to drag the ball:

Oh dear. The ball moves, but it jumps to a weird location (maybe one that’s off-screen entirely!)

The reason for this is simple. pointer is outputting the { x, y } position of the pointer relative to the viewport.

The ball’s { x, y } transform begins at 0, 0. So if we apply the pointer’s absolute position directly to the ball, it won’t move where we want it to.

Instead, we want to apply the movement of the pointer to the ball’s initial position.

To do this, we can provide pointer with an initial { x, y } point, and it will output the movement of its { x, y } relative to that point:

pointer({ x: 0, y: 0 })

Dragging becomes trivial:

Single-axis dragging

Limiting dragging to a single axis is a matter of using just the x or y output from pointer.

We could do this via the reaction:

pointer().start(({ x }) => ballStyler.set('x', x));

But the more reusable way is to compose a new action using pointer‘s pipe method. We can provide it a simple picker function that selects x from pointer‘s output and returns it:

const xPointer = (initialX) => pointer({ x: initialX })
  .pipe(({ x }) => x);

Now we can use our newly composed xPointer like so:

xPointer(ballStyler.get('x'))
  .start(ballStyler.set('x'));

Conclusion

listen can convert DOM events into reactive streams of values. Useful for leveraging pipe and other chainable methods, and composing listen with other actions.

pointer can output values either absolutely, or, if we provide initial coordinates, by applying its delta relatively.

Finally, we can compose new actions to produce reusable bits of code with new behaviour, like single-axis dragging.

Next

Now that we’ve got dragging working, in the next tutorial we will learn how to inspect the velocity of the dragged object and apply that to decay, physics and spring actions to create natural-feeling interactions.