The Boxer

Well, a man had only so many fights in him, to begin with. It was the iron law of the game. One man might have a hundred hard fights in him, another man only twenty; each, according to the make of him and the quality of his fiber, had a definite number, and when he had fought them he was done. Yes, he had had more fights in him than most of them, and he had had far more than his share of the hard, grueling fights.

Recently a number of sites, including Bioware’s promotional page for Mass Effect Andromeda and Active Theory’s work for Under Armour have featured a parallax effect that ties mouse movement to the motion of a page element in the opposite direction. I was interested in recreating the effect using vanilla JavaScript and modern CSS, an example of which you can see above.

Get In The Ring

The markup consists of a <div> container, an image, a heading, and body text (extracted from the short story “A Piece of Steak” by Jack London) surrounded by another <div>:

<div id="boxercontainer">
  <img src="boxer.png" alt>
  <div>
    <h1>The Boxer</h1>
    <p>Well, a man had only so many fights in him, to begin with…
  </div>
</div>

The boxer was extracted from his background and turned into an alpha-masked PNG, reduced in file size by converting it to a 16-bit image.

The outer #boxercontainer element is styled: note that the background-image is made slightly larger than its container and positioned from its center, facts that will be important later:

#boxercontainer {
  width: 80%;
  max-width: 900px;
  margin: 0 auto;
  background-image: url(concrete-background.jpg);
  position: relative;
  padding-bottom: 45%;
  background-size: 120% 120%;
  background-position: 50% 50%;
  overflow: hidden;
  min-height: 650px;
}

The inner div is given a transparent, slightly dark background color to increase the contrast of the text inside it:

#boxercontainer div {
  position: absolute;
  width: 60%;
  left: 20px;
  top: 20px;
  border: 1px solid #fff;
  padding: 2rem;
  background: rgba(0,0,0,0.2);
}

The image is also positioned absolutely (taking advantage of the absolute-position-inside-relative trick), dropped down so that the lower portion of the boxer’s legs are hidden by the overflow: hidden on the parent element. It’s also provided with a drop-shadow filter, making a far smaller image file size than baking the drop-shadow into the PNG; as we’ll see in a moment, this also createsfar more dynamic options for the image.

#boxercontainer img { 
  position: absolute;
  bottom: -35px;
  right: 50px;
  width: 40%;
  filter: drop-shadow(-200px 200px 50px #000);
  padding: 1rem;
  z-index: 2;
}

The z-index value of 2 places the boxer visually above the div containing the text, but it also means that the shadow falls over the words. To correct that, I modified the appearance of the paragraph text:

Boxer image
#boxercontainer p {
  position: relative;
  z-index: 3;
}

With a higher z-index, the shadow now falls under the text, but over the border on the div.

Getting Centered

The core idea of the parallax effect is finding the center of the affected element. The cursor’s distance from this center will displace the element by the same distance in the opposite direction:

Cursor over center of element (left); cursor moved away from element, with element offset an equal distance (original center marked with ×).

To make this happen, the script starts by identifying the elements (using the fact that an id automatically turns into a reference in JavaScript, and using a querySelector for the boxer:

const boxer = boxercontainer.querySelector("img"),
maxMove = boxercontainer.offsetWidth / 30,
boxerCenterX = boxer.offsetLeft + (boxer.offsetWidth / 2),
boxerCenterY = boxer.offsetTop + (boxer.offsetHeight / 2);

maxMove is the maximum distance we want the boxer to move, since we don’t usually want the image to move with complete freedom in response to the mouse being anywhere on the page.

We’ll also need to determine the position of the mouse inside boxercontainer, for which I’ll use a function:

function getMousePos(xRef, yRef) {
  let panelRect = boxercontainer.getBoundingClientRect();
  return {
      x: Math.floor(xRef - panelRect.left) / 
        (panelRect.right - panelRect.left)*boxercontainer.offsetWidth,
      y: Math.floor(yRef - panelRect.top) / 
        (panelRect.bottom - panelRect.top) * boxercontainer.offsetHeight
    };
}

Float Like a Butterfly

Effects like this usually respond to mouse movement in the page:

document.body.addEventListener("mousemove", function(e) {
  let mousePos = getMousePos(e.clientX, e.clientY),
  distX = mousePos.x - boxerCenterX,
  distY = mousePos.y - boxerCenterY;
  if (Math.abs(distX) < 500 && distY < 200) {
  boxer.style.transform = 
    "translate("+(-1 * distX) / 12 + "px," + (-1 * distY) / 12 + "px)";
  }
})

distX is the horizontal distance between the current mouse position and the initial center of the boxer image; distY is the vertical distance. If the vertical difference (positive or negative) is less than 500px, and the horizontal distance less than 200px, then we move the boxer image using a CSS transform:

  • both distances are multiplied by -1 (turning a positive distance into a negative, and a negative into a positive)
  • the result is then divided by 12, to reduce the ratio of movement between the mouse and the image (a 1:1 relationship between mouse position and image position would make movement far too confusing).

When we move our head position, it’s not only elements in the foreground that change position; objects in the background also shift, in the opposite direction. Think of an object placed a few feet in front of a wall: moving to the right will change your perspective on both.

To recreate this effect, I also move the background-image of the concrete wall. The addEventListener callback has the following added at the end of the script:

boxercontainer.style.backgroundPosition = 
    `calc(50% + ${distX / 50}px) calc(50% + ${distY / 50}px)`;

To make the background move, I’m using calc to produce an offset from the background image’s default center position. (I’m also using template literals to make concatenation easier).

Counted Out

As a general rule, parallax effects work well on larger desktop and laptop screens, but not at all on smaller viewports. Because the effect is created with JavaScript, we must also detect the viewport width with JS, via matchMedia:

let fluidboxer = window.matchMedia("(min-width: 726px)");

The if condition for moving the image and background changes to:

if (Math.abs(distX) < 500 && distY < 200 && fluidboxer.matches) { … }

There are more changes in CSS @media queries to change the design at smaller viewport sizes; see the CodePen demo for more details.

Punch-Drunk

Parallax effects can adversely affect users with vestibular disorders (sensitivity to visual motion and change) to the point of nausea and seizure; at the minimum, displaying a warning or some other indication that the page contains motion effects is a good idea. Browser vendors are currently working on a “reduce motion” user setting that will be detectable using an @media query in the near future; in response, features like parallax effects could be automatically turned off on the page for affected users.

Conclusion

Using CSS transforms with JavaScript mouse events can be a cheap and relatively easy way to achieve parallax effects, with a little math. In future articles, I’ll explore these kinds of effects further.

Photograph by Terry George, licensed under Creative Commons

Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/qqqQNe