Tag: Animation

Hacking CSS Animation State and Playback Time

CSS-only Wolfenstein is a little project that I made a few weeks ago. It was an experiment with CSS 3D transformations and animations.

Inspired by the FPS demo and another Wolfenstein CodePen, I decided to build my own version. It is loosely based on Episode 1 – Floor 9 of the original Wolfenstein 3D game.

Editor: This game intentionally requires some quick reaction to avoid a Game Over screen.

Here is a playthrough video:

In a nutshell, my project is nothing but a carefully scripted long CSS animation. Plus a few instances of the checkbox hack.

:checked ~ div { animation-name: spin; }

The environment consists of 3D grid faces and the animations are mostly plain 3D translations and rotations. Nothing really fancy.

However, two problems were particularly tricky to solve:

  • Play the “weapon firing” animation whenever the player clicks on an enemy.
  • When the fast-moving boss got the last hit, enter a dramatic slow motion.

At a technical-level, this meant:

  • Replay an animation when the next checkbox is checked.
  • Slow down an animation, when a checkbox is checked.

In fact, neither was properly solved in my project! I either ended up using workarounds or just gave up.

On the other hand, after some digging, eventually I found the key to both problems: altering the properties of running CSS animations. In this article, we will explore further on this topic:

  • Lots of interactive examples.
  • Dissections: how does each example work (or not work)?
  • Behind-the-scene: how do browsers handle animation states?

Let me “toss my bricks”.

Problem 1: Replaying Animation

The first example: “just another checkbox”

My first intuition was “just add another checkbox”, which does not work:

Each checkbox works individually, but not both together. If one checkbox is already checked, the other no longer works.

Here’s how it works (or “does not work”):

  1. The animation-name of <div> is none by default.
  2. The user clicks on one checkbox, animation-name becomes spin, and the animation starts from the beginning.
  3. After a while, the user clicks on the other checkbox. A new CSS rule takes effect, but animation-name is still spin, which means no animation is added nor removed. The animation simply continues playing as if nothing happened.

The second example: “cloning the animation”

One working approach is to clone the animation:

#spin1:checked ~ div { animation-name: spin1; } #spin2:checked ~ div { animation-name: spin2; }

Here’s how it works:

  1. animation-name is none initially.
  2. The user clicks on “Spin!”, animation-name becomes spin1. The animation spin1 is started from the beginning because it was just added.
  3. The user clicks on “Spin again!”, animation-name becomes spin2. The animation spin2 is started from the beginning because it was just added.

Note that in Step #3, spin1 is removed because of the order of the CSS rules. It won’t work if “Spin again!” is checked first.

The third example: “appending the same animation”

Another working approach is to “append the same animation”:

#spin1:checked ~ div { animation-name: spin; } #spin2:checked ~ div { animation-name: spin, spin; }

This is similar to the previous example. You can actually understand the behavior this way:

#spin1:checked ~ div { animation-name: spin1; } #spin2:checked ~ div { animation-name: spin2, spin1; }

Note that when “Spin again!” is checked, the old running animation becomes the second animation in the new list, which could be unintuitive. A direct consequence is: the trick won’t work if animation-fill-mode is forwards. Here’s a demo:

If you wonder why this is the case, here are some clues:

  • animation-fill-mode is none by default, which means “The animation has no effect at all if not playing”.
  • animation-fill-mode: forwards; means “After the animation finishes playing, it must stay at the last keyframe forever”.
  • spin1’s decision always override spin2’s because spin1 appears later in the list.
  • Suppose the user clicks on “Spin!”, waits for a full spin, then clicks on “Spin again!”. At this moment. spin1 is already finished, and spin2 just starts.

Discussion

Rule of thumb: you cannot “restart” an existing CSS animation. Instead, you want to add and play a new animation. This may be confirmed by the W3C spec:

Once an animation has started it continues until it ends or the animation-name is removed.

Now comparing the last two examples, I think in practice, “cloning animations” should often work better, especially when CSS preprocessor is available.

Problem 2: Slow Motion

One might think that slowing an animation is just a matter of setting a longer animation-duration:

div { animation-duration: 0.5s; } #slowmo:checked ~ div { animation-duration: 1.5s; }

Indeed, this works:

… or does it?

With a few tweaks, it should be easier to see the issue.

Yes, the animation is slowed down. And no, it does not look good. The dog (almost) always “jumps” when you toggle the checkbox. Furthermore, the dog seems to jump to a random position rather than the initial one. How come?

It would be easier to understand it if we introduced two “shadow elements”:

Both shadow elements are running the same animations with different animation-duration. And they are not affected by the checkbox.

When you toggle the checkbox, the element just immediately switches between the states of two shadow elements.

Quoting the W3C spec:

Changes to the values of animation properties while the animation is running apply as if the animation had those values from when it began.

This follows the stateless design, which allows browsers to easily determine the animated value. The actual calculation is described here and here.

Another Attempt

One idea is to pause the current animation, then add a slower animation that takes over from there:

div {   animation-name: spin1;   animation-duration: 2s; }  #slowmo:checked ~ div {   animation-name: spin1, spin2;   animation-duration: 2s, 5s;   animation-play-state: paused, running; }

So it works:

… or does it?

It does slow down when you click on “Slowmo!”. But if you wait for a full circle, you will see a “jump”. Actually, it always jumps to the position when “Slowmo!” is clicked on.

The reason is we don’t have a from keyframe defined – and we shouldn’t. When the user clicks on “Slowmo!”, spin1 is paused at some position, and spin2 starts at exactly the same position. We simply cannot predict that position beforehand … or can we?

A Working Solution

We can! By using a custom property, we can capture the angle in the first animation, then pass it to the second animation:

div {   transform: rotate(var(--angle1));   animation-name: spin1;   animation-duration: 2s; }  #slowmo:checked ~ div {   transform: rotate(var(--angle2));   animation-name: spin1, spin2;   animation-duration: 2s, 5s;   animation-play-state: paused, running; }  @keyframes spin1 {   to {     --angle1: 360deg;   } }  @keyframes spin2 {   from {     --angle2: var(--angle1);   }   to {     --angle2: calc(var(--angle1) + 360deg);   } }

Note: @property is used in this example, which is not supported by all browsers.

The “Perfect” Solution

There is a caveat to the previous solution: “exiting slowmo” does not work well.

Here is a better solution:

In this version, slow motion can be entered or exited seamlessly. No experimental feature is used either. So is it the perfect solution? Yes and no.

This solution works like “shifting” “gears”:

  • Gears: there are two <div>s. One is the parent of the other. Both have the spin animation but with different animation-duration. The final state of the element is the accumulation of both animations.
  • Shifting: At the beginning, only one <div> has its animation running. The other is paused. When the checkbox is toggled, both animations swap their states.

While I really like the result, there is one problem: it is a nice exploit of the spin animation, which does not work for other types of animations in general.

A Practical Solution (with JS)

For general animations, it is possible to achieve the slow motion function with a bit of JavaScript:

A quick explanation:

  • A custom property is used to track the animation progress.
  • The animation is “restarted” when the checkbox is toggled.
  • The JS code computes the correct animation-delay to ensure a seamless transition. I recommend this article if you are not familiar with negative values of animation-delay.

You can view this solution as a hybrid of “restarting animation” and the “gear-shifting” approach.

Here it is important to track the animation progress correctly. Workarounds are possible if @property is not available. As an example, this version uses z-index to track the progress:

Side-note: originally, I also tried to create a CSS-only version but did not succeed. While not 100% sure, I think it is because animation-delay is not animatable.

Here is a version with minimal JavaScript. Only “entering slowmo” works.

Please let me know if you manage to create a working CSS-only version!

Slow-mo Any Animation (with JS)

Lastly, I’d like to share a solution that works for (almost) any animation, even with multiple complicated @keyframes:

Basically, you need to add an animation progress tracker, then carefully compute animation-delay for the new animation. However, sometimes it could be tricky (but possible) to get the correct values.

For example:

  • animation-timing-function is not linear.
  • animation-direction is not normal.
  • multiple values in animation-name with different animation-duration’s and animation-delay’s.

This method is also described here for the Web Animations API.

Acknowledgments

I started down this path after encountering CSS-only projects. Some were delicate artwork, and some were complex contraptions. My favorites are those involving 3D objects, for example, this bouncing ball and this packing cube.

In the beginning, I had no clue how these were made. Later I read and learned a lot from this nice tutorial by Ana Tudor.

As it turned out, building and animating 3D objects with CSS is not much different from doing it with Blender, just with a bit different flavor.

Conclusion

In this article we examined the behavior of CSS animations when an animate-* property is altered. Especially we worked out solutions for “replaying an animation” and “animation slow-mo”.

I hope you find this article interesting. Please let me know your thoughts!


Hacking CSS Animation State and Playback Time originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSS-Tricks

, , , ,

How I Chose an Animation Library for My Solitaire Game

There is an abundance of both CSS and JavaScript libraries for animation libraries out there. So many, in fact, that choosing the right one for your project can seem impossible. That’s the situation I faced when I decided to build an online Solitaire game. I knew I’d need an animation library, but which was the right one to choose?

In this article, I’ll go through which considerations I made, what to look out for and present you with some of the most popular libraries available. I’ll go through some real-world examples with you to illustrate my points, and in the end, hopefully, you’ll be better equipped than me when I first had to choose an animation library.

Your mileage with this advice may vary, of course. Everything I’m sharing here is specific to a thing I wanted to build. Your project may have completely different requirements and priorities and that’s OK. I think what’s important here is getting a first-hand account of thinking like a front-end developer with a particular goal.

Speaking of which, I do consider myself a front-end developer but my background is super heavy in design. So I know code, but not to the extent of someone who is a JavaScript engineer. Just wanted to clear that up because experience can certainly impact the final decision.

Here’s the goal

Before we get into any decision-making let’s take a look at the sorts of animations I needed to make in this CSS-Tricks-ified version of the game:

Crazy, right? There’s nothing exactly trivial about these animations. There’s a lot going on — sometimes simultaneously — and a lot to orchestrate. Plus, a majority of the animations are triggered by user interactions. So, that left me with a few priorities heading into my decision:

  • Smooth animations: The way animations are applied can have a big impact on whether they run smoothly, or display a little choppiness.
  • Performance: Adopting any library is going to add weight to a project and I wanted my game to be as lean as possible.
  • Convenience: I wanted a nice, clean syntax that makes it easier to write and manage the animations. I’d even trade a little extra convenience for a small performance cost if it allows me to write better, more maintainable code. Again, this bodes well for a designer-turned-developer.
  • Browser support: Of course I wanted my game to work on any modern browser using some form of progressive enhancement to prevent completely borking legacy browsers. Plus, I definitely wanted  some future-proofing.

That’s what I took with me as I went in search of the right tool for this particular job.

Choosing between CSS or JavaScript animation libraries

The first thing I considered when choosing an animation library was whether to go with a CSS or JavaScript-based library. There are lots of great CSS libraries, many of them with excellent performance which was a high priority for me. I was looking to do some heavy-duty animations, like the  ability to sequence animations and get callbacks on animation completion. That’s all totally possible with pure CSS — still, it’s a lot less smooth than what most JavaScript libraries offer.

Let’s see how a simple sequenced animation looks in CSS and compare it to jQuery, which has plenty of built-in animation helpers:

The animations look the same but are created differently. To make the CSS animation, first, we have to define the keyframe animation in our CSS and attach it to a class:

.card.move {   animation : move 2s; }  @keyframes move {   0% { left: 0 }   50% { left: 100px }   100% { left: 0 } }

We then execute the animation using JavaScript and listen for a CSS callback on the element:

var cardElement = document.getElementsByClassName("card")[0]; var statusElement = document.getElementsByClassName("status")[0];  cardElement.classList.add("move"); statusElement.innerHTML = "Animating"  var animationEndCallback = function() {   cardElement.classList.remove("move");   statusElement.innerHTML = "Inactive" }  cardElement.addEventListener("webkitAnimationEnd", animationEndCallback); cardElement.addEventListener("oAnimationEnd", animationEndCallback);  cardElement.addEventListener("antionend", animationEndCallback);

Having things happen in different places might be fine in a simple example like this, but it can become very confusing once things get a bit more complex. 

Compare this to how the animation is done with jQuery:

$  (".status").text("Animating") $  ( ".card" ).animate({   left: "100px" }, 1000); $  ( ".card" ).animate({   left: 0 }, 1000, function() {   $  (".status").text("Inactive") });

Here, everything happens in the same place, simplifying things should the animations grow more complex in the future.

It seemed clear that a JavaScript library was the right way to go, but which was the right one to choose for my Solitaire game? I mean, jQuery is great and still widely used even today, but that’s not something I want to hang my hat on. There are plenty of JavaScript animation libraries, so I wanted to consider something built specifically to handle the type of heavy animations I had in mind.

Choosing a JavaScript animation library

It quickly became apparent to me that there’s no lack of JavaScript animation libraries and new, exciting technologies. They all have benefits and drawbacks, so let’s go through some of the ones I considered and why.

The Web Animations API is one such case that might replace many JavaScript animation libraries in the future. With it, you’ll be able to create complex staggered animations without loading any external libraries and with the same performance as CSS animations. The only drawback is that not all browsers support it yet

The <canvas> element presents another exciting opportunity. In it, we can animate things with JavaScript, as we would with the DOM, but the animation is rendered as raster, which means we can make some high-performance animations. The only drawback is that the canvas element is essentially rendered as an image in the DOM, so if we’re looking for pixel-perfection, we might be out of luck. As someone acutely in tune with design, this was a dealbreaker for me.

I needed something tried and tested, so I knew I probably had to go with one of the many JavaScript libraries. I started looking at libraries and narrowed my choices to Anime.js and GSAP. They both seemed to handle complex animations well and had excellent notes on performance. Anime is a well-maintained library with over 42.000 stars on GitHub, while GSAP is a super popular, battle-tested library with a thriving community.

An active community was critical to me since I needed a place to ask for help, and I didn’t want to use a library that might later be abandoned. I considered this as part of my convenience requirements.

Sequencing animations and callbacks

Once I had my choices narrowed down, the next step was to implement a complex animation using my two libraries. A recurrent animation in a solitaire game is that of a card moving somewhere and then turning over, so let’s see how that looks:

Both animations look great! They’re smooth, and implementing both of them was pretty straightforward. Both libraries had a timeline function that made creating sequences a breeze. This is how the implementation looks in AnimeJS:

var timeline = anime.timeline({   begin: function() {     $  (".status").text("Animating")   },   complete: function() {     $  (".status").text("Inactive")   } });  timeline.add({   targets: '.card',   left: [0, 300],   easing: 'easeInOutSine',   duration: 500 }).add({   targets: '.card .back',   rotateY: [0, 90],   easing: 'easeInSine',   duration: 200 }).add({   targets: '.card .front',   rotateY: [-90, 0],   easing: 'easeOutSine',   duration: 200 })

Anime’s timeline() function comes built-in with callbacks on beginning and ending the animation, and creating the sequence is as easy as appending the sequential animations. First, I move the card, then I turn my back-image 90 degrees, so it goes out of view, and then I turn my front-image 90 degrees, so it comes into view.

The same implementation using GSAP’s timeline() function looks very similar:

var timeline = gsap.timeline({   onStart: function() {     $  (".status").text("Animating")   },   onComplete: function() {     $  (".status").text("Inactive")   } });  timeline.fromTo(".card", {   left: 0 }, {   duration: 0.5,   left: 300 }).fromTo(".card .back", {   rotationY: 0 }, {   rotationY: 90,   ease: "power1.easeIn",   duration: 0.2 }).fromTo(".card .front", {   rotationY: -90 }, {   rotationY: 0,   ease: "power1.easeOut",   duration: 0.2 })

Decision time

The main difference between Anime and GSAP appears to be the syntax, where GSAP might be a little more elaborate. I was stuck with two great libraries that had very similar functionality, were able to deal with complex animation, and had a thriving community. It seemed like I had a tie race!

Priority Anime GSAP
Smooth animations
Performance
Convenience
Browser support

So, what made me choose one library over the other?

I was very concerned about how the library would act under pressure. Having laggy animations in a game like Solitaire can greatly impact how fun it is to play the game. I knew I wouldn’t be able to fully see how the library performed before I created the game. Luckily, GSAP had made a stress test that compared different animation libraries to each other, including Anime.

Looking at that, GSAP certainly looked to be the superior library for dealing with loads of complex animations. GSAP was giving me upwards of 26 frames per second on a crazy heavy animation that Anime was only able to top out at 19.  After reading up on GSAP more and looking into their forums, it became clear that performance was of the highest priority to the guys behind GSAP.

And even though both GSAP and Anime have been around a while, Anime’s repo has been sitting somewhat dormant a couple of years while GSAP had made commits in the past couple of months.

I ended up using GSAP and haven’t regretted my decision!

How about you? Does any of this square with how you evaluate and compare front-end tooling? Are there other priorities you might have considered (e.g. accessibility, etc.) in a project like this? Or do you have a project where you had to pare down your choices from a bunch of different options? Please share in the comments because I’d like to know! 

Oh, and if you want to see how it looks when animating a whole deck of cards, you can head over to my site and play a game of Solitaire. Have fun!


How I Chose an Animation Library for My Solitaire Game originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , ,
[Top]

GSAP Flip Plugin for Animation

Greensock made the GSAP Flip plugin free in the 3.9 release. FLIP is an animation concept that helps make super performance state-change animations. Ryan Mulligan has a good blog post:

FLIP, coined by Paul Lewis, is an acronym for First, Last, Invert, and Play. The Flip plugin harnesses this technique so that web developers can effortlessly and smoothly transition elements between states.

GSAP Flip plugin logo.

Examples using the GSAP Flip plugin

Taking advantage of FLIP “by hand” is certainly possible, but tricky. It’s an absolutely perfect thing for an animation library to do for us. Greenstock nailed it, as Ryan says:

1. Get the current state
2. Make your state changes
3. Call Flip.from(state, options)

Deliciously simple. Ryan made an “add to cart” effect with it:

I used it just the other day to make a “mini photo gallery” that could rotate which image was the big one on top:

Which, coincidently, is exactly why I ended up blogging “How to Cycle Through Classes on an HTML Element” the other day.

To Shared LinkPermalink on CSS-Tricks


GSAP Flip Plugin for Animation originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , ,
[Top]

How to Make a Scroll-Triggered Animation With Basic JavaScript

A little bit of animation on a site can add some flair, impress users, and get their attention. You could have them run, no matter where they are on the page, immediately when the page loads. But what if your website is fairly long so it took some time for the user to scroll down to that element? They might miss it.

You could have them run all the time, but perhaps the animation is best designed so that you for sure see the beginning of it. The trick is to start the animation when the user scrolls down to that element — scroll-triggered animation, if you will.

To tackle this we use scroll triggers. When the user scrolls down to any particular element, we can use that event to do something. It could be anything, even the beginning of an animation. It could even be scroll-triggered lazy loading on images or lazy loading a whole comments section. In that way, we won’t force users to download elements that aren’t in the viewport on initial page load. Many users may never scroll down at all, so we really save them (and us) bandwidth and load time.

Scroll triggers are very useful. There are many libraries out there that you can use to implement them, like Greensock’s popular ScrollTrigger plugin. But you don’t have to use a third-party library, particularly for fairly simple ideas. In fact, you can implement it yourself using only a small handful of vanilla JavaScript. That is what we are going to do in this article.

Here’s how we’ll make our scroll-triggered event

  • Create a function called scrollTrigger we can apply to certain elements
  • Apply an .active class on an element when it enters the viewport
  • Animate that .active class with CSS

There are times where adding a .active class is not enough. For example, we might want to execute a custom function instead. That means we should be able to pass a custom function that executes when the element is visible. Like this:

scrollTrigger('.loader', {   cb: function(el) {     el.innerText = 'Loading ...'     loadContent()   } })

We’ll also attempt to handle scroll triggers for older non-supporting browsers.

But first, the IntersectionObserver API

The main JavaScript feature we’re going to use is the Intersection Observer. This API provides a way to asynchronously observe changes in the intersection of a target element — and it does so more in a more performant way than watching for scroll events. We will use IntersectionObserver to monitor when scrolling reaches the point where certain elements are visible on the page.

Let’s start building the scroll trigger

We want to create a function called scrollTrigger and this function should take a selector as its argument.

function scrollTrigger(selector) {   // Multiple element can have same class/selector,   // so we are using querySelectorAll   let els = document.querySelectorAll(selector)   // The above `querySelectorAll` returns a nodeList,   // so we are converting it to an array   els = Array.from(els)   // Now we are iterating over the elements array   els.forEach(el => {     // `addObserver function` will attach the IntersectionObserver to the element     // We will create this function next     addObserver(el)   }) } // Example usage scrollTrigger('.scroll-reveal')

Now let’s create the addObserver function that want to attach to the element using IntersectionObserver:

function scrollTrigger(selector){   let els = document.querySelectorAll(selector)   els = Array.from(els)   els.forEach(el => {     addObserver(el)   }) } function addObserver(el){     // We are creating a new IntersectionObserver instance     let observer = new IntersectionObserver((entries, observer) => { // This takes a callback function that receives two arguments: the elements list and the observer instance.       entries.forEach(entry => {         // `entry.isIntersecting` will be true if the element is visible       if(entry.isIntersecting) {         entry.target.classList.add('active')         // We are removing the observer from the element after adding the active class         observer.unobserve(entry.target)       }     })   })   // Adding the observer to the element   observer.observe(el) } // Example usage scrollTrigger('.scroll-reveal')

If we do this and scroll to an element with a .scroll-reveal class, an .active class is added to that element. But notice that the active class is added as soon as any small part of the element is visible.

Animated screenshot of the scroll-triggered animation with the code for it to the left and DevTools open on the right.

But that might be overkill. Instead, we might want the .active class to be added once a bigger part of the element is visible. Well, thankfully, IntersectionObserver accepts some options for that as its second argument. Let’s apply those to our scrollTrigger function:

// Receiving options as an object // If the user doesn't pass any options, the default will be `{}` function scrollTrigger(selector, options = {}) {   let els = document.querySelectorAll(selector)   els = Array.from(els)   els.forEach(el => {     // Passing the options object to the addObserver function     addObserver(el, options)   }) } // Receiving options passed from the scrollTrigger function function addObserver(el, options) {   let observer = new IntersectionObserver((entries, observer) => {     entries.forEach(entry => {       if(entry.isIntersecting) {         entry.target.classList.add('active')         observer.unobserve(entry.target)       }     })   }, options) // Passing the options object to the observer   observer.observe(el) } // Example usage 1: // scrollTrigger('.scroll-reveal') // Example usage 2: scrollTrigger('.scroll-reveal', {   rootMargin: '-200px' })

And just like that, our first two agenda items are fulfilled!

Let’s move on to the third item — adding the ability to execute a callback function when we scroll to a targeted element. Specifically, let’s pass the callback function in our options object as cb:

function scrollTrigger(selector, options = {}) {   let els = document.querySelectorAll(selector)   els = Array.from(els)   els.forEach(el => {     addObserver(el, options)   }) } function addObserver(el, options){   let observer = new IntersectionObserver((entries, observer) => {     entries.forEach(entry => {       if(entry.isIntersecting){         if(options.cb) {           // If we've passed a callback function, we'll call it           options.cb(el)         } else{           // If we haven't, we'll just add the active class           entry.target.classList.add('active')         }         observer.unobserve(entry.target)       }     })   }, options)   observer.observe(el) } // Example usage: scrollTrigger('.loader', {   rootMargin: '-200px',   cb: function(el){     el.innerText = 'Loading...'     // Done loading     setTimeout(() => {       el.innerText = 'Task Complete!'     }, 1000)   } })
An updated animated screenshot of the same scroll-triggered animation. As boxes enter the screen from the bottom, a they rotate. A "loading" message that changes to "finished loading" message is the last element to scroll into view. The code is open to the left of the animation.

Great! There’s one last thing that we need to take care of: legacy browser support. Certain browsers might lack support for IntersectionObserver, so let’s handle that case in our addObserver function:

function scrollTrigger(selector, options = {}) {   let els = document.querySelectorAll(selector)   els = Array.from(els)   els.forEach(el => {     addObserver(el, options)   }) } function addObserver(el, options) {   // Check if `IntersectionObserver` is supported   if(!('IntersectionObserver' in window)) {     // Simple fallback     // The animation/callback will be called immediately so     // the scroll animation doesn't happen on unsupported browsers     if(options.cb){       options.cb(el)     } else{       entry.target.classList.add('active')     }     // We don't need to execute the rest of the code     return   }   let observer = new IntersectionObserver((entries, observer) =>; {     entries.forEach(entry => {       if(entry.isIntersecting) {         if(options.cb) {           options.cb(el)         } else{           entry.target.classList.add('active')         }         observer.unobserve(entry.target)       }     })   }, options)   observer.observe(el) } // Example usages: scrollTrigger('.intro-text') scrollTrigger('.scroll-reveal', {   rootMargin: '-200px', }) scrollTrigger('.loader', {   rootMargin: '-200px',   cb: function(el){     el.innerText = 'Loading...'     setTimeout(() => {       el.innerText = 'Task Complete!'     }, 1000)   } })

Here’s that live demo again:

And that’s all for this little journey! I hope you enjoyed it and learned something new in the process.


How to Make a Scroll-Triggered Animation With Basic JavaScript originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , ,
[Top]

Empathetic Animation

Animation on the web is often a contentious topic. I think, in part, it’s because bad animation is blindingly obvious, whereas well-executed animation fades seamlessly into the background. When handled well, animation can really elevate a website, whether it’s just adding a bit of personality or providing visual hints and lessening cognitive load. Unfortunately, it often feels like there are two camps, accessibility vs. animation. This is such a shame because we can have it all! All it requires is a little consideration.

Here’s a couple of important questions to ask when you’re creating animations.

Does this animation serve a purpose?

This sounds serious, but don’t worry — the site’s purpose is key. If you’re building a personal portfolio, go wild! However, if someone’s trying to file a tax return, whimsical loading animations aren’t likely to be well-received. On the other hand, an animated progress bar could be a nice touch while providing visual feedback on the user’s action.

Is it diverting focus from important information?

It’s all too easy to get caught up in the excitement of whizzing things around, but remember that the web is primarily an information system. When people are trying to read, animating text or looping animations that play nearby can be hugely distracting, especially for people with ADD or ADHD. Great animation aids focus; it doesn’t disrupt it.

So! Your animation’s passed the test, what next? Here are a few thoughts…

Did we allow users to opt-out?

It’s important that our animations are safe for people with motion sensitivities. Those with vestibular (inner ear) disorders can experience dizziness, headaches, or even nausea from animated content.

Luckily, we can tap into operating system settings with the prefers-reduced-motion media query. This media query detects whether the user has requested the operating system to minimize the amount of animation or motion it uses.

Screenshot of the user preferences settings in MacOS, open to Accessibility and displaying options for how to display things, including one option for reduce motion, which is checked.
The reduced motion settings in macOS.

Here’s an example:

@media (prefers-reduced-motion: reduce) {   *,   *::before,   *::after {     animation-duration: 0.01ms !important;     animation-iteration-count: 1 !important;     transition-duration: 0.01ms !important;     scroll-behavior: auto !important;   } }

This snippet taps into that user setting and, if enabled, it gets rid of all your CSS animations and transitions. It’s a bit of a sledgehammer approach though — remember, the key word in this media query is reduced. Make sure functionality isn’t breaking and that users aren’t losing important context by opting out of the animation. I prefer tailoring reduced motion options for those users. Think simple opacity fades instead of zooming or panning effects.

What about JavaScript, though?

Glad you asked! We can make use of the reduced motion media query in JavaScript land, too!

let motionQuery = matchMedia('(prefers-reduced-motion)');  const handleReduceMotion = () => {   if (motionQuery.matches) {     // reduced motion options   } }  motionQuery.addListener(handleReduceMotion); handleReduceMotion()

Tapping into system preferences isn’t bulletproof. After all, it’s there’s no guarantee that everyone affected by motion knows how to change their settings. To be extra safe, it’s possible to add a reduced motion toggle in the UI and put the power back in the user’s hands to decide. We {the collective} has a really nice implementation on their site

Here’s a straightforward example:

Scroll animations

One of my favorite things about animating on the web is hooking into user interactions. It opens up a world of creative possibilities and really allows you to engage with visitors. But it’s important to remember that not all interactions are opt-in — some (like scrolling) are inherently tied to how someone navigates around your site.

The Nielson Norman Group has done some great research on scroll interactions. One particular part really stuck out for me. They found that a lot of task-focused users couldn’t tell the difference between slow load times and scroll-triggered entrance animations. All they noticed was a frustrating delay in the interface’s response time. I can relate to this; it’s annoying when you’re trying to scan a website for some information and you have to wait for the page to slowly ease and fade into view.

If you’re using GreenSock’s ScrollTrigger plugin for your animations, you’re in luck. We’ve added a cool little property to help avoid this frustration: fastScrollEnd.

fastScrollEnd detects the users’ scroll velocity. ScrollTrigger skips the entrance animations to their end state when the user scrolls super fast, like they’re in a hurry. Check it out!

There’s also a super easy way to make your scroll animations reduced-motion-friendly with ScrollTrigger.matchMedia():


I hope these snippets and insights help. Remember, consider the purpose, lead with empathy, and use your animation powers responsibly!

CSS-Tricks

,
[Top]

Diagonal Stripes Wipe Animation

I was playing this game on Apple Arcade the other day called wurdweb. It’s a fun little game! Little touches like the little shape dudes that walk around the screen (but otherwise don’t do anything) give it a lot of character. I kinda want little shape dudes that walk around on websites. But another UI choice caught my eye, the way that transitions between screens have these diagonal lines that grow and fill the screen, like window blinds closing, kinda.

Here’s a quick screencast showing how those wipes work:

I wanted to have a crack at building this.

The first thing that went through my mind is repeating-linear-gradient and how that can be used to build stripes. So say we set up like this:

.gradient {   background-image:     repeating-linear-gradient(       45deg,       #ff8a00,       #ff8a00 10px,       #e52e71 10px,       #e52e71 20px     ); }

That would buy us stripes like this:

We can use transparent as a color though. Meaning if we covered the screen with stripes like these, we could see through where that color is. Say like this:

In that gradient definition, we use 10px as the “start” and 20px as the “end” of the gradient before it repeats. Part of the trick here is keeping that 20px “end” the same and animating the “start” number up to it. When we do that, it actually covers the screen in a solid color. The problem is… how do you animate it? You can’t do this:

Screenshot of a CSS code snippet on a dark gray background with syntax highlighting. An arrow is pointing from the repeating linear gradient on the element to another repeating linear gradient inside keyframes. A note that says not going to animate is displayed in large white letters above a crying emoji.

What we need to do is animate that “start” pixel value number alone. We can use a custom property, but it’s a little tricky because without declaring them, custom properties are just strings, and not animatable lengths. So we’d have to do it like this.

@property --start {   syntax: "<length>";   inherits: false;   initial-value: 10px; } #cover {   position: fixed;   top: 0;   left: 0;   width: 100%;   height: 100%;   background-image: repeating-linear-gradient(     45deg,     #ff8a00,     #ff8a00 var(--start),     transparent var(--start),     transparent var(--end, 20px)   );   animation: cover 1s linear infinite; } @keyframes cover {   to {     --start: 20px;   } }

We’ve got to use @property here to do this, which I really like but, sadly, has limited browser support. It does work though! I’ve got all that set up, including a quick prefers-reduced-motion media query. I’m using a smidge of JavaScript to change the background halfway through the animation (while the screen is covered) so you can see how it might be used for a screen transition. Again, note that this is only working in Chromium-based browsers at the moment:

Notice I’ve used CSS custom properties for other things as well, like the angle and size of the stripes and the speed of the animation. They are both very trivial to change! I’ve chucked in knobs so you can adjust things to your liking. Knobs? Yeah, they are cool:

Like and subscribe

This whole thing started as a tweet. In this case, I’m glad I did as Temani Afif chimed in with a way to do it with masks as well, meaning pretty solid support across all browsers:

I don’t think animating background color stops or a mask position is particularly performant, but since we’re talking “screen wipes” here, one could imagine that the page isn’t likely to be interacted with anymore until the page transition is over, so maybe that’s not the world’s biggest deal.


The post Diagonal Stripes Wipe Animation appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Recreating the Apple Music Hits Playlist Animation in CSS

Apple Music has this “Spatial Audio” feature where the direction of the music in your headphones is based on the location of the device. It’s tough to explain just how neat it is. But that’s not what I’m here to talk about.

I opened up the Apple Music app and saw a featured playlist of hit songs that support Spatial Audio. The cover for it is this brightly-colored pink container that holds a bunch of boxes stacked one on top of another. The boxes animate in one at a time, fading in at the center of the container, then fading out as it scales to the size of the container. Like an infinite loop.

Animated GIF showing the Apple Music UI we are recreating. It's brightly colored shades of pink against a dark gray background with information about the playlist to the right of the pattern, and options to play and shuffle the sings in orange buttons.

Cool! I knew I had to re-create it in CSS. So I did.

Here’s how it works…

The markup

I started with the HTML. There’s obviously a container we need to define, plus however many boxes we want to animate. I went with an even 10 boxes in the container.

<div class="container">   <div class="box"></div>   <div class="box"></div>   <div class="box"></div>   <!-- etc. --> </div>

That’s literally it for HTML. We are free to jump right into the CSS!

Styling the container

Nothing too fancy here. I measured approximate dimensions based on what I saw in Apple Music, which happened to be 315px × 385px. then I took a screenshot of the cover and dropped it into my image editing app to get the lightest possible color, which is around the outside edges of the container. My color picker landed on #eb5bec.

.container {   background-color: #eb5bec;   height: 315px;   width: 385px; }

As I was doing this, I knew I would probably want this to be a grid container to align the boxes and any other elements in the center. I also figured that the boxes themselves would start from the center of the container and stack on top of one another, meaning there will be some absolute positioning. That also means the container ought to have relative positioning to reign them in.

.container {   background-color: #eb5bec;   height: 315px;   position: relative;   width: 385px; }

And since we want the boxes to start from the center, we can reach for grid to help with that:

.container {   background-color: #eb5bec;   display: grid;   height: 315px;   place-items: center;   position: relative;   width: 385px; }

If the boxes in the container are growing outward, then there’s a chance that they could expand beyond the container. Better hide any possible overflow.

.container {   background-color: #eb5bec;   height: 315px;   overflow: hidden;   position: relative;   width: 385px; }

I also noticed some rounded corners on it, so let’s drop that in while we’re here.

.container {   background-color: #eb5bec;   border-radius: 16px;   height: 315px;   position: relative;   width: 385px; }

So far, so good!

Styling the boxes

We have 10 .box elements in the markup and we want them stacked on top of one another. I started with some absolute positioning, then sized them at 100px square. Then I did the same thing with my image editing app to find the darkest color value of a box, which was #471e45.

.box {   background: #471e45;   height: 100px;   position: absolute;   width: 100px; }

The boxes seem to fade out as they grow. That allows one box to be seen through the other, so let’s make them opaque to start.

.box {   background: #471e45;   height: 100px;   opacity: 0.5;   position: absolute;   width: 100px; }

Cool, cool. We’re unable to see all the boxes as they’re stacked on top of one another, but we’re making progress!

Creating the animation

Ready to write some @keyframes? We’re gonna make this super simple, going from 0 to 100% without any steps in between. We don’t even need those percentages!

@keyframes grow {   from {     /* do stuff */   }   to {     /* do stuff */   } }

Specifically, we want two things to happen from start to finish:

  • The boxes go from our starting opacity value of 0.5 to 0 (fully transparent).
  • The boxes scale up to the edges of the container.
@keyframes grow {   from {     opacity: 0.5;     transform: scale(0);   }   to {     opacity: 0;     transform: scale(3.85);   } }

How’d I land on scaling the boxes up by 3.85? Our boxes are 100px square and the container is 385px tall. A value of 3.85 gets the boxes up to 385px as they fade completely out which makes for a nice linear animation when we get there.

Speaking of which…

Applying the animation

It’s pretty easy to call the animation on our boxes. Just gotta make sure it moves in a liner timing function on an infinite basis so it’s like the Energizer Bunny and keeps going and going and going and going and…

.box {   animation: grow 10s linear infinite; /* 10s = 10 boxes */   /* etc. */ }

This gives us the animation we want. But! The boxes are all moving at the same time, so all we see is one giant box growing.

We’ve gotta stagger those little fellers. No loops in vanilla CSS, unfortunately, so we have to delay each box individually. We can start by setting a custom property for the delay, set it to one second, then redefine the custom property on each instance.

.box {   --delay: 1s;      animation-delay: var(--delay);   /* same as before */ } .box:nth-child(2) {   --delay: 2s; } .box:nth-child(3) {   --delay: 3s; } .box:nth-child(4) {   --delay: 4s; } .box:nth-child(5) {   --delay: 5s; } /* five more times... */

Huzzah!

Keep on rockin’

That’s it! We just recreated the same sort of effect used by Apple Music. There are a few finishing touches we could plop in there, like the content and whatnot. Here’s my final version again:


The post Recreating the Apple Music Hits Playlist Animation in CSS appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

Animation Techniques for Adding and Removing Items From a Stack

Animating elements with CSS can either be quite easy or quite difficult depending on what you are trying to do. Changing the background color of a button when you hover over it? Easy. Animating the position and size of an element in a performant way that also affects the position of other elements? Tricky! That’s exactly what we’ll get into here in this article.

A common example is removing an item from a stack of items. The items stacked on top need to fall downwards to account for the space of an item removed from the bottom of the stack. That is how things behave in real life, and users may expect this kind of life-like motion on a website. When it doesn’t happen, it’s possible the user is confused or momentarily disorientated. You expect something to behave one way based on life experience and get something completely different, and users may need extra time to process the unrealistic movement.

Here is a demonstration of a UI for adding items (click the button) or removing items (click the item).

You could paper over the poor UI slightly by adding a “fade out” animation or something, but the result won’t be that great, as the list will will abruptly collapse and cause those same cognitive issues.

Applying CSS-only animations to a dynamic DOM event (adding brand new elements and fully removing elements) is extremely tricky work. We’re going to face this problem head-on and go over three very different types of animations that handle this, all accomplishing the same goal of helping users understand changes to a list of items. By the time we’re done, you’ll be armed to use these animations, or build your own based on the concepts.

We will also touch upon accessibility and how elaborate HTML layouts can still retain some compatibility with accessibility devices with the help of ARIA attributes.

The Slide-Down Opacity Animation

A very modern approach (and my personal favorite) is when newly-added elements fade-and-float into position vertically depending on where they are going to end up. This also means the list needs to “open up” a spot (also animated) to make room for it. If an element is leaving the list, the spot it took up needs to contract.

Because we have so many different things going on at the same time, we need to change our DOM structure to wrap each .list-item in a container class appropriately titled .list-container. This is absolutely essential in order to get our animation to work.

<ul class="list">   <li class="list-container">     <div class="list-item">List Item</div>   </li>   <li class="list-container">     <div class="list-item">List Item</div>   </li>   <li class="list-container">     <div class="list-item">List Item</div>   </li>   <li class="list-container">     <div class="list-item">List Item</div>   </li> </ul>  <button class="add-btn">Add New Item</button>

Now, the styling for this is unorthodox because, in order to get our animation effect to work later on, we need to style our list in a very specific way that gets the job done at the expense of sacrificing some customary CSS practices.

.list {   list-style: none; } .list-container {   cursor: pointer;   font-size: 3.5rem;   height: 0;   list-style: none;   position: relative;   text-align: center;   width: 300px; } .list-container:not(:first-child) {   margin-top: 10px; } .list-container .list-item {   background-color: #D3D3D3;   left: 0;   padding: 2rem 0;   position: absolute;   top: 0;   transition: all 0.6s ease-out;   width: 100%; } .add-btn {   background-color: transparent;   border: 1px solid black;   cursor: pointer;   font-size: 2.5rem;   margin-top: 10px;   padding: 2rem 0;   text-align: center;   width: 300px; }

How to handle spacing

First, we’re using margin-top to create vertical space between the elements in the stack. There’s no margin on the bottom so that the other list items can fill the gap created by removing a list item. That way, it still has margin on the bottom even though we have set the container height to zero. That extra space is created between the list item that used to be directly below the deleted list item. And that same list item should move up in reaction to the deleted list item’s container having zero height. And because this extra space expands the vertical gap between the list items further then we want it to. So that’s why we use margin-top — to prevent that from happening.

But we only do this if the item container in question isn’t the first one in the list. That’s we used :not(:first-child) — it targets all of the containers except the very first one (an enabling selector). We do this because we don’t want the very first list item to be pushed down from the top edge of the list. We only want this to happen to every subsequent item thereafter instead because they are positioned directly below another list item whereas the first one isn’t.

Now, this is unlikely to make complete sense because we are not setting any elements to zero height at the moment. But we will later on, and in order to get the vertical spacing between the list elements correct, we need to set the margin like we do.

A note about positioning

Something else that is worth pointing out is the fact that the .list-item elements nested inside of the parent .list-container elements are set to have a position of absolute, meaning that they are positioned outside of the DOM and in relation to their relatively-positioned .list-container elements. We do this so that we can get the .list-item element to float upwards when removed, and at the same time, get the other .list-item elements to move and fill the gap that removing this .list-item element has left. When this happens, the .list-container element, which isn’t positioned absolute and is therefore affected by the DOM, collapses its height allowing the other .list-container elements to fill its place, and the .list-item element — which is positioned with absolute — floats upwards, but doesn’t affect the structure of the list as it isn’t affected by the DOM.

Handling height

Unfortunately, we haven’t yet done enough to get a proper list where the individual list-items are stacked one by one on top of each other. Instead, all we will be able to see at the moment is just a single .list-item that represents all of the list items piled on top of each other in the exact same place. That’s because, although the .list-item elements may have some height via their padding property, their parent elements do not, but have a height of zero instead. This means that we don’t have anything in the DOM that is actually separating these elements out from each other because in order to do that, we would need our .list-item containers to have some height because, unlike their child element, they are affected by the DOM.

To get the height of our list containers to perfectly match the height of their child elements, we need to use JavaScript. So, we store all of our list items within a variable. Then, we create a function that is called immediately as soon as the script is loaded.

This becomes the function that handles the height of the list container elements:

const listItems = document.querySelectorAll('.list-item');  function calculateHeightOfListContainer(){ };  calculateHeightOfListContainer();

The first thing that we do is extract the very first .list-item element from the list. We can do this because they are all the same size, so it doesn’t matter which one we use. Once we have access to it, we store its height, in pixels, via the element’s clientHeight property. After this, we create a new <style> element that is prepended to the document’s body immediately after so that we can directly create a CSS class that incorporates the height value we just extracted. And with this <style> element safely in the DOM, we write a new .list-container class with styles that automatically have priority over the styles declared in the external stylesheet since these styles come from an actual <style> tag. That gives the .list-container classes the same height as their .list-item children.

const listItems = document.querySelectorAll('.list-item');  function calculateHeightOfListContainer() {   const firstListItem = listItems[0];   let heightOfListItem = firstListItem.clientHeight;   const styleTag = document.createElement('style');   document.body.prepend(styleTag);   styleTag.innerHTML = `.list-container{     height: $ {heightOfListItem}px;   }`; };  calculateHeightOfListContainer();

Showing and Hiding

Right now, our list looks a little drab — the same as the what we saw in the first example, just without any of the addition or removal logic, and styled in a completely different way to the list constructed from <ul> and <li> tags list that were used in that opening example.

Four light gray rectangular boxes with the words list item. The boxes are stacked vertically, one on top of the other. Below the bottom box is another box with a white background and thin black border that is a button with a label that says add new item.

We’re going to do something now that may seem inexplicable at the moment and modify our .list-container and .list-item classes. We’re also creating extra styling for both of these classes that will only be added to them if a new class, .show, is used in conjunction with both of these classes separately.

The purpose we’re doing this is to create two states for both the .list-container and the .list-item elements. One state is without the .show classes on both of these elements, and this state represents the elements as they are animated out from the list. The other state contains the .show class added to both of these elements. It represents the specified .list-item as firmly instantiated and visible in the list.

In just a bit, we will switch between these two states by adding/removing the .show class from both the parent and the container of a specific .list-item. We’ll combined that with a CSS transition between these two states.

Notice that combining the .list-item class with the .show class introduces some extra styles to things. Specifically, we’re introducing the animation that we are creating where the list item fades downwards and into visibility when it is added to the list — the opposite happens when it is removed. Since the most performant way to animate elements positions is with the transform property, that is what we will use here, applying opacity along the way to handle the visibility part. Because we already applied a transition property on both the .list-item and the .list-container elements, a transition automatically takes place whenever we add or remove the .show class to both of these elements due to the extra properties that the .show class brings, causing a transition whenever we either add or remove these new properties.

.list-container {   cursor: pointer;   font-size: 3.5rem;   height: 0;   list-style: none;   position: relative;   text-align: center;   width: 300px; } .list-container.show:not(:first-child) {   margin-top: 10px; } .list-container .list-item {   background-color: #D3D3D3;   left: 0;   opacity: 0;   padding: 2rem 0;   position: absolute;   top: 0;   transform: translateY(-300px);   transition: all 0.6s ease-out;   width: 100%; } .list-container .list-item.show {   opacity: 1;   transform: translateY(0); }

In response to the .show class, we are going back to our JavaScript file and changing our only function so that the .list-container element are only given a height property if the element in question also has a .show class on it as well, Plus, we are applying a transition property to our standard .list-container elements, and we will do it in a setTimeout function. If we didn’t, then our containers would animate on the initial page load when the script is loaded, and the heights are applied the first time, which isn’t something we want to happen.

const listItems = document.querySelectorAll('.list-item'); function calculateHeightOfListContainer(){   const firstListItem = listItems[0];   let heightOfListItem = firstListItem.clientHeight;   const styleTag = document.createElement('style');   document.body.prepend(styleTag);   styleTag.innerHTML = `.list-container.show {     height: $ {heightOfListItem}px;   }`;   setTimeout(function() {     styleTag.innerHTML += `.list-container {       transition: all 0.6s ease-out;     }`;   }, 0); }; calculateHeightOfListContainer();

Now, if we go back and view the markup in DevTools, then we should be able to see that the list has disappeared and all that is left is the button. The list hasn’t disappeared because these elements have been removed from the DOM; it has disappeared because of the .show class which is now a required class that must be added to both the .list-item and the .list-container elements in order for us to be able to view them.

The way to get the list back is very simple. We add the .show class to all of our .list-container elements as well as the .list-item elements contained inside. And once this is done we should be able to see our pre-created list items back in their usual place.

<ul class="list">   <li class="list-container show">     <div class="list-item show">List Item</div>   </li>   <li class="list-container show">     <div class="list-item show">List Item</div>   </li>   <li class="list-container show">     <div class="list-item show">List Item</div>   </li>   <li class="list-container show">     <div class="list-item show">List Item</div>   </li> </ul>  <button class="add-btn">Add New Item</button>

We won’t be able to interact with anything yet though because to do that — we need to add more to our JavaScript file.

The first thing that we will do after our initial function is declare references to both the button that we click to add a new list item, and the .list element itself, which is the element that wraps around every single .list-item and its container. Then we select every single .list-container element nested inside of the parent .list element and loop through them all with the forEach method. We assign a method in this callback, removeListItem, to the onclick event handler of each .list-container. By the end of the loop, every single .list-container instantiated to the DOM on a new page load calls this same method whenever they are clicked.

Once this is done, we assign a method to the onclick event handler for addBtn so that we can activate code when we click on it. But obviously, we won’t create that code just yet. For now, we are merely logging something to the console for testing.

const addBtn = document.querySelector('.add-btn'); const list = document.querySelector('.list'); function removeListItem(e){   console.log('Deleted!'); } // DOCUMENT LOAD document.querySelectorAll('.list .list-container').forEach(function(container) {   container.onclick = removeListItem; });  addBtn.onclick = function(e){   console.log('Add Btn'); } 

Starting work on the onclick event handler for addBtn, the first thing that we want to do is create two new elements: container and listItem. Both elements represent the .list-item element and their respective .list-container element, which is why we assign those exact classes to them as soon as we create the them.

Once these two elements are prepared, we use the append method on the container to insert the listItem inside of it as a child, the same as how these elements that are already in the list are formatted. With the listItem successfully appended as a child to the container, we can move the container element along with its child listItem element to the DOM with the insertBefore method. We do this because we want new items to appear at the bottom of the list but before the addBtn, which needs to stay at the very bottom of the list. So, by using the parentNode attribute of addBtn to target its parent, list, we are saying that we want to insert the element as a child of list, and the child that we are inserting (container) will be inserted before the child that is already on the DOM and that we have targeted with the second argument of the insertBefore method, addBtn.

Finally, with the .list-item and its container successfully added to the DOM, we can set the container’s onclick event handler to match the same method as every other .list-item already on the DOM by default.

addBtn.onclick = function(e){   const container = document.createElement('li');    container.classList.add('list-container');   const listItem = document.createElement('div');    listItem.classList.add('list-item');    listItem.innerHTML = 'List Item';   container.append(listItem);   addBtn.parentNode.insertBefore(container, addBtn);   container.onclick = removeListItem; }

If we try this out, then we won’t be able to see any changes to our list no matter how many times we click the addBtn. This isn’t an error with the click event handler. Things are working exactly how they should be. The .list-item elements (and their containers) are added to the list in the correct place, it is just that they are getting added without the .show class. As a result, they don’t have any height to them, which is why we can’t see them and is why it looks like nothing is happening to the list.

To get each newly added .list-item to animate into the list whenever we click on the addBtn, we need to apply the .show class to both the .list-item and its container, just as we had to do to view the list items already hard-coded into the DOM.

The problem is that we cannot just add the .show class to these elements instantly. If we did, the new .list-item statically pops into existence at the bottom of the list without any animation. We need to register a few styles before the animation additional styles that override those initial styles for an element to know what transition to make. Meaning, that if we just apply the .show class to are already in place — so no transition.

The solution is to apply the .show classes in a setTimeout callback, delaying the activation of the callback by 15 milliseconds, or 1.5/100th of a second. This imperceptible delay is long enough to create a transition from the proviso state to the new state that is created by adding the .show class. But that delay is also short enough that we will never know that there was a delay in the first place.

addBtn.onclick = function(e){   const container = document.createElement('li');    container.classList.add('list-container');   const listItem = document.createElement('div');    listItem.classList.add('list-item');    listItem.innerHTML = 'List Item';   container.append(listItem);   addBtn.parentNode.insertBefore(container, addBtn);   container.onclick = removeListItem;   setTimeout(function(){     container.classList.add('show');      listItem.classList.add('show');   }, 15); }

Success! It is now time to handle how we remove list items when they are clicked.

Removing list items shouldn’t be too hard now because we have already gone through the difficult task of adding them. First, we need to make sure that the element we are dealing with is the .list-container element instead of the .list-item element. Due to event propagation, it is likely that the target that triggered this click event was the .list-item element.

Since we want to deal with the associated .list-container element instead of the actual .list-item element that triggered the event, we’re using a while-loop to loop one ancestor upwards until the element held in container is the .list-container element. We know it works when container gets the .list-container class, which is something that we can discover by using the contains method on the classList property of the container element.

Once we have access to the container, we promptly remove the .show class from both the container and its .list-item once we have access to that as well.

function removeListItem(e) {   let container = e.target;   while (!container.classList.contains('list-container')) {     container = container.parentElement;   }   container.classList.remove('show');   const listItem = container.querySelector('.list-item');   listItem.classList.remove('show'); }

And here is the finished result:

Accessibility & Performance

Now you may be tempted to just leave the project here because both list additions and removals should now be working. But it is important to keep in mind that this functionality is only surface level and there are definitely some touch ups that need to be made in order to make this a complete package.

First of all, just because the removed elements have faded upwards and out of existence and the list has contracted to fill the gap that it has left behind does not mean that the removed element has been removed from the DOM. In fact, it hasn’t. Which is a performance liability because it means that we have elements in the DOM that serve no purpose other than to just accumulate in the background and slow down our application.

To solve this, we use the ontransitionend method on the container element to remove it from the DOM but only when the transition caused by us removing the .show class has finished so that its removal couldn’t possibly interrupt our transition.

function removeListItem(e) {   let container = e.target;   while (!container.classList.contains('list-container')) {     container = container.parentElement;   }   container.classList.remove('show');   const listItem = container.querySelector('.list-item');   listItem.classList.remove('show');   container.ontransitionend = function(){     container.remove();   } }

We shouldn’t be able to see any difference at this point because allwe did was improve the performance — no styling updates.

The other difference is also unnoticeable, but super important: compatibility. Because we have used the correct <ul> and <li> tags, devices should have no problem with correctly interpreting what we have created as an unordered list.

Other considerations for this technique

A problem that we do have however, is that devices may have a problem with the dynamic nature of our list, like how the list can change its size and the number of items that it holds. A new list item will be completely ignored and removed list items will be read as if they still exist.

So, in order to get devices to re-interpret our list whenever the size of it changes, we need to use ARIA attributes. They help get our nonstandard HTML list to be recognized as such by compatibility devices. That said, they are not a guaranteed solution here because they are never as good for compatibility as a native tag. Take the <ul> tag as an example — no need to worry about that because we were able to use the native unordered list element.

We can use the aria-live attribute to the .list element. Everything nested inside of a section of the DOM marked with aria-live becomes responsive. In other words, changes made to an element with aria-live is recognized, allowing them to issue an updated response. In our case, we want things highly reactive and we do that be setting the aria live attribute to assertive. That way, whenever a change is detected, it will do so, interrupting whatever task it was currently doing at the time to immediately comment on the change that was made.

<ul class="list" role="list" aria-live="assertive">

The Collapse Animation

This is a more subtle animation where, instead of list items floating either up or down while changing opacity, elements instead just collapse or expand outwards as they gradually fade in or out; meanwhile, the rest of the list repositions itself to the transition taking place.

The cool thing about the list (and perhaps some remission for the verbose DOM structure we created), would be the fact that we can change the animation very easily without interfering with the main effect.

So, to achieve this effect, we start of by hiding overflow on our .list-container. We do this so that when the .list-container collapses in on itself, it does so without the child .list-item flowing beyond the list container’s boundaries as it shrinks. Apart from that, the only other thing that we need to do is remove the transform property from the .list-item with the .show class since we don’t want the .list-item to float upwards anymore.

.list-container {   cursor: pointer;   font-size: 3.5rem;   height: 0;   overflow: hidden;   list-style: none;   position: relative;   text-align: center;   width: 300px; } .list-container.show:not(:first-child) {   margin-top: 10px; } .list-container .list-item {   background-color: #D3D3D3;   left: 0;   opacity: 0;   padding: 2rem 0;   position: absolute;   top: 0;   transition: all 0.6s ease-out;   width: 100%; } .list-container .list-item.show {   opacity: 1; }

The Side-Slide Animation

This last animation technique is strikingly different fromithe others in that the container animation and the .list-item animation are actually out of sync. The .list-item is sliding to the right when it is removed from the list, and sliding in from the right when it is added to the list. There needs to be enough vertical room in the list to make way for a new .list-item before it even begins animating into the list, and vice versa for the removal.

As for the styling, it’s very much like the Slide Down Opacity animation, only thing that the transition for the .list-item should be on the x-axis now instead of the y-axis.

.list-container {   cursor: pointer;   font-size: 3.5rem;   height: 0;   list-style: none;   position: relative;   text-align: center;   width: 300px; } .list-container.show:not(:first-child) {   margin-top: 10px; } .list-container .list-item {   background-color: #D3D3D3;   left: 0;   opacity: 0;   padding: 2rem 0;   position: absolute;   top: 0;   transform: translateX(300px);   transition: all 0.6s ease-out;   width: 100%; } .list-container .list-item.show {   opacity: 1;   transform: translateX(0); }

As for the onclick event handler of the addBtn in our JavaScript, we’re using a nested setTimeout method to delay the beginning of the listItem animation by 350 milliseconds after its container element has already started transitioning.

setTimeout(function(){   container.classList.add('show');    setTimeout(function(){     listItem.classList.add('show');   }, 350); }, 10);

In the removeListItem function, we remove the list item’s .show class first so it can begin transitioning immediately. The parent container element then loses its .show class, but only 350 milliseconds after the initial listItem transition has already started. Then, 600 milliseconds after the container element starts to transition (or 950 milliseconds after the listItem transition), we remove the container element from the DOM because, by this point, both the listItem and the container transitions should have come to an end.

function removeListItem(e){   let container = e.target;   while(!container.classList.contains('list-container')){     container = container.parentElement;   }   const listItem = container.querySelector('.list-item');   listItem.classList.remove('show');   setTimeout(function(){     container.classList.remove('show');     container.ontransitionend = function(){       container.remove();     }   }, 350); }

Here is the end result:

That’s a wrap!

There you have it, three different methods for animating items that are added and removed from a stack. I hope that with these examples you are now confident to work in a situation where the DOM structure settles into a new position in reaction to an element that has either been added or removed from the DOM.

As you can see, there’s a lot of moving parts and things to consider. We started with that we expect from this type of movement in the real world and considered what happens to a group of elements when one of them is updated. It took a little balancing to transition between the showing and hiding states and which elements get them at specific times, but we got there. We even went so far as to make sure our list is both performant and accessible, things that we’d definitely need to handle on a real project.

Anyway, I wish you all the best in your future projects. And that’s all from me. Over and out.


The post Animation Techniques for Adding and Removing Items From a Stack appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , ,
[Top]

Exploring the CSS Paint API: Blob Animation

After the fragmentation effect, I am going to tackle another interesting animation: the blob! We all agree that such effect is hard to achieve with CSS, so we generally reach for SVG to make those gooey shapes. But now that the powerful Paint API is available, using CSS is not only possible, but maybe even a preferable approach once browser support comes around.

Here’s what we’re making. It’s just Chrome and Edge support for now, so check this out on one of those browsers as we go along.

Live demo (Chrome and Edge only)

Building the blob

Let’s understand the logic behind drawing a blob using a classic <canvas> element to better illustrate the shape:

When talking about a blob, we’re also talking about the general shape of a distorted circle, so that’s the shape we can use as our base. We define N points that we place around a circle (illustrated in green).

const CenterX = 200; const CenterY = 200; const Radius = 150; const N = 10; var point = [];  for (var i = 0; i < N; i++) {   var x = Math.cos((i / N) * (2 * Math.PI)) * Radius + CenterX;   var y = Math.sin((i / N) * (2 * Math.PI)) * Radius + CenterY;   point[i] = [x, y]; }

Considering the center point (defined by CenterX/CenterY) and the radius, we calculate the coordinate of each point using some basic trigonometry.

After that, we draw a cubic Bézier curve between our points using quadraticCurveTo(). To do this, we introduce more points (illustrated in red) because a cubic Bézier curve requires a start point, a control point, and an end point.

The red points are the start and end points, and the green points can be the control points. Each red point is placed at the midpoint between two green points.

ctx.beginPath(); /* start the path */ var xc1 = (point[0][0] + point[N - 1][0]) / 2; var yc1 = (point[0][1] + point[N - 1][1]) / 2; ctx.moveTo(xc1, yc1); for (var i = 0; i < N - 1; i++) {   var xc = (point[i][0] + point[i + 1][0]) / 2;   var yc = (point[i][1] + point[i + 1][1]) / 2;   ctx.quadraticCurveTo(point[i][0], point[i][1], xc, yc); } ctx.quadraticCurveTo(point[N - 1][0], point[N - 1][1], xc1, yc1); ctx.closePath(); /* end the path */

Now all we have to do is to update the position of our control points to create the blob shape. Let’s try with one point by adding the following:

point[3][0]= Math.cos((3 / N) * (2 * Math.PI)) * (Radius - 50) + CenterX; point[3][1]= Math.sin((3 / N) * (2 * Math.PI)) * (Radius - 50) + CenterY;

The third point is closest to the center of our circle (by about 50px) and our cubic Bézier curve follows the movement perfectly to keep a curved shape.

Let’s do the same with all the points. We can use the same general idea, changing these existing lines:

var x = Math.cos((i / N) * (2 * Math.PI)) * Radius + CenterX; var y = Math.sin((i / N) * (2 * Math.PI)) * Radius + CenterY;

…into:

var r = 50*Math.random(); var x = Math.cos((i / N) * (2 * Math.PI)) * (Radius - r) + CenterX; var y = Math.sin((i / N) * (2 * Math.PI)) * (Radius - r) + CenterY;

Each point is offset by a random value between 0 and 50 pixels, bringing each point closer to the center by a slightly different amount. And we get our blob shape as a result!

Now we apply that shape as a mask on an image using the CSS Paint API. Since we are dealing with a blobby shape, it’s suitable to consider square elements (height equal to width) instead, where the radius is equal to half the width or height.

Here we go using a CSS variable (N) to control the number of points.

I highly recommend reading the first part of my previous article to understand the structure of the Paint API.

Each time the code runs, we get a new shape, thanks to the random configuration.

Let’s animate this!

Drawing a blog is good and all, but animating it is better! Animating the blob is actually the main purpose of this article, after all. We will see how to create different kinds of gooey blob animations using the same foundation of code.

The main idea is to smoothly adjust the position of the points — whether it’s all or some of them — to transition between two shapes. Let’s start with the basic one: a transition from a circle into a blob by changing the position of one point.

Animated gif shoeing a cursor hovering the right edge of a circular image. The right side of the image caves in toward the center of the shape on hover, and returns when the cursor leaves the shape.
Live demo (Chrome and Edge only)

For this, I introduced a new CSS variable, B , to which I am applying a CSS transition.

@property --b{   syntax: '<number>';   inherits: false;   initial-value: 0; } img {   --b:0;   transition:--b .5s; } img:hover {   --b:100 }

I get the value of this variable inside the paint() function and use it to define the position of our point.

If you check the code in the embedded linked demo, you will notice this:

if(i==0)    var r = RADIUS - B; else   var r = RADIUS

All the points have a fixed position (defined by the shape’s radius) but the first point specifically has a variable position, (RADIUS - B ). On hover, The value of B is changing from 0 to 100, moving our point closer to the middle while creating that cool effect.

Let’s do this for more points. Not all of them but only the even ones. I will define the position as follow:

var r = RADIUS - B*(i%2);
An animated gif showing a cursor hovering over a circular image. The shape of the image morphs to a sort of star-like shape when the cursor enters the image, then returns when the cursor leaves.
Live demo (Chrome and Edge only)

We have our first blob animation! We defined 20 points and are making half of them closer to the center on hover.

We can easily have different blobby variations simply by adjusting the CSS variables. We define the number of points and the final value of the B variable.

Live demo (Chrome and Edge only)

Now let’s try it with some random stuff. Instead of moving our points with a fixed value, let’s make that value random move them all around. We previously used this:

var r = RADIUS - B*(i%2);

Let’s change that to this:

var r = RADIUS - B*random();

…where random() gives us a value in the range [0 1]. In other words, each point is moved by a random value between 0 and B . Here’s what we get:

Live demo (Chrome and Edge only)

See that? We get another cool animation with the same code structure. We only changed one instruction. We can make that instruction a variable so we can decide if we want to use the uniform or the random configuration without changing our JavaScript. We introduce another variable, T, that behaves like a boolean:

if(T == 0)    var r = RADIUS - B*(i%2); else    var r = RADIUS - B*random();

We have two animations and, thanks to the T variable, we get to decide which one to use. We can control the number of points using N and the distance using the variable V. Yes, a lot of variables but don’t worry, we will sum up everything at the end.

What is that random() function doing?

It’s the same function I used in the previous article. We saw there that we cannot rely on the default built-in function because we need a random function where we are able to control the seed to make sure we always get the same sequence of random values. So the seed value is also another variable that we can control to get a different blob shape. Go change that value manually and see the result.

In the previous article, I mentioned that the Paint API removes all of the complexity on the CSS side of things, and that gives us more flexibility to create complex animations. For example, we can combine what we have done up to this point with keyframes and cubic-bezier():

Live demo (Chrome and Edge only)

The demo includes another example using the parabolic curve I detailed in a previous article.

Controlling the movement of the points

In all the blobs we’ve created so far, we considered the same movement for our points. Whether we’re using the uniform configuration or the random one, we always move the points from the edge to the center of the circle following a line.

Now let’s see how we can control that movement in order to get even more animations. The idea behind this logic is simple: we move the x and y differently.

Previously we were doing this:

var x = Math.cos((i / N) * (2 * Math.PI)) * (Radius - F(B)) + CenterX; var y = Math.sin((i / N) * (2 * Math.PI)) * (Radius - F(B)) + CenterY;

…where F(B) is a function based on the variable B that holds the transition.

Now we will have something like this instead:

var x = Math.cos((i / N) * (2 * Math.PI)) * (Radius - Fx(B)) + CenterX; var y = Math.sin((i / N) * (2 * Math.PI)) * (Radius - Fy(B)) + CenterY;

…where we’ve updating the x and y variables differently to makes more animations. Let’s try a few.

One axis movement

For this one, we will make one of the functions equal to 0 and keep the other one the same as before. In other words, one coordinate remains fixed along the animation

If we do:

Fy(B) = 0

…we get:

A cursor hovers two circular images, which has points that pull inward from the left and right edges of the circle to created jagged sides along the circles.
Live demo (Chrome and Edge only)

The points are only moving horizontally to get another kind of effect. We can easily do the same for the other axis by making Fx(B)=0 (see a demo).

I think you get the idea. All we have to do is to adjust the functions for each axis to get a different animation.

Left or right movement

Let’s try another kind of movement. Instead of making the points converging into the center, let’s make them move into the same direction (either right or left). We need a condition based on the location of the point which is defined by the angle.

Illustration showing the blue outline of a circle with 8 points around the shape and thick red lines bisecting the circle to show the axes.

We have two group of points: ones in the [90deg 270deg] range (the left side), and the remaining points along the ride side of the shape. If we consider the indexes, we can express the range differently, like [0.25N 0.75N] where N is the number of points.

The trick is to have a different sign for each group:

var sign = 1; if(i<0.75*N && i>0.25*N)    sign = -1; /* we invert the sign for the left group */ if(T == 0)    var r = RADIUS - B*sign*(i%2); else    var r = RADIUS - B*sign*random(); var x = Math.cos((i / N) * (2 * Math.PI)) * r + cx;

And we get:

An animated gif showing a cursor entering the right side of a circular image, which has points the are pulled toward the center of the circle, then return once the cursor exits.
Live demo (Chrome and Edge only)

We are able to get the same direction but with one small drawback: one group of the points are going outside the mask area because we are increasing the distance on some points while decreasing the distance on others. We need to reduce the size of our circle to leave enough space for all of our points.

We simply decrease the size of our circle using the V value that defines the final value for our B variable. In other words, it’s the maximum distance that one point can reach.

Our initial shape (illustrated by the grey area and defined with the green points) will cover a smaller area since we will decrease the Radius value with the value of V:

const V = parseFloat(properties.get('--v')); const RADIUS = size.width/2 - V;
Live demo (Chrome and Edge only)

We fixed the issue of the points getting outside but we have another small drawback: the hover-able area is the same, so the effect starts even before the cursor hits the image. It would be good if we can also reduce that area so everything is consistent.

We can use an extra wrapper and a negative margin trick. Here’s the demo. The trick is pretty simple:

.box {   display: inline-block;   border-radius: 50%;   cursor: pointer;   margin: calc(var(--v) * 1px);   --t: 0; }  img {   display: block;   margin: calc(var(--v) * -1px);   pointer-events: none;   -webkit-mask: paint(blob);   --b: 0;   transition:--b .5s; } .box:hover img {   --b: var(--v) }

The extra wrapper is an inline-block element. The image inside it has negative margins equal to the V variable which reduces the overall size of the shape’s box. Then we disable the hover effect on the image element (using pointer-events: none) so only the box element triggers the transition. Finally we add some margin to the box element to avoid any overlap.

Like the previous effect, this one can also be combined with cubic-bezier() and keyframes to get more cool animations. Below is an example using my sinusoidal curve for a wobbling effect on hover.

Live demo (Chrome and Edge only)

If we add some transforms, we can create a kind of strange (but pretty cool) sliding animation:

A circular image is distorted and slides from right to left on hover in this animated gif.
Live demo (Chrome and Edge only)

Circular movement

Let’s tackle another interesting movement that will allow us to create infinite and “realistic” blob animations. Instead of moving our points from one location to another, we will rotate them around an orbit to have a continuous movement.

The blue outline of a circle with ten green points along its edges. A gray arrow shows the circle's radius and assigns it to a point on the circle with a variable, r, that has a red circle around it showing its hover boundary.

The initial location of our points (in green) will become an orbit and the red circle is the path that our points will take. In other words, each point will rotate around its initial position following a circle having a radius r.

All we need to do is make sure there is no overlap between two adjacent paths so the radius need to have a maximum allowed value.

I will not detail the math but the max value is equal to:

const r = 2*Radius*Math.sin(Math.PI/(2*N));
A blobby shape moves in a circular motion around an image of a cougar's face.
Live demo (Chrome and Edge only)

This is the relevant part of the code:

var r = (size.width)*Math.sin(Math.PI/(N*2)); const RADIUS = size.width/2 - r; // ...  for(var i = 0; i < N; i++) {   var rr = r*random();   var xx = rr*Math.cos(B * (2 * Math.PI));   var yy = rr*Math.sin(B * (2 * Math.PI));    var x = Math.cos((i / N) * (2 * Math.PI)) * RADIUS + xx + cx;   var y = Math.sin((i / N) * (2 * Math.PI)) * RADIUS + yy + cy;   point[i] = [x,y]; }

We get the max value of the radius and we reduce that value from the main radius. Remember that we need to have enough space for our points so we need to reduce the mask area like we did with the previous animation. Then for each point we get a random radius rr (between 0 and r). Then we calculate the position inside the circular path using xx and yy. And, finally, we place the path around its orbit and get the final position (the x, y values).

Notice the value B which is, as usual, the one with the transition. This time, we will have a transition from 0 to 1 in order to make a full turn around the orbit.

Spiral movement

One more for you! This one is a combination of the two previous ones.

We saw how to move the points around a fixed orbit and how to move a point from the edge of the circle to the center. We can combine both and have our point move around an orbit and we do the same for the orbit by moving it from the edge to the center.

Let’s add an extra variable to our existing code:

for(var i = 0; i < N; i++) {   var rr = r*random();   var xx = rr*Math.cos(B * (2 * Math.PI));   var yy = rr*Math.sin(B * (2 * Math.PI));     var ro = RADIUS - Bo*random();   var x = Math.cos((i / N) * (2 * Math.PI)) * ro + xx + cx;   var y = Math.sin((i / N) * (2 * Math.PI)) * ro + yy + cy;   point[i] = [x,y]; }

As you can see, I used the exact same logic as the very first animation we looked at. We reduce the radius with a random value (controlled with Bo in this case).

A blob morphs shape as it moves around an image of a cougar's face.
Live demo (Chrome and Edge only)

Yet another fancy blob animation! Now each element has two animations: one animates the orbit (Bo), and the other animates the point in its circular path (B). Imagine all the effects that you can get by simply adjusting the animation value (duration, ease, etc.)!

Putting everything together

Oof, we are done with all the animations! I know that some of you may have gotten lost with all the variations and all the variables we introduced, but no worries! We will sum everything up right now and you will see that it’s easier than what might expect.

I want to also highlight that what I have done is not an exhaustive list of all the possible animations. I only tackled a few of them. We can define even more but the main purpose of this article is to understand the overall structure and be able to extend it as needed.

Let’s summarize what we have done and what are the main points:

  • The number of points (N): This variable is the one that controls the granularity of the blob’s shape. We define it in the CSS and it is later used to define the number of control points.
  • The type of movement (T): In almost all the animations we looked at, I always considered two kind of animations: a “uniform” animation and a “random” one. I am calling this the type of movement that we can control using the variable T set in the CSS. We will have somewhere in the code to do an if-else based on that T variable.
  • The random configuration: When dealing with random movement, we need to use our own random() function where we can control the seed in order to have the same random sequence for each element. The seed can also be considered a variable, one that generates different shapes.
  • The nature of movement: This is the path that the points take. We can have a lot of variations, for example:
    • From the edge of the circle to the center
    • A one axis movement (the x or y axis)
    • A circular movement
    • A spiral movement
    • and many others…

Like the type of movement, the nature of the movement can also be made conditional by introducing another variable, and there is no limit to what can be done here. All we have to do is to find the math formula to create another animation.

  • The animation variable (B): This is the CSS variable that contains the transition/animation. We generally apply a transition/animation from 0 to a certain value (defined in all the examples with the variable V). This variable is used to express the position of our points. Updating that variable logically updates the positions; hence the animations we get. In most cases, we only need to animate one variable, but we can have more based on the nature of the movement (like the spiral one where we used two variables).
  • The shape area: By default, our shape covers the entire element area, but we saw that some movement require the points to go outside the shape. That’s why we had to reduce the area. We generally do this by the maximum value of B (defined by V), or a different value based on the nature of the movement.

Our code is structured like this:

var point = [];  /* The center of the element */ const cx = size.width/2; const cy = size.height/2; /* We read all of the CSS variables */ const N = parseInt(properties.get('--n')); /* number of points */ const T = parseInt(properties.get('--t')); /* type of movement  */ const Na = parseInt(properties.get('--na')); /* nature of movement  */ const B = parseFloat(properties.get('--b')); /* animation variable */ const V = parseInt(properties.get('--v'));  /* max value of B */ const seed = parseInt(properties.get('--seed')); /* the seed */ // ...  /* The radius of the shape */ const RADIUS = size.width/2 - A(V,T,Na);  /* Our random() function */ let random =  function() {   // ... } /* we define the position of our points */ for(var i = 0; i < N; i++) {    var x = Fx[N,T,Na](B) + cx;    var y = Fy[N,T,Na](B) + cy;    point[i] = [x,y]; }  /* We draw the shape, this part is always the same */ ctx.beginPath(); // ... ctx.closePath(); /* We fill it with a solid color */ ctx.fillStyle = '#000'; ctx.fill();

As you can see, the code is not as complex as you might have expected. All the work is within those function Fx and Fy, which defines the movement based on N,T and Na. We also have the function A that reduces the size of the shape to prevent points overflowing the shape during the animation.

Let’s check the CSS:

@property --b {   syntax: '<number>';   inherits: false;   initial-value: 0; }  img {   -webkit-mask:paint(blob);   --n: 20;   --t: 0;   --na: 1;   --v: 50;   --seed: 125;   --b: 0;   transition: --b .5s; } img:hover {   --b: var(--v); }

I think the code is self-explanatory. You define the variables, apply the mask, and animate the B variable using either a transition or keyframes. That’s all!

I will end this article with a final demo where I put all the variations together. All you have to do is to play with the CSS variables


Exploring the CSS Paint API series:


The post Exploring the CSS Paint API: Blob Animation appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Stealing Game Animation Techniques to Engage Users

Today’s websites are overflowing with animations—often too many. They get in the way of the content and slow down our busy users. But at the same time: they’re wonderful. They bring websites to life, are fun to implement and can be incredibly impressive to show off. I think they’re great. Sorry impatient users.

The way I see it, the problem isn’t necessarily that websites have too many animations, but that the animations don’t vibe with the content they’re promoting. They’re out of place with their subject matter. They feel contrived and provide no additional value.

This is an article for web developers who want to get fancy-shmancy with the finest animations around, but don’t want to do it at the cost of annoying users. I’ll show you some of the ways I’ve personally used website animations while trying to annoy very few users. You might be thinking that “not annoying users” is a very low bar that I’ve set and well, uh.. hmm.. yes. Good point.

This is a topic I’ve somewhat stumbled my way into. I work as a web developer for an indie video game publisher called Devolver Digital. I, along with Vieko, make websites for these video games. While I’m primarily a dev, a good chunk of my job is to conceptualize and create designs. I once thought that design didn’t seem that hard, the truth is, as most of you probably know, it’s not that straightforward. It’s really, really hard.

My first design for the Ape Out website 😬

When I first started making video game websites, I would sketch it out in a notebook. This would be fine, but then when I tried to implement the design in Photoshop, it just wouldn’t come together. I would restart from scratch and try again. It still wouldn’t work. Then I would try to skip the whole design step and jump into the code, hoping that the animations would bring it all together. The animations were cool, but it still wasn’t good enough. My design skills were lacking.

But then I figured out a way to fake it.

Now, when I show website concepts to my colleagues and clients for feedback, they tell me that I’m the best designer in the whole world and they send me cookies. They don’t know that I’m hiding a terrible secret.

I don’t know if I should reveal this secret. Web designers around the world are going to hate me. My colleagues will ask for their cookies back. OK—I ate the cookies. I’ll let you in on the secret. The secret is that…

I copy everything from video games.

I copy the colors, the buttons, the modal boxes, and even the core concepts and gameplay mechanics. I play the games (they’re fun), take screenshots, capture footage, and then I steal it all. Most important of all, I steal the animations.

While video games make for a perfect medium from which to burgle, I believe that people like you can also steal from your corresponding industry. While stealing from games helped me cheat as a designer, it was stealing the animations that really unlocked the full potential of each website.

It’s free real estate

We, at Devolver Digital, have a massive variety of games. Some are calming, narrative experiences, while others have very intriguing gameplay elements. Some have very simple visuals that hide deep secrets, and others are Shadow Warrior 3.

Each one of these games has a unique way of wiggling themselves into a player’s brain and setting up a cozy little living space. With each website, we attempt to replicate this brain connection and harness it. Modern web tech is at a place now where we can make websites that effectively engage our users into making this brain connection before they’ve even grabbed a gamepad.

So basically, we heist assets/animations/mechanics/everything from the game to give the user a glimpse at what it would be like to play it.

For example

I want to start with one of my favorite projects. It’s the website for a game called Olija. It’s about a harpoon-wielding hero exploring hostile lands to find his way home. The game has two core aspects. The first is its visual fluidity. The pixel size in its pixel art is fairly large, so it can be difficult to convey this game’s beauty without seeing it in motion. The second core aspect is its story. It feels like the game is based on a pre-existing book or TV series.

The goal for the website was to take these two core aspects, and entice the user to engage with them; to make them feel like a part of the story and to make them feel in control of the action.

I’m going to jump away from Olija for a second here. I should have mentioned this earlier. Websites have an aspect that gives them an advantage over most other media: they’re interactive (obviously). This is great because we can use this interactivity to get the user to engage with the site at a deeper level. This is key. We have a few tools we can use to make their experience more vibrant and memorable. These tools are things like mouse movement, mouse hovers, pointer down/up states, the scrollbar, and the keyboard. My favorite is the scrollbar. You’ll see it used often in my projects. I feel it’s the most intuitive to use and I like that it requires little thought or active energy from the user.

Back to Olija. One of the first things the user sees upon scrolling down is a movie-credits-like fade animation. It’s slow. It takes three viewport heights (300vh) to scroll through it all but it’s a critical part. It sets up the pacing and immediately shows the tone of the game. It’s a very simple animation but it was vital. The rest of the page felt much more natural once this section was extended and slowed down.

Olija credits sequence

After the story sections, the user gets to dynamic screenshot-like sections. There’s one section in particular in which the hero runs across a lively forest backdrop. Showing a lighter moment like this one reinforces the epic-ness of the story and shows off the game’s unique style.

One of the trickier parts for Olija, is that pixel-art is based on sprite sheets. We can’t just animate an element’s transform properties to move it around the screen. We have to also animate its current position in the sprite sheet. Here’s a Pen that shows how we did it.

We took a similar approach with Ape Out. The game feels like an action movie. As the game’s title accurately describes, the player is an ape trying to escape from various scenarios. The idea for the accompanying website was to show a scene that had already been played out, letting the user imagine all the action that leads up to the final epic moment. Again, they control the pace of the camera that explores this scene with the scroll bar.

Ape Out Camera Pan

At first glance it might look like a 3D WebGL canvas, but it’s actually a lot of divs placed with a 3D transform where the corresponding perspective origin is updated based on the scroll position. Not sure if it’s any better or easier than using ThreeJS but it’s.. uhh.. doable. Here’s a barebones version of it in CodePen.

Enter the Gungeon in the Living Room (Illustration by Björn Feldmann)

As a last example for story-driven experiences, I want to mention the Enter the Gungeon website. It’s a mix of the Olija and Ape Out ideas, but instead of trying to capture the game’s narrative, it attempts to celebrate the time players have spent with it. The goal was to slow down and let the user reminisce, then use this nostalgia to promote the Exit the Gungeon and House of the Gundead games.

Unique selling point

Sometimes games have a unique hook or gameplay element and the website is the perfect place to showcase it. A great example is Loop Hero. It’s a very simple game on the surface. The hero automatically travels along a looping path and fights the monsters they encounter.

The player controls the hero’s equipment and whether they abandon the path to return to the village. This made infinite looping a clear concept for the website. When the user reaches the bottom of the page they are seamlessly transported back to the top and the page resets. The scene in the viewport is exactly the same so the user is none-the-wiser.

Loop Hero’s looping website

There are a few other concepts on this site that are directly taken from the game. Once the user scrolls to the battle trigger area, the scrollbar is disabled (sorry, I know it’s bad) and they are forced to watch the battle play out. The hero and monsters each have health points, attack speed, and attack power values with random variance. This means that the battle outcomes are not hard-coded. Each time the user loops through the page, the background map is updated with new tiles to show how the game progresses and evolves.

One of my favorite features of the Loop Hero website is the fade effect between the top section and the looped road section below it. The game developer at Four Quarters sent me the shader code (written with help from @kartonnnyi) and the Perlin Noise image used for this effect. I can’t say I fully understand how it all works, but I was able to put it together with gl-react.

People with a deep understanding of how to write shaders have my deepest respect. I don’t think I’ll ever fully be able to wrap my head around them. The stuff at Shadertoy blows my mind.

Boomerang X and Shadow Warrior are similar websites that use gameplay footage as a direct way to show off the game. They both engage the user with elements connected with the background video. The Shadow Warrior website content shakes when there’s an impact in the video and blood slowly gathers on the menu/logos. The Boomerang X logo zips back and forth along with the game footage’s boomerang. The logo is further connected with the user by having it react to the user’s mouse position.

Alright, now let me tell you about Devolver Tumble Time. I think most web developers, upon being given the opportunity to make a website for this game, would feel an urge to replicate the tumbler mechanic. From first-hand experience, I can say that it’s not as simple as it looks. I knew that it was critical to get the tumbler to run as fluidly as possible, otherwise it would reflect poorly on the game and turn people away.

I started out with MatterJS. It seemed like the obvious choice. The tumbler is two-dimensional, so pick the most popular 2D engine right? Well, it turns out the tumbler isn’t 2D and I don’t think centrifugal force is a thing in MatterJS. I, like another soul before me, attempted to hack it in. I tried to implement their attempt unsuccessfully.

IT’S TUMBLE TIME

To get the tumbler to work, I needed two directions for the gravity. The items should fall down the y-axis, but they also need to be able to “stick” to a spinning disc through gravity/friction on the z-axis. I spent a weekend refactoring the tumbler code in ThreeJS and achieved the desired fluidity.

Down into the abyss

I want to end this article with a somewhat deep-dive into the Phantom Abyss website. It encompasses a lot of the techniques used in recent projects and it only weighs 4MB.

The website might seem fairly barebones, but the longer the user looks at the page, the more there is to see. The shifting blocks and fading phantoms should be obvious, and then the user might see that the waterfalls are animated, along with some dust particles, the torches, the woman’s hair and whip, and then lastly the phantom’s eyes above the logo, a sparkle on some of the relics, and the fires and birds in the far background.

Phantom Abyss Animations (Illustration by Dan Mumford)

Now, I’m definitely not expecting many users to notice all these minor details, but they bring depth to the page. They engage the user into subconsciously understanding that the game holds secrets and depth.

This is the first project on which I used SVGator, a really neat tool for animating SVGs. I used this for the torches, the woman’s hair/whip, and the phantom’s blinking eyes.

The torches were not easy. My first attempt looked like creepy wild tentacles reaching out to grab whatever they could find. My second attempt was better but not quite there. My third attempt still wasn’t good enough, but adding blur and some sparks brought it to the point where it is today. If you look very closely the torches still look a bit like tentacles. In my next project I saw that the key-art featured a lot of tentacles. I knew what I had to do.

Start inside

These are just a few of the examples of animation tricks we’ve used on websites. Devolver Digital has a never-ending catalog of games and from that site you can check out other websites we’ve made. Ragnorium, Heave Ho, and Gato Roboto are some of our sites that stole a lot from their games. Also, full credit to Vieko as he was the original game animation burglar with his sites for Minit and Sludge Life.

Our sites are mostly all hosted on Vercel ❤ with NextJS 👌. We also rely heavily on Framer Motion 🤩 and often react-three-fiber 🎆. There are dozens of alternative tools that are also great but we love these ones and they’ve made our lives much easier.

Today, we developers have access to an immense set of tools and techniques with new ones being announced all the time. It’s very easy to get caught up in what other talented devs are doing and we then try to replicate their effects. This isn’t necessarily a bad approach, but it can lead to being closed-off and repetitious. This causes us to then create animations that users have already seen dozens of times.

Instead of looking at other devs and their work for inspirational animations and transitions, we should look at the content and subject matter we’re showcasing. Invent completely new animations. We should take a step back and really contemplate how we can engage the users to connect with our websites and deepen their experiences.


The post Stealing Game Animation Techniques to Engage Users appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]