Hexagon page animations

 from Red Blob Games’s Blog
Blog post: 16 Dec 2024

When I first wrote my hexagon guide[1] in 2013 I used d3.js[2], which has a nice animation system. I had some trouble with CSS transitions in SVG back then, so I was using Javascript transitions using SVG attributes instead of CSS. This looked something like:

d3.select(".grid")
  .transition()
  .attr("transform", "rotate(30)");

That would rotate the grid from flat-topped to pointy-topped, but the text would be rotated too:

Screenshot of a hexagonal grid rotated 30 degrees into 'pointy top' orientation, with the text rotated too
Text rotates with the diagram

The solution was to undo the rotation on the <text> nodes:

d3.select(".grid text")
  .transition()
  .attr("transform", "rotate(-30)");
Screenshot of the same hexagonal grid rotated 30 degrees, with the text rotated back to be upright
Text re-rotated to undo the diagram rotation

When I rewrote the hexagon guide in Vue in 2018[3], I wrote my own animations that set a Javascript value between 0 and 1, and turned that into an angle between 0° and 30°. I then applied that angle to both the grid and the text:

<svg>
  <g class="grid" :transform="rotationTransform">
    <g v-for="hex in grid">
      <text :transform="undoRotationTransform" />
    </g>
  </g>
</svg>

In both of these implementations, I used a Javascript value to change SVG attributes. As an optimization, I limited the animation to diagrams that were visible. Diagrams that weren’t visible would “snap” to the final position.

During the rewrite I was hoping to simplify the implementation a little bit.

SVG2 added a feature in 2016 specifically to solve the problem of rotated text: vector-effect: non-rotation[4]. Caniuse shows it being supported[5], and the MDN page shows it as supported[6]. However when I tested it back in 2018, and again in 2024, it didn’t work anywhere[7]. There’s a Chrome Bug[8] that says the non-rotation value is unimplemented since 2017, and a Firefox bug[9] that says they won’t implement it unless Chrome does (probably because developers won’t use all the features Firefox has implemented that Chrome didn’t). So I can’t use this feature.

I wanted to use CSS transitions[10] back then, but browser features and bugs back then limited what I could do. Since 2018, browsers have adopted lots of new features, and many bugs have been fixed. I decided to try using CSS transitions again.

But why CSS transitions? Partly because it’s easier to program, and partly because it should be faster. With Javascript transitions, I can start from something like this:

<svg>
  <g transform="rotate(0)">
    <text transform="rotate(0)">
    <text transform="rotate(0)">
    <text transform="rotate(0)">
    <text transform="rotate(0)">
    <text transform="rotate(0)">
    <text transform="rotate(0)">

</g>
</svg>

and want to turn it into this:

<svg>
  <g transform="rotate(30)">
    <text transform="rotate(-30)">
    <text transform="rotate(-30)">
    <text transform="rotate(-30)">
    <text transform="rotate(-30)">
    <text transform="rotate(-30)">
    <text transform="rotate(-30)">

</g>
</svg>

During the transition, I have to change all of those numbers every animation frame. As of today, I have 2145 elements ✕ 60frames/sec ✕ 0.5sec = 64,350 html updates to run the full animation. With CSS transitions, I can do this instead:

<svg>
  <g class="rotate-by-0">
    <text class="unrotate">
    <text class="unrotate">
    <text class="unrotate">
    <text class="unrotate">
    <text class="unrotate">
    <text class="unrotate">

</g>
</svg>

and then change it to

<svg>
  <g class="rotate-by-30">
    <text class="unrotate">
    <text class="unrotate">
    <text class="unrotate">
    <text class="unrotate">
    <text class="unrotate">
    <text class="unrotate">

</g>
</svg>

just once, not every frame. And I only have to change the class at the top, not all the individual <text> elements. That means I have 39 html updates to run the full animation … down from 64,350!

First, I needed to make sure CSS transitions would work. I designed a CSS easing function and tested it with an online playground[11]. It worked across the browsers I test on.

I implemented this change in small steps and tested each step across browsers:

  1. Switch from transform attribute to transform style, still controlled via Javascript.
  2. Refactor styles to have the diagram rotate and the text unrotate.
  3. Switch from inline style to class, moving the style rules to the global CSS.
  4. Add CSS transition rule, transition: transform 0.5s; transition-timing-function: cubic-bezier(0.5, -0.2, 0.5, 1.2);.
  5. Some elements couldn’t use CSS transitions so wrote a Javascript approximation to it, along with a test to make sure the CSS and Javascript versions stay in sync.

I did run into one bug, with Safari this time. The animations should all play when I change the CSS class. In Safari, for elements not currently on the screen, it would start the animation when the element is scrolled into view. That’s too late. It should have finished by then. To work around this bug, I used IntersectionObserver to trigger the animation only on visible elements.

I’m glad I was able to simplify the implementation. I hope the animation runs smoother on more devices, but on my computers I wasn’t able to tell.

Email me , or tweet @redblobgames, or comment: