CSS-only DVD Screensaver animation

A visual walkthrough

Five minutes later, my mind had already wandered into ”how would I animate the bouncing DVD logo” territory.

My first thought was that Javascript was required to figure out when the collisions with the edges of the screen would happen, otherwise I would not be able to change the color.

Thus, I set out to prove myself wrong. In this article I'll go through my thought process and how I got it to work with just HTML and CSS.

My strategy to reverse engineer an animation is always the same: good old divide and conquer:

The whole thing
=
Bounce around
+
Change colors

Let's focus on the movement.

Can we divide it further?

Can we divide it further? Yes, with one of the most useful strategies in the animator's toolkit: the bouncing animation can be split off into independent movements across the horizontal and vertical axes.

That's simple enough to animate. Let's write some code!


:root {

Let's set our desired container and logo sizes, as well as the animation speed, using CSS variables:


  --container-w: 320; /* no units so we can do division */
  --container-h: 180;
  --logo-w: 40;
  --logo-h: 20;
  --speed: 90; /* pixels per second */

We can easily calculate how long it takes for the logo to move through the screen, both horizontally and vertically.


  --duration-x: calc(
    (var(--container-w) - var(--logo-w)) / var(--speed) * 1s
  );
  --duration-y: calc(
    (var(--container-h) - var(--logo-h)) / var(--speed) * 1s
  );
}

Assume the container element has relative positioning and the appropriate size. We then set the properties for the logo and animate left and top in a linear fashion, forever, alternating back and forth.


.logo {
  position: absolute;
  width: calc(var(--logo-w) * 1px);
  height: calc(var(--logo-h) * 1px);
  animation:
    x var(--duration-x) linear infinite alternate,
    y var(--duration-y) linear infinite alternate;
}

@keyframes x {
  from { left: 0; }
  to { left: calc(100% - var(--logo-w)); }
}

@keyframes y {
  from { top: 0; }
  to { top: calc(100% - var(--logo-h)); }
}

If we only needed to change colors when the logo hit the left or right wall, this idea would work:


First, define keyframes to change color at regular intervals, five of them in this case. I'm using equispaced hue values on the HSL color spectrum.


@keyframes colorX {
  from { background: hsl(0deg 100% 50%); }
  20% { background: hsl(72deg 100% 50%); }
  40% { background: hsl(144deg 100% 50%); }
  60% { background: hsl(216deg 100% 50%); }
  80% { background: hsl(288deg 100% 50%); }
  to { background: hsl(0deg 100% 50%); }
}

Then we just need to add a new animation with the appropriate duration and the step-start timing function so the value "jumps" once every --duration-x.


.logo {
  animation:
    x var(--duration-x) linear infinite alternate,
    y var(--duration-y) linear infinite alternate,
    colorX calc(var(--duration-x) * 5) step-start infinite;
}
But...
How can we do this for all four walls?
How can we do this for all four walls?

We animate two CSS variables, then define our hue based on a linear combination of both.

Animate CSS variables? Can you even do that?

Well, yes, but it's something you hardly ever see because they don't behave how you expect.

In most cases, animating a CSS variable is about as useful as animating any non-interpolable property such as display or visibility.

.animate-width-variable {
  width: calc(var(--width) * 1%);
  animation: frames 1s linear infinite alternate;
}

@keyframes frames {
  from { --width: 10; }
  to { --width: 100;
}

This is the result:

As you can see, it's not a smooth one. It just goes from one value to the next with no interpolation.

Thing is... we don't need smooth interpolation, we're using step-start timing anyway.

@keyframes colorX {
  from { --color--x: 0; }
  20% { --color-x: 2; }
  40% { --color-x: 4; }
  60% { --color-x: 1; }
  80% { --color-x: 3; }
  to { --color-x: 0; }
}

@keyframes colorY {
  from { --color-y: 0; }
  20% { --color-y: 2; }
  40% { --color-y: 4; }
  60% { --color-y: 1; }
  80% { --color-y: 3; }
  to { --color-y: 0; }
}

.logo {
  animation:
    x var(--duration-x) linear infinite alternate,
    y var(--duration-y) linear infinite alternate,
    colorX calc(var(--duration-x) * 5) step-start infinite,
    colorY calc(var(--duration-y) * 5) step-start infinite;
  color:
    hsl(calc(
      360 / 25 * (var(--color-y) * 5 + var(--color-x))
    ) 100% 50%);
}

There's a total of 5x5=25 possible colors, equispaced hue values ranging from 0deg when --color-x and --color-y are both 0, to 345.6deg when they're both 4.

I could've used 0-1-2-3-4 for the variables, but if we increment in a star pattern (0-2-4-1-3), each color jump is a bit more noticeable.

I hope you both enjoyed the article and learned something you might use for a more practical project.

Here's a Codepen link to the final result.

And just because I can't fathom finishing this article without even mentioning corner hits, I've made a version with interactive parameters and a corner collision calculator. See it in the next slide.

Sorry, the interactive experience is not available on smaller screens.

Note

In the future you may see more animated CSS properties out in the wild thanks to the new @property syntax. Interpolation is still not supported on Firefox as of February 2024.

As a matter of fact, I'm already using CSS variable interpolation in the animated background of this site. The background defaults to a static version on browsers that don't support it.


Sources


Published on February 14, 2024.

Read the sequel where I build a corner hit timer.