Creating a Realistic Paper Tearing Animation using React Spring
Lactose-free buttery animations
Long gone are the days where you just wrap your components in ReactTransitionGroups and hope to dear life that your animations chain one after another to get that sweet buttery animation. But now we have the better butter, a buff butter that never buffers (your daily tongue twister from a lovely mister). I am, of course, talking about React Spring. It’s an animation library that emulates spring physics into our app so the animations appear smoother.
I’ve been working on a productivity app, and one thing I’ve wanted to emulate is my use of Post-it notes to keep track of things to do throughout the day. And if I want an app to recreate the experience of writing and maintaining those notes, then I need to emulate the material that makes those notes - Paper
Setting up the Butter
Let’s set up the foundations for our app. We’re going to need to recreate a wooden board, and then stick our bits of paper on that board. Along with their colors being consistent with the real world, we’d also want them to feel like real-world objects.
We’re going to use some lovely textures from the wonderful folks at TransparentTextures to accomplish this. This is what our Home component looks like
export default function Home() {
return (
<div className={list}>
<div className={listHeader}>Today</div>
<TodoList />
</div>
)
}
Here is our TodoList
import Todo from './Todo'
import { todolist } from './TodoList.module.scss'export default function TodoList (props) {
let todos = [{"id":"2bd7cbc716", "name":"Research and design a date picker","isChecked":false},
{"id":"efc6e1edc4","name":"Create the Add Todo view","isChecked":false},
{"id":"72bed57be0","name":"Animate the Check Todo action","isChecked":false},
{"id":"0be851c06b","name":"Make the Todo call","isChecked":false},
{"id":"72634b2dcf","name":"Do things","isChecked":false}]return (
<div className={todolist}>
{todos.map(action => {
return <Todo key={action.id} action={action}></Todo>
})}
</div>
)
}
And finally, our Todo
import { todo, todoWrapper, name, isChecked } from './TodoList.module.scss'
import cn from 'classnames/bind'import SwipeTodo from '../SwipeableItem/SwipeTodo';let cx = cn.bind({todo, isChecked});export default function Todo({key, action}) {return (
<div className={todoWrapper}>
<SwipeTodo action={action}>
<div className={cx({todo: true, isChecked: action.isChecked})}>
<div className={name}>{action.name}</div>
</div>
</SwipeTodo>
</div>
)
}
Don’t worry about the SwipeTodo or the { } brackets around the className. I’m using CSS Modules, but how I declare my CSS still looks the same-
.list {
padding: 5px 0px;
height: 100%;
overflow: auto;
background: $primary;
background-image: url("https://www.transparenttextures.com/patterns/wood-pattern.png");
border-radius: 5px;
}.listHeader {
color: $black;
font-family: marker;
text-align: center;
font-size: 30px;
}.focusFooter {
display: flex;
justify-content: flex-end;
margin-top: 10px;
margin-right: 20px;
margin-bottom: 20px;
}.todolist {
display: block;
margin: 10px 20px;
border-radius: 5px;
border: 0;
box-shadow: 0px 3px 2px -1px rgba(0,0,0,0.2),
0px 2px 2px 0px rgba(0,0,0,0.04)
;display: flex;
flex-flow: row wrap;
box-shadow: none;
justify-content: space-evenly;
margin: 5px 20px;}.todoWrapper {
width: 150px;
max-width: 150px;
height: 150px;
margin: 5px;
flex-basis: 45%;
font-family: Permanent Marker, Arial, Helvetica, sans-serif;
user-select: none;
position: relative;
}.todo {
width: 150px;
height: 150px;
border-radius: 2px;
justify-content: center;
font-family: Permanent Marker, Arial, Helvetica, sans-serif;
background: adjust-hue($secondary, 5%);
background-image: url("https://www.transparenttextures.com/patterns/embossed-paper.png");
border: none;
box-shadow: 0px 3px 2px -1px rgba(0,0,0,0.2),
0px 2px 2px 0px rgba(0,0,0,0.04) ; position: absolute;
left: 0;
}.name {
font-size: 18px;
text-align: center;
color: black;
}
The font we use here is the Permanent Marker
Churning the Butter
In case I’m getting too heavy-handed with the butter analogies do let me know, I’ll make a note of it in my diary.
In order to understand how our SwipeTodo component works, we need to understand how a paper actually tears from wood. The paper can be broken down into two major parts
- At the start, the front side of the paper is shown
- As it is torn away, the backsides shows and increase in width, while we see less of the front of the paper
- As more is peeled away, the backside moves to the right, so it seems like the front side is “turning itself” into the backside of the paper
Now that we understand the working of a real paper peel, let’s see how we can implement it in code. Firstly, we want to style the back paper different from the front. We do that by choosing a lighter color and a smooth texture (this is where our use of textures pays off) to differentiate it. And we give it a higher z-index and a box-shadow to make it look like it's over the front of the paper
.swiper {
position: absolute;
right: 0;
height: 100%;
overflow: hidden;
width: 100%;
}.backPaper {
height: 100%;
position: absolute;
left: 0;
width: 10px;
background: $secondary-lighter;
z-index: 5;
box-shadow: 4px 5px 15px -2px rgba(0,0,0,0.4);
}.frontPaper {
width: 150px;
height: 150px;
position: absolute;
}
Another way to understand the positioning in this is that we are aligning the parent (.swiper) to the right of its parent (.todoWrapper), which we will be moving towards the right. So swiper makes sure our paper moves to the right, so .backPaper and .frontPaper only need to control their width and position with respect to .swiper.
Now let’s get into the code of SwipeTodo, and how we use React Spring to implement this motion so its smooth and buttery (I’m obsessed with that word aren’t I)
import { useSpring, animated, interpolate } from 'react-spring';
import { useDrag } from 'react-use-gesture';
import { backPaper, frontPaper, swiper } from '../TodoList/TodoList.module.scss';export default function SwipeTodo({action, children}) {
const [{x, y}, set] = useSpring(() => ({x: 0, y: 0}));
const bindSwipe = useDrag(({down, movement: [mx, my], velocity}) => {
let realisticX = mx * (1 + velocity);
let realisticY = my;if (!down) {
// either torn or back based on current position
realisticX = realisticX > 150? window.innerWidth: 0;
}set({x: realisticX, y: realisticY});
});return (
<animated.div key={action.id} {...bindSwipe()} className={swiper} style={{overflow: 'hidden', width: '100%', right: interpolate([x], (x) => `-${x}px`)}}>
<animated.div className={backPaper} style={{width: interpolate([x], x=> `${x}px`)}}></animated.div>
<animated.div className={frontPaper} style={{left: interpolate([x], x => `-${x}px`)}}>
{children}
</animated.div>
</animated.div>)
}
Okay, that might be a lot of new code to understand in one go, so lets go through it line by line. Let’s go through the logic of calculating the x and y
const [{x, y}, set] = useSpring(() => ({x: 0, y: 0}));
const bindSwipe = useDrag(({down, movement: [mx, my], velocity}) => {
let realisticX = mx * (1 + velocity);
let realisticY = my;if (!down) {
// either peels away or snaps back based on it's x value
realisticX = realisticX > 150? window.innerWidth: 0;
}set({x: realisticX, y: realisticY});
});
- useSpring here just generates a new spring so our animations are smooth. Think of it this way - if our x and y change from 0 to a 100, we want it to slowly tick up to it, not in one go. Also the set function allows us to manually set the x and y values that the spring will act on
- useDrag is a function that we use to track the “drag” gesture of the user. It gives us the x-y coordinates, velocity of the swipe and when it ends
- We use realisticX to set the value of x (y we will ignore). We want the velocity to have an impact on the x, so the paper moves faster if flicked
- We also use a threshold when the user lets go of the paper. If they let go after 150px (it’s width) then we set the x to the ends of the window
<animated.div key={action.id} {...bindSwipe()} className={swiper} style={{overflow: 'hidden', width: '100%', right: interpolate([x], (x) => `-${x}px`)}}>
<animated.div className={backPaper} style={{width: interpolate([x], x=> `${x}px`)}}></animated.div>
<animated.div className={frontPaper} style={{left: interpolate([x], x => `-${x}px`)}}>
{children}
</animated.div>
</animated.div>
- animated.div is just a helper element that can read our changing x and y values and update the dom accordingly
- We use the interpolate helper to read the x value and update the “right”, “left” and “width” values for our elements
Voila! It’s time to enjoy some of that lactose-free goodness
Spread the Butter
Yes, before you ask, I had to show another gif of that animation. For science reasons.
Some might say my deep dive into paper physics might be overkill. And they might be right. My family is dying and I‘m missing a few limbs due to an unrelated black market paper acquisition deal. But it’s all worth it
Where do we go from here
To be honest there are a few improvements that can be made-
- Instead of the whole paper sticking to the board, we can have just the top half stick, like a post-it note. Though that is a little more complicated, involving triangles instead of rectangles for the peel off
- We ignored the y-axis during this implementation. We could incorporate a tearing off motion that works on both the axes
Both these improvements are a little complicated, and I might make a follow-up blog post if I see interest in this pique up. In the meantime, I need to try to get back my organs from the black market. They will accept views on Medium as a currency, won’t they?
Note: No cows were harmed in the making of this butter