20 December 2018

Case Study: lynnandtonic.com 2018 refresh

It’s that time of year again where I refresh my portfolio site. The 2017 redesign got a huge response, probably my favorite version of the site ever. If you’re curious, you can view the archived 2017 site or read the case study.

In sum though, the 2017 redesign featured 20 media queries where the site changed drastically every 100px. I knew this year I wanted to build upon that concept.

In the previous version, the 21 separate layouts each utilized their available space, but their order in sequence wasn’t all that deliberate. I thought it might be interesting if things still changed as you scaled the browser, but the change was meaningful depending on whether you were scaling up or down.

A few ideas seemed promising:

With all of these, it felt like animation was going to be an important part. I researched and thought a lot about various animation techniques and styles. Should it be CSS-drawn? Should JavaScript lend a hand here? How can I have interesting imagery that changes over many breakpoints without having a massive amount to download? I knew it’d probably be a lot of work, but how much work?

A good compromise I landed on was animating a single character. I could keep the illustration style pretty simple and possibly show various frames by shifting a background-image sprite in the same way we do for other imagery on our websites.

So what could be fun with that constraint? I thought about a person moving across the screen as the browser moved or getting more cramped inside a “box” as the browser shrunk. I knew I wanted the animation to work both ways—so you could resize the browser randomly and everything still made sense.

Exploratory illustrations of Lynn’s face smooshed against an implied glass wall.some early exploratory sketches

If anything was going to shrink/grow with the browser, why not clothes? They could shrink teeny tiny and grow huge and baggy. That might be fun. I tried a few illustration styles (including a nod to The Loud House) before settling on paying tribute to the genius Bob’s Burgers.

Exploratory illustrations of Lynn in different styles.more exploratory sketches

With this style (from the show outro), I could keep things simple and make a fun background that wouldn’t distract from the character animation in the foreground.

An animated gif of Jimmy Pesto Jr. dancing in the Bob’s Burgers outro.

Preparing the artwork

I started by drawing three full frames to see if it felt like a good direction to go. All the people I showed laughed when they saw it, so I decided to go all in.

Three illustrations of Lynn, one with tiny, shrunken clothes, one normal, and one with huge, oversized clothes.

As I drew all the in-between frames, I realized it would be a lot to download, especially if I was using SVG (so many anchor points). So I continued making every frame for the items that needed it and reusable frames for other pieces.

An image sprite of all the frames for Lynn’s clothing.

The hair, faces, mouths, arms and legs became little reusable libraries. I sprited together as much as made sense to reduce the number of files.

And with SVG, every anchor point matters, so I spent some time simplifying and optimizing the artwork in Illustrator. I made small changes to reduce points, like smoothing lines, removing some shadowing, and ditching the ribbed lines on the hoodie’s cuffs.

I’ll tell you what, I learned a lot of new shortcuts in Illustrator. Nothing quite like extremely tedious tasks to get you streamlining. After export, I ran it through a handful of SVG optimizers until I got a result I felt comfortable with.

In between all of that, I did dabble with the idea of using PNGs instead. File size was sometimes smaller, but I lost the scaling flexibility I ultimately decided I wanted.

Markup and Styling

Sorry this isn’t a single div 😂. I set up the background and character with the following markup:

  <div class="lynns-burgers">
    <div class="lynn">
      <div class="other legs"></div>
      <div class="other shoes"></div>
      <div class="clothes pants"></div>
      <div class="other arm arm-left"></div>
      <div class="other arm arm-right"></div>
      <div class="other hair"></div>
      <div class="other shirt"></div>
      <div class="clothes hoodie"></div>
      <div class="other face"></div>
      <div class="other mouth"></div>
    </div>
  </div>

With regards to the CSS, I’ll start by saying I would not have attempted this without a preprocessor. I’m using Stylus variables and mixins a whole bunch to make everything more manageable (I ultimately needed to write over 220 media queries). This does add a lot of moving pieces and a few layers of abstraction. It made it worlds easier for me to accomplish what I needed but, as I’ve realized trying to write this, it makes things a lot harder to explain.

So, in the interest of time and all of our brain goo, I’ll go over a couple specific pieces that went into producing the animated sequence. If you want to dig in and see all the weird decisions I made, you can view the source code on GitHub.

For most of the pieces (like the shoes, legs, clothing, hair), I give the <div> dimensions, declare a background sprite, and position it appropriately. Then its background-position is shifted, giving each media query a new frame of the sequence.

Let’s say the frames changed every 20px, and the width of each .hair frame is 100px, then the CSS might look something like this:

  @media screen and (min-width: 501px) and (max-width: 520px) {
    .hair {
      background-position: 0 0;
    }
  }
  @media screen and (min-width: 521px) and (max-width: 540px) {
    .hair {
      background-position: -100px 0;
    }
  }
  @media screen and (min-width: 541px) and (max-width: 560px) {
    .hair {
      background-position: -300px 0;
    }
  }

Some of the pieces (like arms, mouths, and chin dimples), needed to have their background-position changed with each query, but also to be positioned and sometimes rotated.

So a media query for these pieces might look more like this:

  @media screen and (min-width: 501px) and (max-width: 520px) {
    .arm-left {
      background-position: -50px 0;
      left: 103px;
      top: 56px;
      transform: rotate(14deg);
    }
  }

This is a lot of CSS to write (44 frames for most character pieces), so Stylus is a huge help here. This might need some closer inspection at the source files to really grok, but here’s a preview of what I was doing.

I was able to reuse a lot of the media query syntax with variables and mixins:

  /* Set min-width and frame increment */
  $min-w = 412px
  $step = 23

  /* Shift background-position only */
  frame(number, args) {
    @media screen and (min-width: $min-w + $step * number) and (max-width: $min-w + $step * (number + 1) - 1) {
      background-position: args;
    }
  }

  /* Shift position only */
  move(number, left, bottom) {
    @media screen and (min-width: $min-w + $step * number) and (max-width: $min-w + $step * (number + 1) - 1) {
      left: left;
      bottom: bottom;
    }
  }

  /* Shift background-position, position, and rotate */
  place(number, args, left, top, deg) {
    @media screen and (min-width: $min-w + $step * number) and (max-width: $min-w + $step * (number + 1) - 1) {
      background-position: args;
      left: left;
      top: top;
      transform: rotate(deg);
    }
  }

With a bunch of other variable declarations, I was able to write each of the frames/queries like this:

  .hair
    frame(0, $hair-2)
    frame(1, $hair-3)
    frame(2, $hair-4)
    frame(3, $hair-2)
    frame(4, $hair-5)
    ...

  .arm-left
    place(0, $arm-3, calc(var(--arm-base) * .74), calc(var(--arm-base) * 1.18), 14deg)
    place(1, $arm-5, calc(var(--arm-base) * .72), calc(var(--arm-base) * 1.18), 10deg)
    place(2, $arm-7, calc(var(--arm-base) * .78), calc(var(--arm-base) * 1.22),  9deg)
    place(3, $arm-5, calc(var(--arm-base) * .78), calc(var(--arm-base) * 1.18),  4deg)
    place(4, $arm-5, calc(var(--arm-base) * .76), calc(var(--arm-base) * 1.18),  7deg)
    ...

I know that place declaration looks a bit scary, but it provided the most clarity for me about what was happening with each query. I’m using calc() and CSS custom properties here which I’ll get to in a bit.

Hold up

You might be wondering why I didn’t save the entire character for each frame so I’d only have to shift one large image sprite. Why the pain of all these different variables, mixins, and things?

The practical reason is the sprite would have been a ginormous file, even as a PNG and especially as an SVG. Breaking the illustration into pieces made file sizes more manageable.

But to be honest, shifting one large sprite just isn’t as fun?

I wanted to try this and see if I could do it. It was challenging and satisfying figuring out how to organize things, how to piece everything together, and how to work through all the small tweaks to make the animation work. Something something Frank Chimero, the long, hard, stupid way something something.

So what else was involved?

There are a few more media query mixins I used for more fine-grain control of what was happening. A ‘range’ query to have a frame show for multiple $step increments, a ‘hide’ query to add display: none to elements when they weren’t needed (like the legs once the pants grew long), and a ‘last’ query with no max-width for the final frame. Those would show up like this:

  .legs
    ...
    frame(        7,     $legs-1)
    frame-range(  8, 12, $legs-5)
    frame-range( 12, 16, $legs-2)
    hide-min(    16)

  .hair
    ...
    frame(       39,     $hair-2)
    frame-range( 40, 42, $hair-3)
    frame(       42,     $hair-2)
    frame-last(  43,     $hair-14)

There’s two different $step increments being set: 23px for laptop screens and 47px for monitor-sized screens. Most desktop users should get the full sequence without having to move to a larger screen.

For tall screens, the artwork gets resized which allowed me to dabble with CSS custom properties. With calc() and custom properties, it was easy to swap in new values depending on the user’s context. I know a transform probably would have worked too, but again, not as fun.

The site also takes advantage of some PostCSS plugins. I have four CSS files compiling from Stylus:

  📂 css
    📄 generated-home-base.css    /* base styles */
    📄 generated-home-large.css   /* for monitor views, 47px step increment */
    📄 generated-home-small.css   /* for laptop views, 23px step increment */
    📄 generated-post.css         /* for PostCSS processing */

Note: If it was possible to use a combination of calc() and custom properties inside the media queries, I could have eliminated the need for separate large.css and small.css files.

PostCSS plugin css_mqpacker takes the large.css and small.css and consolidates repeated media queries. So the many, many media queries would go from something like this:

  @media screen and (min-width: 401px) {
    .hair {
      background-position: -100px 0;
    }
  }
  @media screen and (min-width: 401px) {
    .legs {
      background-position: -200px -150px;
    }
  }

to something like this:

  @media screen and (min-width: 401px) {
    .hair {
      background-position: -100px 0;
    }
    .legs {
      background-position: -200px -150px;
    }
  }

Seems small, but saves a lot of characters across 220 media queries.

Then postcss_import compiles the four files into one glorious home.css.

Any weird hiccups?

I originally had the .lynns-burgers container centered using flexbox. Easy peasy. But when I resized the browser, the artwork would shift left/right slightly every few pixels as it was repositioned. This created a vibrating effect that I did not like one bit.

Using the ol’ fixed position, then translate pattern did the trick. I’m guessing it’s because the calculation is happening on the width of element instead and the browser is never dividing an uneven number of pixels.

  .lynns-burgers {
    position: fixed;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%);
  }

But THEN, the character artwork started doing the vibration thing. Oy. The issue here was position values being calculated as decimals. I needed to make sure each calc() would return a whole pixel value, even as the custom properties changed.

So that would ultimately end up looking something like this:

  /* set custom properties */
  @media screen and (max-height: 900px) {
    :root {
      --mouth-base: 100px;
    }
  }
  @media screen and (min-height: 901px) {
    :root {
      --mouth-base: 150px;
    }
  }

  /* calculation multipliers should be even */
  .mouth
    place(0, $mouth-2,  calc(var(--mouth-base) * 1.14), calc(var(--mouth-base) * .52), -3deg)
    place(1, $mouth-12, calc(var(--mouth-base) * 1.16), calc(var(--mouth-base) * .52),  5deg)
    place(2, $mouth-14, calc(var(--mouth-base) * 1.18), calc(var(--mouth-base) * .50),  5deg)
    ...

That’s it! Sort of.

Looking through the final files and writing it all out makes it seem like so. much. (And there’s a lot I didn’t include here.) But it was definitely a process of discovery. I’m certain there are more efficient ways to do this, but that’s for another day.

The finished site!

An Archive

Oh! I also added an archive featuring every version of this site dating back to 2007. It was super fun to look back through them. I still love every one of them so much. You can view them all in their dated glory here.

Whew, so… why all that?

As usual, I wanted to try some new things and make something that might bring some joy to the web (it’s been a long year). Here’s a good reminder of why I do work like this.

I’ve been spending a lot of time in Illustrator lately. I recently redid some CSS grid and flexbox diagrams for CSSTricks and I’m working on a top secret illustration project with Heroku. You might say I’ve been in an illustratin’ mood.

I’ve never done frame animation like this and it was an insightful introduction. Wow, the patience it requires!

I was also able to gain some experience optimizing my Illustrator workflow, vector artwork, and exported SVGs. I got to dabble with CSS properties (so cool) and with PostCSS (also very cool).

And best of all I had fun with it. I hope you have fun with it too. Until next year!


Back to Thoughts