Graduate Program KB

Module 8 - Animations

Transforms

  • The transform function allows elements to change in some way

  • translate can shift an element horizontally or vertically

    • Its direction can be set with positive and negative values
    • Setting a length unit as a percentage will refer to the element's own size rather than the space in the container
    • The item's in-flow position is unaffected in any layout mode, other adjacent elements aren't manipulated
    • translateX and translateY properties perform the same thing but for a single axis
    transform: translate(0px, 0px);
    
  • scale takes in a unitless value to shrink or grow an element by a factor

    • Note that all descendants of the element are also scaled
    • scale can also take in two values to scale the x and y axis independently
    transform: scale(2.5);
    
    transform: scale(1.5, 2.5);
    
  • rotate just rotates the elements, typically using the deg unit

    • The turn unit also exists, where 1turn is equivalent to 360deg
    transform: rotate(180deg);
    
    transform: rotate(0.5turn);
    
  • skew kind of stretches the element diagonally

    • The x and y axis can be manipulated independently with skewX and skewY
    transform: skew(45deg);
    
  • All the operations above can be applied sequentially to transform with a separating space

    • The order in which they're applied can result in different effects
  • The transform operations execute from an anchor called the origin, which is the center by default

    • Depending on where the anchor is placed, operations are performed relative to that location
    • You can set the origin in multiple ways such as:
      • left top
      • 25px bottom
      • 0% 150%
    transform-origin: center;
    
  • Transforms don't work with inline elements in flow layout

    • Use display: inline-block or switch to another layout mode

CSS Transitions

  • The transition property allows changes that occur to smooth out

    • Animations without this property would look like they're teleporting
    • The browser is instructed to interpolate from one state to another when using transition
    button {
        transition: transform 250ms;
    }
    
    button:hover {
        transform: translateY(10px);
    }
    
  • This property requires two values, the name of the property to animate and the duration of the animation

    • A comma-separated list can be used if multiple properties need to be animated
    button {
        transition: transform 250ms, opacity 400ms;
    }
    
    button:hover {
        transform: translateY(10px);
        opacity: 0;
    }
    
  • Timing functions determine the pace at which an element moves in each frame

    • The transition-timing-function property can specify the different timing functions available, or you can use the shorthand
    button {
        transition: transform 250ms;
        transition-timing-function: linear;
    }
    
    button {
        transition: transform 250ms linear;
    }
    
  • Types of timing functions:

    • linear: Moves at a constant pace
    • ease-out: The initial pace is fast but slows down towards the end
    • ease-in: Opposite of ease-out, starts slow and then speeds up
    • ease-in-out: Combines the other two functions, going slow at the start and end but fast during the middle section. The timing is symmetrical
    • ease: Similar to ease-in-out but it's not symmetrical, there's a brief ramp-up with lots of deceleration. This is the default value
  • Custom timing curves can be defined using the cubic-bezier function

    • It takes in 4 values which represent the points of the time and state
    • The values set are between 0 and 1
    • Here are the built-in timings replicated using the cubic-bezier function
    /* linear */
    transition-timing-function: cubic-bezier(0, 0, 1, 1);
    
    /* ease-out */
    transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
    
    /* ease-in */
    transition-timing-function: cubic-bezier(0.75, 0, 1, 1);
    
    /* ease-in-out */
    transition-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1);
    
    /* ease */
    transition-timing-function: cubic-bezier(0.44, 0.21, 0, 1);
    
  • transition-delay allows us to maintain the current state for a brief interval

    • Useful in nested dropdown menus so users can move their cursor to adjacent dropdowns without it closing immediately
    • In the example below, if the user unhovers their cursor from the dropdown wrapper then nothing will happen for 300ms
    .dropdown {
        opacity: 0;
        transition: opacity 400ms;
        transition-delay: 300ms;
    }
    
    .wrapper:hover .dropdown {
        opacity: 1;
        transition: opacity 100ms;
        transition-delay: 0ms;
    }
    

Keyfame Animations

  • Keyframe animations are declared using the @keyframes at-rule. Transitions between sets of CSS declarations can be specified

    • Supposed to be general and reusable
    • Each @keyframes statement needs a name
    @keyframes slide-in {
        from {
            transform: translateX(-100%);
        }
    
        to {
            transform: translateX(0%);
        }
    }
    
  • Keyframe animations can be applied selectors using the animation property

    • It requires the keyframe statement name and the duration of the animation
    • The browser interpolates the declarations within the from and to blocks over the specified duration
    .box {
        animation: slide-in 1000ms;
    }
    
  • Timing functions can also be applied to animations using the animation-timing-function property

  • Keyframe animations run once by default, but can be looped with the animation-iteration-count property

    • Either an integer can be specified or the special value infinite can be used
  • Using percentages to represent progress instead of from and to in the keyframe statement enables us to add multiple steps

    @keyframes fancy-spin {
        0% {
            transform: rotate(0turn) scale(1);
        }
    
        25% {
            transform: rotate(1turn) scale(1);
        }
    
        50% {
            transform: rotate(1turn) scale(1.5);
        }
    
        75% {
            transform: rotate(0turn) scale(1.5);
        }
    
        100% {
            transform: rotate(0turn) scale(1);
        }
    }
    
  • animation-direction controls the order of the keyframes, the default value is normal

    • reverse plays the animation backwards, going from 100% to 0%
    • alternate ping-pongs between normal and reverse on subsequent iterations
      • Useful for breathing animations which typically have a grow and shrink theme
      • Reduces keyframe statement size
  • Similar to transition, the animation shorthand can combine all the mentioned properties

    • Since each property accepts different values, the order doesn't matter here
    • The exception is animation-delay, another property which must come after duration
      • Better to exclude it from the shorthand to increase readability
    animation: slide-in 1000ms linear infinite alternate;
    

Fill Modes

  • Declarations in the from and to blocks only apply while the animation is running

    • After the animation ends, the CSS declarations set directly to the element is reapplied
    • One way to override this behaviour is to set the declarations in the to block to the element
    • In the snippet, the box turns invisible over a second and will REMAIN invisible since opacity: 0 is also set in .box
    @keyframes fade-out {
        from {
            opacity: 1;
        }
    
        to {
            opacity: 0;
        }
    }
    
    .box {
        animation: fade-out 1000ms;
        opacity: 0;
    }
    
  • However, instead of relying on default declarations, we can use animation-fill-mode instead

    • Allows us to persist the final value from the animation, using the forwards value
    • Similarly, there may be no initial CSS for an element before an animation occurs
      • We can use backwards to apply the animation's initial state backwards in time
    • The value both applies forwards and backwards functionality together
    • animation-fill-mode can be used in shorthand as well without any conflicts

Dynamic Updates

  • Animations can be set to run if some event occurs, using JavaScript, the animation property can be added dynamically at any time

    • The following code renders a button and box
      • The button toggles the animation, causing the box to jump up and down, otherwise it remains still
      • When the animation property is set to a valid value, the animation begins
    function App() {
        const [animated, setAnimated] = React.useState(false);
    
        return (
            <Wrapper>
                <Box style={{animated : "jump 1000ms infinite" : undefined }} ></Box>
                <button onClick={() => setAnimated(!animated)}></button>
            </Wrapper>
        )
    }
    
  • "Interruptions" occur when animations are disabled during the animation process, creating a jarring effect since all the CSS is reset

    • animation-play-state keeps the animation applied between a running and paused state
    • When the animation resumes, it will continue where it left off
      • Consider modifying our previous example, when the animation stops now, the box will be mid-air from jumping instead of resetting
    <Box style={{animationPlayState: animated ? "running" : "paused" }} ></Box>
    
    ...
    
    const Box = styled.div`
        ...
        animation: jump 1000ms infinite;
    `;
    
  • transition is good for smoothening out harsh transitions betwen values or changing CSS as a result of application state or user action

  • There are some things only @keyframes can do compared to transition without relying on JavaScript

    • Looped animations
    • Multi-step animations
    • Pauseable animations

With styled-components

  • The @keyframes at-rule should be declared globally, this can be achieved with styled-components by importing their package
    • The keyframes function is called using tagged template literals, just like styled helper functions
    • The syntax to define an animation is the same as vanilla CSS
    import styled, { keyframes } from "styled-components";
    
    const float = keyframes`
        ...
    `;
    
    const FloatingCircle = styled.div`
        animation: ${float} 1000ms infinite alternate ease-in-out;
    `
    

Animation performance

  • The pixel pipeline:

    • Recalculating style: Figure out which CSS declarations apply to which elements
    • Layout: Figure out where each element sits on the page
    • Paint: Paint elements, process of figuring out the colour of every pixel (rasterisation) and filling it in
    • Compositing: Transform previously-painted elements
  • Different CSS properties trigger different steps in the pixel pipeline

    • Try avoid properties affecting layout such as width, height, padding and margin, it's slower
    • Animating properties relating to colours are faster because they don't affect layout, starting from the painting step
    • transform and opacity can be animated with just the compositing step alone, but there is support for other properties depeneding on browser
  • Hardware acceleration is the process of transferring the optimisation process from the browser (CPU) to the GPU

    • The GPU receives everything as a texture, and is good at texture-based transformations which create a performant animation
    • GPUs and CPUs renders a bit differently, so there may be a slight shifting of elements
    • The will-change property allows us to be intentional about which elements are handled by the GPU (which elements are hardware-accelerated)
    button {
        will-change: transform;
    }
    

Designing Animations

  • Types of animation

    • Transitions: Change content significantly, such as moving positions
    • Supplements: Add/remove information from the page without changing their location
    • Feedback: Helps user understand how the application responds to user input
    • Demonstrations: Education purpose, visual way of showing how something works
    • Decorations: Aesthetics, don't affect information on the page
  • Animation duration needs to be specified in milliseconds

  • Most animations generally use a duration between 200ms to 500ms

Action-Driven Animation

  • Use CSS transitions to animate state

    • Modals are either opened or closed for example
  • Animations based off pseudo-selectors like :hover can be achieved by using different transition values

    button {
        transition: transform 500ms; /* Exit */
    }
    
    button:hover {
        transform: scale(1.1);
        transition: transform 150ms;    /* Enter */
    }
    
  • Can dynamically change duration using JavaScript and React with styled-components

Orchestration

  • Improve animation by sequencing it, orchestrating different elements

  • Implementing orchestration essentially requires ternaries

  • Example of one styled component:

    <Backdrop
        style={{
            opacity: isOpen ? 0.75 : 0,
            transition: 'opacity',
            transitionDuration: isOpen
                ? '1000ms'
                : '500ms',
            transitionDelay: isOpen
                ? '0ms'
                : '100ms',
            transitionTimingFunction: isOpen
                ? 'ease-out'
                : 'ease-in',
        }}
        onClick={handleDismiss}
    />
    
    • Can extract the animation settings from the overall structure using a helper function
    function getTransitionStyles(isOpen) { /* Return 3 objects, one for each animated element */ }
    
    const { backdropStyles, modalStyles, closeButtonStyles } = getTransitionStyles(isOpen);
    
    return (
        <Wrapper>
            <Backdrop
                style={{
                    opacity: isOpen ? 0.75 : 0,
                    ...backdropStyles,
                }}
                onClick={handleDismiss}
            />
        ...
        </Wrapper>
    )
    

Accessibility

  • Animations enhance user experience, but certain people feel nausea or dizziness for example

  • Modern operating systems allow users to opt out of animations

    • Google "reduce animations" and mention the specific operating system
  • Apple first added the prefers-reduced-motion media query for Safari, since then other browsers and OS have followed suit

  • Accessing media query using CSS:

    • reduce will be set if the user clicks the "reduce animations" checkbox
    • no-preference is the default value
    button {
        transition: transform 500ms; /* Exit */
    }
    
    button:hover {
        transform: scale(1.1);
        transition: transform 150ms;    /* Enter */
    }
    
    @media (prefers-reduced-motion: reduce) {
        button {
            transition: none;
        }
    }
    
  • Accessing media query in JavaScript:

    • Can also use event listeners to update the value
    function getPrefersReducedMotion() {
        const mediaQueryList = window.matchMedia('(prefers-reduced-motion: no-preference)');
    
        const prefersReducedMotion = !mediaQueryList.matches;
    
        return prefersReducedMotion;
    }
    
  • Can also create a custom hook in React to perform the same functionality

  • Different people have different tolerances for the acceptable level of motion

    • Small colour changes, small fade-in-outs or minor pixel movements can remain unchanged
    • Large sweeping motions or opacity changes should be disabled

3D Transforms

  • There is no perspective in isometric projection, everything appears at the same scale

  • Perspective projection mimics real life, where things vanish into the distance

  • The 3D engine in CSS uses isometric projection by default, but the it can be switched with the perspective property

    • Elements appear 3D, further elements look smaller and vice versa, or which edge is closer/further away from us
    • The perspective property must be on the parent element
    • Supply a length value to switch to perspective projection
    • The value is a measure of how close the user is to the screen, small changes appear huge
  • Another way to apply perspective projection is to use the perspective function

    • Applied to each element rather than the parent
    .box {
        transform: perspective(250px) rotateX(45deg);
    }
    

Rendering Contexts

  • If applying a rotating transition on sibling elements, they may overlap

    • Use z-index on each element
  • Alternatively, use transform-style property with the preserve-3d value

    • Instead of using stacking contexts and z-index layers, elements are positioned in 3D space
  • transform-style: preserve-3d creates a 3D rendering context

    • Allows elements to show up "in front"
    • Allows elements to intersect

Gotchas

  • When applying perspective to descendants, it can trigger a "doom flicker"

    • When an element is hovered, it causes a rotation
    • If the cursor is near the BottleRocketImage, it will rotate away from the cursor which means it's no longer hovered
    • Essentially, the element flickers between hover and non-hover states
  • To resolve "doom flicker", it needs three things

    1. A shared parent that sets perspective
    2. A wrapper over each individual element that listens for hover/focus
    3. A child that applies the transition when its wrapper is hovered/focus
    • transform-style: preserve-3d must be applied to .card-link now since .wrapper and .card are no longer in a direct parent-child relationship
      • When .wrapper sets it, a new context is not created, but rather it forwards the context downwards to allow grandchildren to participate
      • Acting as a bridge, it can be applied to as many descendants as long as each layer repeats this trick
      /* 1. Shared parent */
    .wrapper {
        perspective: 500px;
        transform-style: preserve-3d;
    }
    
    /* 2. Individual card wrapper */
    .card-link {
        display: block;
        transform-style: preserve-3d;
    }
    
    /* 3. Child to be transformed: */
    .card {
        transform-origin: top center;
        will-change: transform;
        transform: rotateX(0deg);
        transition: transform 750ms;
    }
    
    /*  Apply the transforms to the childwhen we hover/focus the wrapper: */
    .card-link:hover .card,
    .card-link:focus .card {
        transform: rotateX(-35deg);
        transition: transform 250ms;
    }
    
  • Scrollburglars can appear when hovering over some transformed element

  • Can apply overflow: hidden but not on the parent element which creates the 3D rendering context

    • .wrapper is "fragile", there is a list of properties that can disable the 3D rendering context
      • overflow, clip-path, opacity, filter, mix-blend-mode
    • Need to apply the property to a new parent element, .outer-wrapper
    .outer-wrapper {
        overflow: hidden;
    }
    
    .wrapper {
        perspective: 500px;
        transform-style: preserve-3d;
    }
    

Ecosystem World Tour

  • The web animations API is a low-level animation API built into the browser

  • Mirrors the @keyframes API

    const elem = document.querySelector('.box');
    
    const frames = [
        { opacity: 0, transform: 'translateY(100%)' },
        { opacity: 1, transform: 'translateY(0%)' },
    ];
    
    elem.animate(
    frames,
        {
            duration: 1000,
            iterations: Infinity,
        }
    );
    
  • Pros:

    • Built into the CfnBrowserSettings, so no large package needs to be included in bundle
    • Good performance, won't be affected by any work happening in the JS main thread
    • It has solid browser support
    • It allows more customisation compared to CSS keyframe animations
  • Cons:

    • Not fundamentally different from CSS keyframe animations, nothing new added
    • Subtle differences between keyframe animations and the web animations API, such as default timing function is linear instead of ease
  • Framer Motion is a production-ready library for React

    • Can animate CSS properties that can't normally be animated
    • Works by calculating a complex sequence of transforms required to interpolate between the start and end positions
    • Can even animate transitions between components
  • Pros:

    • Can animate all kinds of things that would normally not be animatable
    • Uses hardware-accelerated transforms for performant transitions
    • Supports using spring physics instead of Bezier curves
    • Works seamlessly with React, can add sophisticated animations in small amounts of code
  • Cons:

    • Package weighs about 32kb gzip
    • Like all JS-based animation libraries, the animation might run choppy if the main thread becomes occupied
    • Only works with React
  • React Spring enables us to model animations based on spring physics, rather than Bezier curves

  • Fundamentally, React Spring is a number generator

    • This means it can be used for all types of animations, not just transitioning CSS properties
  • Pros:

    • Fluid, organic motion compared with CSS transitions / keyframe animations
    • Highly optimised performance
    • Relatively small, about 18kb gzip
    • A rich API with lots of advanced options, including orchestration tools
    • Ties in with an ecosystem, can use react-use-gesture to create the card-grabbing animation
  • Cons:

    • Can't do "magic" animations like Framer Motion, though, Framer Motion supports spring physics
    • Steep learning curve
    • Like all JS-based animation libraries, the animation might run choppy if the main thread becomes occupied
  • GreenSack GSAP is an old animation library

  • Offers advanced Bezier-based easing and a timeline to manage orchestration

  • Pros:

    • Large, active community
    • Can be used with any framework
    • Highly optimised performance
    • Rich plugin ecosystem
  • Cons:

    • Doesn't support true spring physics, but can emulate it more accurately than CSS transitions
    • Not framework-agnostic, won't tie in as neatly as framework-specific solutions