Use React to make a photo follow the mouse (aka transform perspective or tilt)

Adam Mackintosh
18 min readMar 11, 2018

--

We have seen photos like this that move with the mouse:

Fig 1.0 — Dat Perspective

I’m going to let you know right now, this effect can produce some amazing looking results. There is something magical that happens when photos and/or your entire UI achieve a floating look. Maybe it’s trendy, maybe it’s Maybelline; Surely, it’s rad ✓.

TURBO USERS: Grab the completed files from GitHub:

The GIF JIF above shows us what we are making and/or learning, but we are going to use some technologies:

  • React JS Environment
  • ES6 JavaScript
  • Spread Operator
  • findDOMNode (the one your mother warned you about)
  • getBoundingClientRect
  • requestAnimationFrame
  • Brutal Mathematics (just kidding)

Depending where the mouse moves with respect to the image, we are going to mangle the photo dimensions using CSS. Simmer down, it’s not that crazy if we break down the process into manageable chunks.

We need React to watch two things:

  1. the X and Y coordinates of the photo
  2. the X and Y coordinates of the mouse

If you encounter any difficulties, post a comment. I will update the article. You have to read the whole article first though. Remember, there is no such thing as a stupid question. As we detail, I will take opportunities to explain why we use certain techniques.

Let’s do this. I will write more articles if you clap at least zero times.

I recommend following me on Twitter as well. I try to constantly drop engineering gems, especially full-stack JavaScript related:

Phase 1: render the image

Let’s slap some boilerplate in, so we’re all on the same page. Fire up Create-React-App (CRA) from your local wizards at Facebook. If you aren’t using CRA, you should consider it because it brings an emphasis on zero-config or at least minimal config. No one likes to spend 700 hours configuring their app before they start developing it, not that there’s anything wrong with that.

You have two options:

  1. Clone the GitHub repo and read the project’s App.js file.
  2. Continue reading and type now in your terminal: create-react-app react-tilt

Once you get CRA running or your preferred React environment, get in a position to add this:

import React, { Component } from 'react'
import dogBackground from './dogBackground.jpg'
const styles = {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
}
class TiltPhaseOne extends Component {
render() {
return (
<div style={styles}>
<h1>Mouse over the photo:</h1>
<img src={dogBackground} alt="" />
</div>
)
}
}
export default TiltPhaseOne

Do what you need to cause this above code to render.

Right click, ‘Save As’ on this photo:

Fig 1.0 — Dog Background

Phase 2: track mouse movements

For this task, we look at these Synthetic Events:

  • onMouseEnter
  • onMouseMove
  • onMouseLeave

React Synthetic Events

Sounds pretty intuitive right? These are React Synthetic Events that fire on those events. It’s very important to understand — React does not handle events like you would expect in vanilla JS. The bottom line is React manages these events without us requiring to start and stop the handlers manually.

If you want to read more, I recommend starting with the React Documentation:

If you want to dig deeper, start with this article:

We made our component a Class so we can sprinkle some methods into it (and manage state as well, because Classes are for Components that deal with behavior right?).

Let’s add the constructor and the three handlers. We also need to add a wrapper div around the photo so our component can become reusable:

import React, { Component } from 'react'
import dogBackground from './dogBackground.jpg'
const styles = {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
}
class TiltPhaseTwo extends Component {
constructor(props) {
super(props)
this.state = {}
this.handleMouseEnter = this.handleMouseEnter.bind(this, this.props.handleMouseEnter)
this.handleMouseMove = this.handleMouseMove.bind(this, this.props.handleMouseMove)
this.handleMouseLeave = this.handleMouseLeave.bind(this, this.props.handleMouseLeave)
}
handleMouseEnter(e) {
console.log('onMouseEnter', e.clientX, e.clientY)
}
handleMouseMove(e){
console.log(
'onMouseMove',
e.nativeEvent.clientX, e.nativeEvent.clientY
)
}
handleMouseLeave(e) {
console.log('onMouseLeave', e.clientX, e.clientY)
}
render() {
return (
<div style={styles}>
<h1>Mouse over the photo:</h1>
<div
style={{
display: 'flex',
justifyContent: 'center',
width: '100%',
height: '100%',
}}
onMouseEnter={this.handleMouseEnter}
onMouseMove={this.handleMouseMove}
onMouseLeave={this.handleMouseLeave}
>
<img src={dogBackground} alt="" />
</div>
</div>
)
}
}
export default TiltPhaseTwo

Run this code and press F12 to open the Dev Tools Console. Probe the event handlers. You will find your typical stuff available on e such as e.target.value (if we had an input field). More important to us, e.nativeEvent contains clientX and clientY. You may recall them from your previous JavaScript journeys.

We are not using fat arrow syntax for the mouse events because we will be intentionally passing around the context of this in callbacks. If you can get this working without binding in the constructor and with the code shortened a bit, please share in the comments.

On vs. Handle

Notice how we called the Class Methods handle rather than on. I like to remind people about the distinction between the two. on refers to the event on which we are doing something. handle refers to the action we are taking or the result of the event. When an event occurs, we are going to handle it with our Class Methods.

If we were delegating the handling up to a parent or calling back to some other location, we should use on. This is why you see callbacks that look like this:

<button onClick={onClick}>
Submit
</button>

We know the handling won’t be handled in that Component. Usage of on signals you to look upstream. React prefers unidirectional data flow. This is why we care to make the distinction. It helps us know where to look.

Now that we have our mouse-related events starting to be handled, what else do we need to do to get our photo tilting and warping?

e.nativeEvent

The React Documentation states:

If you find that you need the underlying browser event for some reason, simply use the nativeEvent attribute to get it.

React normally utilizes a synthetic event, which is a proxy to the original event. It’s like when a male human tries to contact a female human, and her brother steps in between to efficiently handle the event. The brother is the proxy. In cases like ours, we are interested in the raw DOM activity, so we usenativeEvent to signal to React that we want the DOM element directly, no post-processing, no frills, no gimmicks just raw performance. We are essentially cutting out the middleman because we don’t need him. We like optimizing performance. The female human’s brother appreciates good performance and hates janky performance.

Phase 3: track the photo coordinates

We are going to incrementally update your Class Methods. From now on when I show code, just replace the entire function with the new one (in case you get confused).

Import findDomNode in, and let’s store the div as a Class Property called element. Initializing it with the value of null tells future developers that this.element is a thing and that they will see it used later in the code.

I prefer if you manually type this code in. You will retain more secrets, but you can paste each function in:

import { findDOMNode } from 'react-dom'// ...constructor(props) {
super(props)
this.state = {}
this.element = null
this.width = null
this.height = null
this.left = null
this.top = null
// Remember, we are not using move or leave events yet,
// but add them now.
this.handleMouseEnter = this.handleMouseEnter.bind(this, this.props.handleMouseEnter)
this.handleMouseMove = this.handleMouseMove.bind(this, this.props.handleMouseMove)
this.handleMouseLeave = this.handleMouseLeave.bind(this, this.props.handleMouseLeave)
}
componentDidMount() {
this.element = findDOMNode(this)
}
// ...handleMouseEnter(e) {
this.updateElementPosition()
}
// ...updateElementPosition() {
const rect = this.element.getBoundingClientRect()
this.width = this.element.offsetWidth
this.height = this.element.offsetHeight
this.left = rect.left
this.top = rect.top
console.log('REKT', rect)
console.log('OFFSET WIDTH', this.element.offsetWidth)
console.log('OFFSET HEIGHT', this.element.offsetHeight)
}
// ...

this.element now contains a live reference to the DOM Node. That means persistent and real-time.

Recall that JavaScript is all about maintaining live references. It’s why immutability is a thing, and it’s why functions are first class citizens.

We are also introducing another Class Method called this.updateElementPostion() which fires on theonMouseEnter event. Let’s come back to that when we talk about getBoundingClientRect(). These assignments help us calculate the X and Y coordinates when your mouse enters the photo area.

findDOMNode

There is one key mention with this. You’ve probably heard people express a bit of hesitation in some cases when findDOMNode is mentioned. It provides direct access to the DOM Node, but React manages the DOM for us. That is the central reason we don’t want everybody to start linking directly to DOM Nodes. Not letting React manage your DOM elements is like paying an accountant to track every cent of your money and then losing receipts.

Hesitation is therefor a natural and justified response to this findDOMNode Kool-aid.

Let’s guzzle directly from the React Documentation:

If this component has been mounted into the DOM, [findDOMNode] returns the corresponding native browser DOM element. This method is useful for reading values out of the DOM, such as form field values and performing DOM measurements. In most cases, you can attach a ref to the DOM node and avoid using findDOMNode at all.

I point this out because just like e.nativeEvent, we specifically want that direct link to the DOM Node. What we are doing is read-only, so it’s fine. Direct access to read-only? Sounds like efficient data collection to me.

Fig 3.0 — Read-only is fine

Now that we have this, we just need to get the X and Y coordinates. For this, we utilize this.element.getBoundingClientRect(). Let’s explore that.

getBoundingClientRect

We always check the MDN first:

The returned value is a DOMRect object which is the union of the rectangles returned by getClientRects() for the element, i.e., the CSS border-boxes associated with the element. The result is the smallest rectangle which contains the entire element, with read-only left, top, right, bottom, x, y, width, and height properties describing the overall border-box in pixels. Properties other than width and height are relative to the top-left of the viewport.

Source: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect

Fig 3.1 — You what mate ??

Basically, getBoundingClientRect() allows us to grab the coordinates of an element’s origin and the dimensions of the screen. Your task at the moment is to examine those console.log()s and see what kind of data is there. Notice how the numbers change or don’t? Good, now we’re getting somewhere.

Imagine this kind of stuff while you are not only looking at those logs but also working with DOM elements in general:

Fig 3.2 — Graphing Coordinates

Imagine animating DOM elements. That type of work usually has start and finish coordinates. Pretty cool eh? Cartesian grids are cool because they unlock math and consistently repeatable results, assuming your numbers start and end correctly. You can see wildly incorrect results if just one value is off. This is the tight rope we walk in the DOM. The good news is the DOM is usually pretty declarative, so once you figure out the formula, it’s reuseable. Our work today will be. Thank dog.

getBoundingClientRect gets the X and Y coordinates and the width and height of a DOM element.

Things are about to escalate very quickly, but all we are doing is re-calculating where the mouse is with respect to the photo. We are doing that every time the mouse moves via the onMouseMove event. If you get overwhelmed, just imagine we are declaring a couple formulas and telling React to go nuts with them every time the mouse moves.

We need these numbers and this math because we are about to start calculating distances and positions that are relative to a known origin. We need that type of information because we are going to bend the perspective using the CSS transform property. It’s fine if there is some magic still.

Phase 4: calculate necessary changes

We are going to need to talk about each function. Let’s start by updating our class for Phase 4. Here’s what I want you to do:

  1. paste all of this code in as the guts of your Class
  2. fix the formatting
  3. look at the type of things that are happening in the code
  4. take your time, this is serious learning potential
  5. join me at the bottom of this code block:

NOTE: Remember, I said type it all out manually. I am super serious about that. I want you to internalize and recruit every neuron. It takes too long? I typed out this whole article. You won’t remember anything while you are pasting.

// This is your entire Class now
// with Phase 5 included but commented out.
constructor(props) {
super(props)
this.state = {
style: {}
}
const defaultSettings = {
reverse: false,
max: 35,
perspective: 1000,
easing: 'cubic-bezier(.03,.98,.52,.99)',
scale: '1.1',
speed: '1000',
transition: true,
axis: null,
reset: true
}
this.width = null
this.height = null
this.left = null
this.top = null
this.transitionTimeout = null
this.updateCall = null
this.element = null
this.settings = {
...defaultSettings,
...this.props.options,
}
this.reverse = this.settings.reverse ? -1 : 1
this.handleMouseEnter = this.handleMouseEnter.bind(this, this.props.handleMouseEnter)
this.handleMouseMove = this.handleMouseMove.bind(this, this.props.handleMouseMove)
this.handleMouseLeave = this.handleMouseLeave.bind(this, this.props.handleMouseLeave)
}
componentDidMount() {
this.element = findDOMNode(this)
}
// componentWillUnmount() {
// clearTimeout(this.transitionTimeout)
// cancelAnimationFrame(this.updateCall)
// }
handleMouseEnter(cb = () => { }, e) {
this.updateElementPosition()
// this.setState(prevState => ({
// style: {
// ...prevState.style,
// }
// }))
// this.setTransition()
// return cb(e)
}
// reset() {
// window.requestAnimationFrame(() => {
// console.log('RESETTING TRANSFORM STATE', `perspective(${this.settings.perspective}px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)`)
// this.setState(prevState => ({
// style: {
// ...prevState.style,
// transform: `perspective(${this.settings.perspective}px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)`,
// }
// }))
// })
// }
handleMouseMove(cb = () => { }, e) {
e.persist()
if (this.updateCall !== null) {
window.cancelAnimationFrame(this.updateCall)
}
this.event = e
this.updateCall = requestAnimationFrame(this.update.bind(this, e))
return cb(e)
}
// setTransition() {
// clearTimeout(this.transitionTimeout)
// console.log('SET TRANSITION', `Speed: ${this.settings.speed}ms Easing: ${this.settings.easing}`)
// this.setState(prevState => ({
// style: {
// ...prevState.style,
// transition: `${this.settings.speed}ms ${this.settings.easing}`,
// }
// }))
// this.transitionTimeout = setTimeout(() => {
// console.log('TRANSITION COMPLETE')
// this.setState(prevState => ({
// style: {
// ...prevState.style,
// transition: '',
// }
// }))
// }, this.settings.speed)
// }
handleMouseLeave(cb = () => { }, e) {
// this.setTransition()
// if (this.settings.reset) {
// this.reset()
// }
// return cb(e)
}
getValues(e) {
const x = (e.nativeEvent.clientX - this.left) / this.width
const y = (e.nativeEvent.clientY - this.top) / this.height
const _x = Math.min(Math.max(x, 0), 1)
const _y = Math.min(Math.max(y, 0), 1)
const tiltX = (this.reverse * (this.settings.max / 2 - _x * this.settings.max)).toFixed(2)
const tiltY = (this.reverse * (_y * this.settings.max - this.settings.max / 2)).toFixed(2)
const percentageX = _x * 100
const percentageY = _y * 100
console.log('JUST GOT NEW VALUES', `X: ${x} Y: ${y} -- TILT X: ${tiltX} TILT Y: ${tiltY} -- TILT X%: ${percentageX} TILT Y%: ${percentageY}`)
console.log('Notice how X turned into percentageX.')
return {
tiltX,
tiltY,
percentageX,
percentageY,
}
}
updateElementPosition() {
const rect = this.element.getBoundingClientRect()
this.width = this.element.offsetWidth
this.height = this.element.offsetHeight
this.left = rect.left
this.top = rect.top
}
update(e) {
let values = this.getValues(e)
console.log('VALUES', values)
console.log('NEW CSS TRANSFORM VALUES', `perspective(${this.settings.perspective}px) rotateX(${this.settings.axis === 'x' ? 0 : values.tiltY}deg) rotateY(${this.settings.axis === 'y' ? 0 : values.tiltX}deg) scale3d(${this.settings.scale}, ${this.settings.scale}, ${this.settings.scale})`)
// this.setState(prevState => ({
// style: {
// ...prevState.style,
// transform: `perspective(${this.settings.perspective}px) rotateX(${this.settings.axis === 'x' ? 0 : values.tiltY}deg) rotateY(${this.settings.axis === 'y' ? 0 : values.tiltX}deg) scale3d(${this.settings.scale}, ${this.settings.scale}, ${this.settings.scale})`,
// }
// }))
this.updateCall = null
}
render() {
const style = {
...this.props.style,
...this.state.style
}
return (
<div style={styles}>
<h1>Mouse over the photo:</h1>
<div
style={style}
onMouseEnter={this.handleMouseEnter}
onMouseMove={this.handleMouseMove}
onMouseLeave={this.handleMouseLeave}
>
<img src={dogBackground} alt="" />
</div>
</div>
)
}

Ok, perfect, now just examine this photo real quick:

Fig 4.0 — Just wait ‘til this code his 88 mph

Start at the top of the code. Let’s work down. Remember, this is Phase 4. We only care about what we are calculating, not about what CSS we are applying yet.

Our goal is to supply the CSS with the values it needs to change the perspective of the image. It started as a rectangle, but we are tilting it. For example, if we tilt it to the right, the right side will appear farther away, so the length of the right side will get smaller. The corners were 90 degrees, but now they are less on the right side:

Fig 4.1 — Image Perspective

EDIT: I noticed while reading this article a week after after publishing that my wording above is a bit suspect mathematically.

In reality, all 4 corners always add up to 360 degrees. In Fig 4.1, all 4 corners are 90 degrees for the white square. For blue, the opposing corners are the inverse of eachother. Recall from math class that opposing corners add up to 180 degress. This is how you can solve for unknowns. If you know the bottom left corner is 70 degrees and something + 70 = 180, then you can deduce that the top-right corner is 110 degrees.

CSS is going to handle this math for us. We just need to know a couple X and Y coordinates and where the mouse is at the moment of calculation. I have been omitting the Z axis, but take a look at this 2 minute video here before we go any further:

Vid 4.2–3D Web Design

When we tilt our image, it gives the illusion of 3D movement. We are bordering into some next-level stuff here.

Default Settings

Since we are making a reusable component, we need some default settings.

  • reverse: if true, photo will transform in the opposite direction
  • max: defines the tilt rotation amount (in degrees)
  • perspective: the transform perspective, (lower = more extreme)
  • easing: allows you to change the easing style and values (default is great)
  • scale: amount to zoom in onMouseEnter (ie: 1 = 100%, 2 = 200%)
  • speed: effect transition speed (in milliseconds)
  • transition: transition on or off (boolean)
  • axis: disable an axis (string, value can be X or Y)
  • reset: reset the effect onMouseLeave (boolean)

Feel free to invent your own. Now that we have some formulas in place, you can jigger them to meet your desires or your project’s requirements. Remember, you can pass these props into your component later for awesome dynamic control.

Notice this.settings. See how we are spreading the defaultSettings in and then overwriting those defaults with this.props.options? this.props.options is an object that has a key for each setting described above. Cool!

We setup the Tilt component to accept configuration settings that we can change them on the fly, even automatically as React updates state! Whaaaat! This config object pattern is one of my favorite ways to design components. It’s pretty much mandatory for versatility reasons.

Additional Class Properties

We have a couple extra Class Properties now because they are holding the state. We are avoiding setState because we don’t want to trigger any unecessary re-rendering. As you could imagine, we are trending towards the worst idea ever when we consider re-renderingonMouseMove.

requestAnimationFrame

This helps execute animation related JavaScript efficiently. With it, we are telling the browser we want to load up on calls to this.update(). It helps us avoid using setTimeout and setInterval. Those can be unruly and janky. You can read more about it here, here, and here:

Obviously, every time your mouse moves, which could be a lot when you are like, “oh hey, look at that cool animation. Let’s trigger it repeatedly!” requestAnimationFrame helps us avoid detonating the browser. What a time to be alive.

For the sake of thoroughness and clarity. I referred to it once before, but there is a concept known as Jank or jankyness when working with UX/UI. The browser is doing what we call repaints and reflows. The important thing is that it does this, and then it calculates a number of things and then repaints again.

JANK: If the browser needs to repaint before it is done calculating everything it tries to, you will see this janky behavior because the browser basically abandons the work it was doing to keeps going. It’s hard to explain but easy to see. You see it when you see choppy looking animations.

I almost forgot to mention that requestAnimationFrame also stops consuming CPU in inactive browser tabs. If requestAnimationFrame was a flavor, it would taste really good.

getValues

There is a bit of a chain reaction going on, and that’s the only reason why this code looks a bit crazy.

  1. requestAnimationFrame triggers this.update().
  2. this.update() calls this.getValues() which is the function that does all our dirty work. It re-calculates the X and Y position of the mouse with respect to the container and calculates the tilt percentage. Our CSS will eat these numbers up.
  3. Once this.update() gets those values, it updates the CSS. It’s actually kind of simple if you ignore all the complexity, wait wut?

Let’s revisit the chain of actions again:

  1. Mouse moves (handler fires)
  2. X and Y values update (state updates)
  3. Re-render with the new CSS (state or props just changed)

Phase 5: update the CSS

Now, uncomment everything starting from the top and let’s examine them real quick to ensure no one gets left in the dust.

componentWillUnmount

We added a componentWillUnmount Lifecycle Method which cleans up leftover garbage when the Component unmounts.

handleMouseEnter

We talked about this.updateElementPosition(). Now, we’ve added this.setTransition() which handles the transition as your mouse enters or leaves the container. The objective of this method is to aid with a smooth transition or at least a custom transition. The exact effects depend on your default settings and desires.

Callbacks — There are some callbacks sprinkled around the Class. You can spot them by looking forcb(e). These are to aid with the asynchronous operations. They allow the code to operate asynchronously but also sequentially. We care about this because we don’t want to block the main thread, and we don’t want undefined values by reading at the wrong time.

reset

We need to update the CSS onMouseLeave because we may wish for the container/image to quickly snap back to its original position… or we may not. Notice how this.reset() is modifying the transform property. This could straighten the edges.

Fig 5.0 — CSS Transform

setTransition

Both onMouseEnter and onMouseLeave present opportunities to trigger a function that handles a transition-type animation. With this opportunity, you can control the speed and transition effects.

onMouseLeave

When the mouse leaves, we can optionally reset as described above. This is where the reset function is fired from.

Fig 5.1 — Reset the effects onMouseLeave

Phase 6: make the component reusable

Congratulations, you now understand some pretty advanced stuff.

Fig 6.0 — Just add water, in the form of mental sweat

We aren’t done yet, however. We need to make the component reusable. We need to make this a really badass unit.

What will our strategy be?

  1. Component should be a single file
  2. Component should accept a config object
  3. Component should support children

Here is my recommendation:

NOTE: If you are turbo-scrolling and want the solution, paste this:

import React, { Component } from 'react'
import { findDOMNode } from 'react-dom'
class TiltPhaseSix extends Component {
constructor(props) {
super(props)
this.state = {
style: {}
}
const defaultSettings = {
reverse: false,
max: 35,
perspective: 1000,
easing: 'cubic-bezier(.03,.98,.52,.99)',
scale: '1.1',
speed: '1000',
transition: true,
axis: null,
reset: true
}
this.width = null
this.height = null
this.left = null
this.top = null
this.transitionTimeout = null
this.updateCall = null
this.element = null
this.settings = {
...defaultSettings,
...this.props.options,
}
this.reverse = this.settings.reverse ? -1 : 1
this.handleMouseEnter = this.handleMouseEnter.bind(this, this.props.handleMouseEnter)
this.handleMouseMove = this.handleMouseMove.bind(this, this.props.handleMouseMove)
this.handleMouseLeave = this.handleMouseLeave.bind(this, this.props.handleMouseLeave)
}
componentDidMount() {
this.element = findDOMNode(this)
}
componentWillUnmount() {
clearTimeout(this.transitionTimeout)
cancelAnimationFrame(this.updateCall)
}
handleMouseEnter(cb = () => { }, e) {
this.updateElementPosition()
this.setTransition()
return cb(e)
}
reset() {
window.requestAnimationFrame(() => {
this.setState(prevState => ({
style: {
...prevState.style,
transform: `perspective(${this.settings.perspective}px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)`,
}
}))
})
}
handleMouseMove(cb = () => { }, e) {
e.persist()
if (this.updateCall !== null) {
window.cancelAnimationFrame(this.updateCall)
}
this.event = e
this.updateCall = requestAnimationFrame(this.update.bind(this, e))
return cb(e)
}
setTransition() {
clearTimeout(this.transitionTimeout)
this.setState(prevState => ({
style: {
...prevState.style,
transition: `${this.settings.speed}ms ${this.settings.easing}`,
}
}))
this.transitionTimeout = setTimeout(() => {
this.setState(prevState => ({
style: {
...prevState.style,
transition: '',
}
}))
}, this.settings.speed)
}
handleMouseLeave(cb = () => { }, e) {
this.setTransition()
if (this.settings.reset) {
this.reset()
}
return cb(e)
}
getValues(e) {
const x = (e.nativeEvent.clientX - this.left) / this.width
const y = (e.nativeEvent.clientY - this.top) / this.height
const _x = Math.min(Math.max(x, 0), 1)
const _y = Math.min(Math.max(y, 0), 1)
const tiltX = (this.reverse * (this.settings.max / 2 - _x * this.settings.max)).toFixed(2)
const tiltY = (this.reverse * (_y * this.settings.max - this.settings.max / 2)).toFixed(2)
const percentageX = _x * 100
const percentageY = _y * 100
return {
tiltX,
tiltY,
percentageX,
percentageY,
}
}
updateElementPosition() {
const rect = this.element.getBoundingClientRect()
this.width = this.element.offsetWidth
this.height = this.element.offsetHeight
this.left = rect.left
this.top = rect.top
}
update(e) {
const values = this.getValues(e)
this.setState(prevState => ({
style: {
...prevState.style,
transform: `perspective(${this.settings.perspective}px) rotateX(${this.settings.axis === 'x' ? 0 : values.tiltY}deg) rotateY(${this.settings.axis === 'y' ? 0 : values.tiltX}deg) scale3d(${this.settings.scale}, ${this.settings.scale}, ${this.settings.scale})`,
}
}))
this.updateCall = null
}
render() {
const style = {
...this.props.style,
...this.state.style
}
return (
<div
style={style}
onMouseEnter={this.handleMouseEnter}
onMouseMove={this.handleMouseMove}
onMouseLeave={this.handleMouseLeave}
>
{this.props.children}
</div>
)
}
}
export default TiltPhaseSix

Conclusion

There you have it. You can create some awesome stuff now. Here is a sampler pack for how to use our Phase 6 refined gem:

import React, { Component } from 'react'
import TiltPhaseSix from './TiltPhaseSix'
import dogBackground from './dogBackground.jpg'
import dogForeground from './dogForeground.png'
import './App.css'
const options = {
max: 10,
perspective: 1000,
scale: 1.05,
}
class App extends Component {
render() {
return (
<div id="App">
<TiltPhaseSix
options={{}}
style={{
background: `url(${dogBackground}) no-repeat fixed center`,
backgroundSize: 'fit',
height: 700,
width: 740,
}}
>
<TiltPhaseSix
options={options}
>
<img src={dogForeground} alt="" />
</TiltPhaseSix>
</TiltPhaseSix>
</div>
)
}
}
export default App

That will render this:

Fig 7.1 — Completed dog

Here is some further inspiration:

Source: https://nrly.co/

Source: https://codepen.io/alexnoz/pen/brazWd

Follow me on Twitter. You will be glad you did :)

Have fun!

--

--

Adam Mackintosh

I prefer to work near ES6+, node.js, microservices, Neo4j, React, React Native; I compose functions and avoid classes unless private state is desired.