Overhauling our collision engine
Tags: javascript, react, ux, webdev
Published: 2019-12-05 03:24:06

> How we created a more delightful drag and drop experience

Drag and drop is a very human interaction: pick something up and move it somewhere else. [`react-beautiful-dnd`](https://github.com/atlassian/react-beautiful-dnd) (`rbd`) is an open source library we created at Atlassian that attempts to reflect the **physicality** of this human interaction in order to provide a more relatable experience.

`rbd` contains logic for calculating what should move out of way of the dragging item, and what droppable area the dragging item is currently over (**the collision engine**). Over time, some limitations of the collision engine have emerged. This blog will share our journey rebuilding the collision engine in order to make drag and drop more delightful.

## Problem 1: Knowing when to move

The original collision engine used the centre position of the *dragging item* to determine what other items should move out of the way. An item would move out of the way when the centre position of the dragging item goes over the start or end edge of another item.

> "a dragging items impact is based on its centre of gravity — regardless of where a user grabs an item from" - [Rethinking drag and drop](https://medium.com/@alexandereardon/rethinking-drag-and-drop-d9f5770b4e6b)

✅ When items are **roughly the same size** then the interaction is all good

![reordering similar sized items](https://thepracticaldev.s3.amazonaws.com/i/ggm3k1so672o9yta3q24.gif)

✅ Using the centre position of the dragging item holds up when dragging **large items next to small items**

<img width="668" alt="collision diagram" src="https://thepracticaldev.s3.amazonaws.com/i/0zbeicfvqxszcd33be6t.png">

❌ Using the centre position of the dragging item **does not work well when dragging small items next to large items**

<img width="679" alt="collision diagram" src="https://thepracticaldev.s3.amazonaws.com/i/hiyciyj62nzxfwcso1xd.png">

In this example, when moving a small item past a big item, the big item would satisfy the condition to move up (the dragging centre is after the top edge), and once moved up, it would satisfy the condition to move down (the dragging centre is before the bottom edge). This would cause big item not being dragged to flicker up and down.

We overcame this weakness in the **old** collision engine by checking different edges depending on what direction the user was heading in. For example, when moving down in a list we only check to see if the centre of the dragging item has gone past the top edge of another item.

<img width="746" alt="collision diagram" src="https://thepracticaldev.s3.amazonaws.com/i/36ynsoi1r3z77vjd0kor.png">

This solution (*hack*) was not without drawbacks. When dragging large items next to small items, things quickly turn into a **dumpster fire** if the user changed directions quickly.

![large-items-sucked](https://thepracticaldev.s3.amazonaws.com/i/pmbhwwvlxzij75vnoku4.gif)

<img width="300" src="https://thepracticaldev.s3.amazonaws.com/i/p2zja3j8a82h1b2r1ycv.png" alt="oh no">

When dragging large items next to small ones, small direction changes could result in quite a different displacement of items.

So yeah, things were not great.

## Solution: Flipping the logic

The shortcomings of the collision engine have lingered on in the `rbd` repo for quite some time. There was an open question as to how to maintain **physicality,** while also overcoming problems with using the centre position of the dragging item.

About a month ago we got a fantastic suggestion from [@caspersmith](https://github.com/caspersmith): *flip the logic*. Rather than using the centre position of the dragging item for determining displacement, look at the centre position of the *other items*. When an **edge of the dragging item** goes over the **centre position of another item**, then move that item out of the way. It turned out that this idea holds up extremely well mathematically and still feels great.

<img width="941" alt="collision diagram" src="https://thepracticaldev.s3.amazonaws.com/i/cgw3cyfhdf64qd0ofvng.png">

By using the edges of the dragging item we can never end up in a situation where we hit the same item before and after it is displaced, regardless of the items size. The new approach also means we no longer need to lean on the user direction (hack).

![big-item-fixed](https://thepracticaldev.s3.amazonaws.com/i/htc22buntwna25sqtyc0.gif)

![success](https://thepracticaldev.s3.amazonaws.com/i/ftktagfz6ofprbrxr03r.gif)

## Problem 2: What is being dragged over?

When dragging something around we need to communicate to the user what droppable area the dragging item is currently over. Previously we did this by **exclusively using the centre position of the dragging item**. This is done in an attempt to make the dragging item feel more physical:  by using the centre position we are using the dragging item's centre of mass to control its location.

✅ Things feel fantastic when lists are **roughly the same size**

![between-two-lists](https://thepracticaldev.s3.amazonaws.com/i/zydxc4vh8kf5elsyjkjl.gif)

❌ Significant problems arise when a user is trying to **drag a large item into a small list**

![big lists broken](https://thepracticaldev.s3.amazonaws.com/i/01na7au8ztzpph9xwu5a.gif)

It can be jarring (and sometimes impossible) for users to drag a big item into a small list. They need to line up the centre position of the big item over this list - which can require overshooting the list with your cursor to get the centre position in the right spot.

<img width="300" src="https://thepracticaldev.s3.amazonaws.com/i/p2zja3j8a82h1b2r1ycv.png" alt="oh no">

## Solution: Understanding intention

We spent *many* hours whiteboarding various potential strategies to improve the experience for dragging large items into small areas. I think we lost a few years of our lives during this process.

### The new approach

1. Find **drop candidates 🙋‍♀️**(droppable areas that *could* be the active area)
2. Choose the **drop candidate furthest away** from where the drag start started

### Finding drop candidates 🙋‍♀️

Previously there was only one way to determine what the droppable area would be: the dragging item centre position. We have now added more ways for droppable areas to be the active droppable area.

A droppable area can be a **drop candidate** when

1. **Centre hit (existing)**: the centre position of the dragging item is over the droppable area
2. **Cross axis hit (new)**:

    *(this logic only comes into play when dragging large items into smaller sized areas)*
    - There is **some** overlap of the dragging item and the droppable area; and
    - **Only one** edge of the dragging item is over the list on the cross axis of the droppable area; and
    - The edge must have **gone past the cross axis centre** of the droppable area (`end edge > cross axis centre line`, `start edge < cross axis centre line`)
3. **Totally covered on cross axis (new)**:
    - The dragging item is **totally** over a droppable area on the cross axis
    - There is **some** overlap on the main axis

<img alt="detected candidates" src="https://thepracticaldev.s3.amazonaws.com/i/sghxsm48kc33i6ctkp74.png">

### Choose candidate that is furthest away

When there are **multiple drop candidates** (multiple drop areas that we think the user is over) **we choose the one that is furthest away from where the drag started**. There will only be multiple drop candidates when dragging large items over small droppable areas, so this logic doesn't come into play for the standard use case of dragging items over areas that are of a similar size on the cross axis.

<img width="885" alt="furthest away diagram" src="https://thepracticaldev.s3.amazonaws.com/i/2bxriw7t9nmu3pcakpy9.png">

Determining what drop candidate is furthest away is not as straightforward as measuring the distance between the centre points of the droppable areas (which is what we started with). Doing that gives preference to giant droppable areas as their centres are often far away. Rather, we measure the distance between where the dragging item started, the point at which the dragging item hits the cross axis of the droppable area.

<img width="735" alt="calculating distance between lists" src="https://thepracticaldev.s3.amazonaws.com/i/ncophvj0sistm3ilrn1a.png">

### What does the new approach achieve?

When droppable areas are **roughly the same size** on the cross axis, things will operate in the same way they did before: the centre position will be used to control what droppable area an item is over (which feels great)

Strictly using the dragging items centre position was our ideal situation for all interactions. But, as we have seen, using the centre doesn't work well for **moving big items into small lists**. We looked for a solution would best respect the users **intention**. We landed on the view that when a draggable item hits a new droppable area it is likely that the user is trying to move into that area - so that is what we will try to give preference to. That is why we give preference to areas that are further away from where the user started. Doing this also requires the least amount of movement to move something into a new area.

**We decided this new strategy for large items was a necessary divergence from the physicality of `rbd` in order to provide an experience that feels intuitive and delightful for users, which in the end, is what we are pursuing physicality to achieve.**

![large-items-nice](https://thepracticaldev.s3.amazonaws.com/i/4gykiy1tevqzy2ku5tlx.gif)

## Thanks

We hope you have enjoyed this blog, and that you and the people who use your software enjoy the overhauled `rbd` collision engine. The new collision is available in the `12.2.0` release of [`rbd`](https://github.com/atlassian/react-beautiful-dnd).

Thanks to all the Atlassian's who helped work on this and to [Daniel Del Core](https://twitter.com/danieldelcore), [Michael Dougall](https://twitter.com/itsmadou) and [Ee Venn Soh](https://twitter.com/vennsoh) who helped put this blog together.

Cheers