hello, i'm anthony calzadilla

Anthony Calzadilla

Family man. Long-time web developer. Always sweating the details. Crazy about animation. Perpetually making (or breaking) things. Avid enthusiast of the absurd.

Sorry head-hunters, my job keeps me busy. But if you want to geek out about web stuff or just say hi. Hit me up on the social thingy's listed above.

BACK

Devil in the details: HOW TO BUILD A BULLETPROOF FULL-PAGE ROTATION ANIMATION THAT REVEALS A HIDDEN MENU WITH GREENSOCK (GSAP)

We’re going to build a simple-looking web page displaying one toggle button. When the toggle button is clicked, the page will rotate (-13°) degrees to reveal the navigation. Clicking the same button rotates the page (+13°) back into place.

We’ll use the GreenSock animation plugin to create performant UI animations based on the user’s interaction with the website.

The dynamic values that we will be working with are:

  • The current width of the user’s browser window.
  • The current vertical position of the page if the user has scrolled.

Click the Toggle button in the pen below to see the UI effect we are going to create.

See the Pen
DITD—Final Example
by Anthony Calzadilla (@clzd)
on CodePen.

Figure (1)

<div id="parent">
	<div id="child">
		<img src="thumbnail.png" alt="">
		<p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
		<img src="thumbnail.png" alt="">
		<p> ... </p>
	</div>
</div>

Create a #parent div. Make another #child div inside it with text, images and other content.

The following animated figures provide a conceptual overview of the UX animation. Each step is highlighted as an example with a corresponding text description. Explaining the logic, techniques and challenges involved for each example.

Figure (2)

Figured 02
  • Example (A)
    The #child will expand vertically to accommodate any text and/or content inside it. Likewise, #parent will expand to accommodate the height of #child.

Figure (3)

Figured 03
  • Example (B)
    We want to rotate #parent (-13°) degrees from the bottom-right corner of the content for the desired effect.
  • Example (C)
    But the longer the content is, the more horizontal distance is caused by the same rotational value of (-13°) degrees.

Figure (4)

Figured 04

THE FIX:
Remove #parent from the ‘flow’ of the document by switching its CSS to position:fixed. Then set its top, left, right and bottom properties to 0. Forcing its dimensions to be equal to the Window’s.

  • Example (D)
    Note: The longer the content is, the more distance caused by the rotational value of (-13°) degrees.
  • Example (E)
    Setting #parent‘s CSS to transform-origin:100% 100%. Makes the rotation point the bottom-right corner of the Window. Ensuring a consistent rotational distance regardless of content length.

THE VERTICAL POSITION GLITCH:
Because #parent was removed from the natural flow of the web page by setting CSS to position:fixed. #child loses its vertical position and defaults back to the top of the page. This is bad UX because user loses their place within the page they had scrolled to.

Figure (5)

Figured 05

ANOTHER FIX:
On click we get the pageOffsetY value of #child. Then use that value to move #child up when #parent ‘s CSS is set to position:fixed.

Figure (5) breaks down several steps that occur simultaneously. Although they appear as several linear examples for clarity. The following events happen all at once.

  • Example (F)
    Listening to the Window’s scroll-events and saving the value of distance scrolled in a variable.
  • Example (G)
    Note: Upon setting #parent to position: fixed, it loses its place in the flow of the document and #child ‘s vertical position resets to the top of the Window because it automatically defaults to its next closest ancestor element.
  • Example (H)
    Taking the previously saved pageYoffset value and using it as a negative translateY(-pageYoffset) property/value to move the content up by the distance scrolled. Preserving the user’s scroll position during the opening hinge animation.
  • Example (I)
    On close, we simultaneously remove the fixed positioning on #parent and the negative pageOffsetY of #child, rotate/animate the #parent back into its original position and set #parent back to position: static, remove the pageOffsetY value transformation value.

GSAP Code to Start Rotation Animation

Below is the code for the function that rotates the page when the user clicks toggle button. There are comments describing the important parts of the function. Please note that in the example below there are calls to other functions not illustrated in this post. Dive into the pen on CodePen for the full working code.

menuIn: () => {
    s.eBool = false;
    // Save the current scroll position to saveScroll
    s.saveScroll = s.last_known_scroll_position;
    // Create New GSAP Timeline
    var tl = new TimelineMax();
    tl.set(".nav_icon", { trasformOrigin: "50% 50%" }, "start")
    .set(".nav_icon path", { fill: "#666" }, "start")
    // Remove pointer events while page open animation is running
    .set([s.elBtn], { pointerEvents: "none", transformOrigin: "50% 50%" }, "start")
    .addLabel("go", "+=0.25")
    // Add 'go' class to <body>
    .call(MenuToggle.bodyClass, ["go"], this, "go")
    // Set <body> to overflow hidden
    .set(s.elBody, { overflow: "hidden" }, "go")
    // Move <#child>'s vertical position upwards using negative value of s.last_known_scroll_position
    .set(s.elChild, { y: -s.last_known_scroll_position }, "go")
    // Switch <#page> to position fixed and width/height 100% so it matches any Window 
    .set(s.elParent, { position: "fixed", overflow: "hidden", width: "100%", height: "100%" }, "go")
    // Rotate <#page> from the bottom-right corner of Window over 0.75 seconds
    .to(s.elParent, 0.75, { transformOrigin: "100% 100%", rotation: s.pageRotation, y: s.pageTop, ease: Back.easeOut.config(1.75) }, "open")
    // Scale toggle button 1.5x bigger over 0.25 seconds
    .to(s.elBtn, 0.25, { scale: 1.5, ease: Power2.easeIn }, "open")
    // Stagger in the social nav icons
    .staggerFrom(".nav_icon", 0.75, { y: "+=15px", scale: 0, autoAlpha: 0, rotation: -3, ease: Elastic.easeOut.config(1, 0.5) }, 0.15, "open")
    // Return pointer events to toggle button
    .set(s.elBtn, { pointerEvents: "all" });

    return tl;
}

GSAP Code to End Rotation Animation

Below is the code for the function that rotates the page back into its original position when the user clicks toggle button. There are comments describing the important parts of the function. Please note that in the example below there are calls to other functions not illustrated in this post. Dive into the pen on CodePen for the full working code.

 menuOut: () => {
    s.eBool = true;
    // Create New GSAP Timeline
    let tl = new TimelineMax();
    // Remove pointer events while page open animation is running
    tl.set(s.elBtn, { pointerEvents: "none" }, "abc")
    // Scale toggle button back to 1 over 0.1 seconds
    .to(s.elBtn, 0.1, { scale: 1, ease: Power2.easeIn }, "rotateOut")
    // Rotate <#page> back into original position over 0.45 seconds
    .to(s.elParent, 0.45, { rotation: 0, y: 0, ease: Back.easeIn.config(2) }, "rotateOut")
    // Fade and Scale social icons to 0 over 0.25 seconds
    .to(".nav_icon", 0.25, { scale: 0, autoAlpha: 0 }, "rotateOut")
    .to("html", 0.25, { backgroundColor: "#191718" }, "rotateOut+=0.28")
    // Switch <#page> to position relative and overflow to visible for scrolling 
    .set(s.elParent, { position: "relative", overflow: "visible", width: "100%", height: "100%" }, "out")
    // Clear inline style CSS of negative s.last_known_scroll_position added by GSAP
    .set(s.elChild, { clearProps: "all" }, "out")
    // Set overflow to visible for scrolling
    .set(s.elBody, { overflow: "visible" }, "out")
    // Call unScroll() to reinstate users last scroll position before menu open
    .call(MenuToggle.unScroll)
    // Return pointer events to toggle button
    .set(s.elBtn, { pointerEvents: "all" })
    .set(".nav_icon", { clearProps: "all" });

    $('body').removeClass('go');
    return tl;
}               

Final Code

Below is the finished example. You can experiment directly with the code on Codepen.

See the Pen
DITD—Final Example
by Anthony Calzadilla (@clzd)
on CodePen.

BACK