Crashing cars and improving hover detection

azhenley1 pts0 comments

Crashing cars and improving hover detection | Motion MagazineMotion+<br>I'm going to show you an effect that you'll recognise immediately, perhaps without ever having paid it much attention.

Take any collection of elements that react to hover: a list of menu items, swatches in a colour picker, squares in a grid. Now quickly swipe your cursor across them:

In real life, your hand moves across your desk, or your finger across the screen, in a continuous, unbroken motion. But this isn't reflected in the example above. Here, lights in the path of motion are switched on seemingly at random. Move slower, and you'll see that every element lights up, and the faster you swipe, the more elements are skipped.

Now run your mouse over this version. Swipe it as fast as you like: every cell you cross lights up, with nothing skipped.

Honestly when I created this second example I couldn't stop playing with it. It is weird how responsive it feels, why doesn't it always work like this? By the end of this post you'll know why, and how to build this improved hover yourself.

Surprisingly, this is the exact same problem that video game engines encounter when deciding whether a car has crashed, or any other type of collision has taken place. As such, a solution to our skipped elements was invented decades ago.

Discrete vs continuous motion

CSS selectors like :hover, Motion events like onHoverStart, and JS events like pointerenter are all afflicted by this skipped element problem.

The reason being, pointer position is sampled discretely, rather than continuously. Streamed as a series of points, via events, to the browser and then by the browser to our code.

To illustrate, lets imagine a row of elements, with a pointer moving slowly across them. The pointer events always come in at the same rate, so with slower motion these events are closer together. Meaning that it's more likely at least one pointer event lands on each element, triggering its hover state:

SAMPLED IN EACH → ALL FIRE> SlowFIG.01<br>Faster movement means the gaps between these events increases. Which means any elements lying in these gaps are completely skipped.

FIREDSKIPPEDSKIPPED> FastFIG.02<br>If you've ever written a physics engine, this'll feel familiar, because it's a textbook collision detection problem with a specific name: tunnelling .

Physics engines run a loop. Every iteration, they calculate the next position of objects based on their position and velocity. Then, they check which objects hit which. It's similar to pointer events in sense that you're checking discrete snapshots of positions rather than continuous motion (the latter essentially requiring infinite computation).

Picture a car driving towards a thin wall. During Animation Frame A it's just short of the wall, and then as it drives a little further, in Frame B it's overlapping the wall. The engine spots the overlap and registers a hit.

WALLFRAME AFRAME BOVERLAP → HIT> SLOW: COLLISIONFIG.03<br>In a game, it will probably do something like move the car back outside the wall and trigger a crashing animation, so they appear to impact.

But now, imagine a car that's moving even faster. It's moving so fast that, in Frame B, it's already out the other side of the wall. In no single frame does the car overlap the wall, so the collision check, which only ever looks once per frame, sees nothing. The car sails straight through.

WALLFRAME AFRAME BNO OVERLAP → TUNNELS> FAST: TUNNELFIG.04<br>This is tunnelling, and it's the exact same problem as our pointer sampling. The pointer is the fast object, each hover target a thin wall.

The fix

Games fix tunnelling by, instead of asking "where is the object this frame, and does that overlap anything?", you ask "what path did the object take since last frame, and did that path cross anything?".

Rather than test a point, you test a line.

WALLFRAME AFRAME BLINE CROSSES → HIT> SWEPT: COLLISIONFIG.05<br>For pointers, that line is easy to calculate. You can measure the pointer's position this frame, and look back at where it was in the previous frame. Draw a line between the two and you've got the route the cursor took. So instead of checking which element contains the current point, you check which elements the line intersects .

LINE CROSSES → ALL CAPTURED> Continuous detectionFIG.06<br>Building it with Motion

To actually implement this, we need to:

Get the pointer's previous and current position.

Measure the elements we wish to check against.

Perform a cheap geometric test.

Reading the pointer

In this post, we're going to be using Motion APIs and concepts, but the basic procedure is of course replicable in plain JavaScript.

Motion+ ships a usePointerPosition hook that gives you pointer positions as motion values, in viewport-relative coordinates .

const pointer = usePointerPosition()<br>The reason I default to usePointerPosition is that it's extremely composable. No matter how many components call it, it only ever registers a single pointer listener and a single pair...

pointer motion frame hover elements events

Related Articles