2 June 2023
statistics   web   devlog  

Animating a Latin Square Design with Javascript

Latin Square Design with Javascript Logo

Similar to the previous post, we won’t dive too much into the statistical nature of the Latin Square design, but this devlog will focus on the animation elements of the graphic and their creation. Since CSS animations has a few limitations that we ran into in the last animation for interactivity and synchronization, we’ll be using the Web Animations API this time around. Further, CSS stylesheets are static at compilation time so we’re unable to introduce “randomness” into the animation. For statistical designs, it’s nice to be able to see different randomizations. We could use a random function in Sass, but we’d still have to compile the stylesheets and it will only be random for the first time it’s presented to the reader. I added some interactivity with javascript this time, in which the user can click “rerandomize” to see a different, but appropriately randomized design. Play/pause functionality is also added by clicking on the animation, along with highlighting the main components of the design.

The Latin Square “Sudoku” Design!

The Latin Square design is special because it adjusts for two blocking variables at the same time, and is highly efficient for the number of experimental units it uses. If we’re working with a treatment that has tt levels, then our blocking variables will also have tt levels. Just like in a randomized complete block design, each treatment appears in each row and column exactly once. For 9 levels, this makes it similar to a standard Sudoku puzzle!

Safari, you’re an absolute pain for SVG animations 🥲

There are a number of browsers specific quirks that I’ve encountered working with animation of SVG elements, and it seems the frustration is somewhat common. My primary browser is Arc, which is based in Chromium, so most of the animations should work for chrome and the other chromium based browsers. For cross-browser support, the website caniuse.com has been indispensable for various CSS3 features.

Here are some tips for animating svg elements for cross-browser support, and responsiveness:

  • Wrap your SVG <text> elements with a <g> group tag.

    • I find animating the parent of text elements less likely to fail in most browsers. In particular, Safari animations failed when applied directly to the text element. Targeting the parent element is normally the first thing I try if other animation properties fail as well.
  • Use SVG inline attributes rather than CSS styling (wherever you can).

    • The <text> element again is picky. Although altering the xand y position attributes through styling worked for <rect>, and <circle>, it seems to fail for <text> elements and I’m not sure why. It’s useful to keep in mind that these are different specifications, and although it’s nice when CSS styling works to modify the SVG, it shouldn’t be expected.
  • Avoid filter:.

    • Although it’s supported on most browsers other than IE, I would still have trouble rendering certain elements, and the element sometimes disappeared entirely. Googling other people’s experiences, some people suggested will-change: transform, or using transform3d instead of transform, or filters on parent elements, but you can save yourself from headache if you just avoid it altogether.
  • Always use viewBox combined with preserveAspectRatio.

    • It is much easier to scale the SVG to take advantage of the vector properties, and for responsive design of viewing the animation on different screens. For the case above, since it’s a Latin Square design, it’s important that the animation actually looks like a square, so the preserveAspectRatio is critical.
  • Always specify units if styling SVG through stylesheets.

    • In my testing, safari and chromium seemed to not need the units in stylesheets when modifying svg elements, but firefox did need them in order for the properties to be honored. Specifying units when you can is a good habit.

SVG Attributes vs CSS Styles

Let’s look at 3 svg text elements:

  1. One with no styling.
  2. Styled with the style= attribute
  3. Setting attributes directly, but styled the same as 2.
html
<svg viewBox="-10 -15 130 40">
  <!-- Background and origin code omitted -->

  <text>No Styling Text</text>

  <text x="10" y="12.5"
       fill="red" 
       font-size="5"
       textLength="80"
       dominant-baseline="hanging"
       rotate="5 10 15 20 30 40 0">
    Inline Attribute Text
  </text>

  <text style="x:10; y:12.5;
               fill: red;
               font-size:5px;
               textLength:80px;
               dominant-baseline:hanging;
               rotate:5 10 15 20 25 30 35 40;">
    CSS Style Text
  </text>
</svg>
OriginNo Styling TextInline Attribute TextCSS Style Text

Let’s start by discussing the “No Styling Text”. The default text-anchor: start and dominant-baseline: auto places our text as if we’re writing on the line y=0y=0, starting at the origin. The origin is marked in blue for clarity. (We would barely be able to see this text if we hadn’t adjusted the viewBox to include negative values. Thus, when using a standard “0 0 200 100” value, if you can’t see your text element, it might be outside your svg window!). Our default text size is ~19px, which takes up most of our 50 pixel window, and the text is the standard black.

So we’ve tried to style the text the same way through CSS styles and SVG attributes, but we notice that only some of the styling attributes through CSS were applied. What’s happening here? font-size, dominant-baseline, fill all worked, but the rest did not. Looking at the text modified by attributes, every modification worked, which is why I prefer this method. The attributes that can be modified through CSS are known as SVG Presentation Attributes. For the detailed specification of these special attributes, see the W3C Specification.

Text SVGCSSfilldominant-baselinestrokeopacitystroke-widthtransformfont-sizeyxrotaterotatepseudo-classes
Venn diagram shows attributes of <text> that are modifiable through CSS styles, and exclusive properties of each as well. The list is (very) incomplete, but shows common styles and attributes for the elements. For a full list, see the W3C specification or MDN Docs. Pseudo-classes are mentioned specifically because :hover effects and :active classes can’t be set inline.

It’s worth noting that fill and dominant-baseline are not really CSS properties that are used outside the context of SVG, but font-size clearly is. In fact, they only make sense if the target of the styles are SVG elements. Further, the attribute rotate exist in both SVG and CSS, but they mean different things. In CSS, rotate is roughly shorthand for transform: rotate(), but in SVG, the rotate attribute for text will take a list of numbers and rotate each letter in the order of the list provided — so it doesn’t make sense to try and modify the SVG attribute through CSS styles. (There’s also another rotate attribute for animateMotion which describes rotation of an element as it moves along a path.)

The partial overlap between CSS property and SVG attribute names is confusing enough, but even more frustrating, is that the attributes modifiable through styles differ by element. For example, styles for x and y do not work for text, but it does work for rect, and for circle (with cx and cy). Check out how these two rectangles differ when styled by SVG attribute or CSS.

html
<svg viewBox="0 0 100 30">
<!-- text, background and origin code omitted  -->

 <!-- elements modified by attribute -->
 <rect x="20" y="5"
       width="70" height="10"
       fill="red" rotate="10deg"/>
 <!-- rotate="10deg" doesn't work here,
      but transform="rotate(10)" will -->

 <!-- element modified by style -->
 <rect style="x:20px; y:5px;
              width:70px; height:10px;
              fill:red; rotate:10deg;"/>
</svg>
OriginBy attributeBy style

Notice we did not need to specify units for the SVG attributes. Above, when we were working with text, it seems to also work consistently using the styles without units because they matched up with SVG attributes. However, specifically with rectangles and Firefox, the styling without units will fail. This is just one of the quirks of SVG and browsers. Hence, it’s normally a good habit to use the px unit when working with SVG elements through styles.

Here, the css styling works for all attributes except for pathLength, and the only one that didn’t work for the svg attributes is rotate=. Again, we emphasize that styling x and y works for rect but not for text. Recall that pathLength=5 sets the length of the perimeter around the rectangle. When we call stroke-dasharray=5, the same length as pathLength, this declares that a single dash should take up 5 units of length, and thus go around the entire perimeter of the rectangle. We end up with a solid border. The “natural” length of the perimeter is 2×width+2×height=2(70)+2(10)=1602 \times \text{width} + 2 \times \text{height} = 2(70) + 2(10) = 160, which is why the rectangle styled with CSS shows a dashed stroke. The rotation didn’t work by attribute since there is no attribute named “rotate” for <rect>. The rotation for svg is also around the default origin, not the center of the element. You will need to move the origin to the center of the rectangle to get the more intuitive rotation effect. This gives us a slightly different venn diagram of attributes that can be modified through styles:

Rect SVGCSSfillheightstrokeopacitystroke-widthtransformwidthyxpathLengthrotatepseudo-classes
Coloring emphasizes differences with SVG Text venn diagram above. Green attributes are ones SVG <rect> has that SVG <text> does not. Yellow attributes show ones<rect> and <text> both have, but are shifted to a different part of the venn diagram.

It is obviously more convenient to work with stylesheets and classes for managing the look of svg elements, so it’s useful to know which svg attributes can be modified through styles. If we use attributes, we’d be retyping several attributes for similarly styled elements, breaking the DRY principle. For the attributes that don’t work in styles, there are normally workarounds. For positioning, the workaround is to use transform: translate(dx, dy). This is also useful for animations since transform is an animatable property. However, working with the transform property has it’s own pitfalls to beware of for cross-browser support. For example, using the transform svg attribute works in all browsers, but using the transform css property style, does not work in IE. The reference point for rotations and scaling can also vary by browser.

The inconsistency of animation properties of SVG point to using more established animation libraries like GSAP that are based in javascript, so that the animation just “works”, and we don’t need to manually fill in different queries or code browser specific support depending on where we are viewing the animation. That’ll be the next step up in complexity for future animations.

Web Animations API, better synchronization

True synchronization in CSS animations simply doesn’t exist. The model for having animations sync is to try and coordinate the start time of the animations and have them run for the same amount of time. If you want to “hook into” an already running animation, you need to be able to query the state of the running animation. Most of the animation tools prior to the development of Web Animation Tools, like setInterval and requestAnimationFrame rely on parallel timing of animations, and on the browser’s refresh rate, which is normally 60Hz. 60Hz equates to 16 milliseconds of time for your logic to execute and have the browser recalculate layout and repainting. However, if the browser is busy, or the animation is complex, the refresh rate can drop. This is why we see animations stuttering or lagging of frames. The Web Animations API (WAAPI) synchronizes each animation to Document.timeline, so that we can set the timeline of each animation in relation to a single timeline. This means we can set the starting time of a new animation to the same start time as a previously running animation, and get them to sync. In my opinion, the syncing capabilities and performance enhancements are the most compelling additions of this new web standard.

Web Animations is essentially a common model for animation that underlies CSS and SVG. It covers the common features of both plus a few only found in one or the other.
— Brian Birtles

Here are some resources and links to get started with using and understanding WAAPI:

First let’s look at a minimal example of a moving red square:

html
<div id="waapi-example-box"
     style="height:20px;
            width:20px;
            background-color: red;">
</div> 
js
window.onload = function() {
    const waapiBox = document.querySelector("#waapi-example-box");

    const keyframes =  [
        {transform: "translate(0px, 0px)"},    // from
        {transform: "translate(200px, 0px)"}]; // to 

    const animOptions = {
        duration: 1000,         // 1 second
        iterations: Infinity,   // loop infinitely
        direction: "alternate", // bounce back and forth
        easing: "ease-in-out"};

    waapiBox.animate(keyframes, animOptions)
}

Element.animate takes two main arguments, a keyframe array with a information about the properties and values the object should have. The “keyframes” element is very similar to the @keyframes usage in CSS, in that the programming is “declarative” and the program will interpolate what needs to happen in between to ensure we get from point A to point B. The second argument is an options object, which is similar to the animation property in CSS. The options object is where we can specify the duration, timing function, and other properties of the animation. The names for the animation options are slightly different, but they encapsulate mostly the same options. duration in this case must be specified in milliseconds.

The code used in the Latin Square design animation is not all that different. There is some additional javascript code to figure out positioning depending on which label is being animated, and we also use the offset key to specify the timing of the keyframes. Finally, we use a forEach call to start the animation for each row label.

javascript
/* positioning dictionary */
const rowLabels = {
      "row-label1" : {xPos: x1, yPos: y0},
      "row-label2" : {xPos: x1, yPos: y1},
      "row-label3" : {xPos: x1, yPos: y2}
    }

// the animation options
const rowExplodeOptions = {
   duration: 6000, // 6 second animation loop
   delay: 0, // start immediately
   iterations: Infinity, // loop infinitely
   fill: "forwards", // maintain the styling of the last keyframe
   direction: "normal" // play in the order of keyframes
}

let rowAnim;
Object.entries(rowLabels).forEach(([label, pos], i) => {
   let xlabel = pos.xPos + blockShort * .5;
   let ylabel = pos.yPos + blockShort * .5;

// the keyframes of the animation
   let labelPosition = [
      // start from exploded position
      // `transform-origin: center` is in stylesheet so scale is relative to middle
      {transform: `scale(1.1, 1.1)
                   translate(${xlabel}px,
                             ${ylabel - (i - 1) * -5}px)`,
       opacity: "100%"},

      // hold frame for .6 seconds
      {transform: `scale(1.1, 1.1)
                   translate(${xlabel}px,
                             ${ylabel - (i - 1) * -5}px)`,
       opacity: "100%",
       offset: .1},

       // scale down the row blocks into position
      {transform: `translate(${xlabel}px, ${ylabel}px)`,
       opacity: "100%",
       offset: .15},

      // fade out by transition opacity
      {transform: `translate(${xlabel}px, ${ylabel}px)`,
       opacity: "0%",
       offset: .2},

      // hold the frame for the rest of the loop
      {transform: `translate(${xlabel}px, ${ylabel}px)`,
       opacity: "0%",
       offset: 1},
   ]

   // start the animation
   rowAnim = document.querySelector(`g.${label}`)
                     .animate(labelPosition, rowExplodeOptions)
})

As you can see, the transform feature is very powerful for transitions as it allows for multiple transformations applied, scaling and translating. The offset key is crucial to the timing of the animation, since we’ve set the entire animation option to be a loop of 6 seconds, we can use the offset as a value between 0-1 to specify what portion of time the animation should be. Conversely, we can also use the delay option to specify when the animation should start. But the delay does not get repeated if you want the animation to loop.

As I mentioned, synchronization is one of the main benefits of using WAAPI. If there are already running animations, and you want to start another animation with synchronization without interrupting the existing animations, the solution with WAAPI is quite straightforward.

Let’s add some more moving boxes and see how we can synchronize them.

js
/* Vue Boilerplate removed */
function add() {

   // create and append the box
   const waapiParent = document.querySelector("#parent")
   let box = document.createElement("div")
   box.classList.add("waapi-box")
   waapiParent.appendChild(box)

   // create animation, with same keyframes/animOptions as before
   let boxAnim = box.animate(keyframes, animOptions);

   // The synchronization step, set same starting time
   const firstBox = waapiParent.firstChild.getAnimations()[0]
   boxAnim.startTime = firstBox.startTime
}

The core logic is to append object to the DOM, call the Element.animate(), and query the animation already running to set properties of the new animation. In this case, we’re setting the startTime of the new animation to the startTime of the first animation running. Hence, we’re saying “animate this new object as if it had started at the same time as the first one”. Since the animation objects are operating on the same document timeline, the browser will be able to calculate the positioning of the new object and display it in sync with the first.

Some other neat features of WAAPI, is that we can call getAnimations() on either the Document we get an array of all the currently running animations on the page. We can also call getAnimations() on individual elements as we did above, as we can have multiple animation objects attached to the same element with different timings. This function is useful for pausing or starting all animations on the the page. For the Latin Square Design, that’s the functionality implemented when clicking on the design.

js
/* Vue boilerplate removed */

function toggleAnimations() {
   document.getAnimations().forEach((animation) => {
      animation.playState == "paused" ? 
         animation.play() : 
         animation.pause()
   })
   this.animPaused = !this.animPaused
}

Finally, it’s neat that you can attach multiple animation triggers onto the same element, and specify how they can be interact with one another. Here we can attach many instances of Animation depending on which button you press, including the same one! The effect will accumulate on top of previous animations. For example, hitting the rotate button multiple times will cause the rotation to speed up, or multiple scaling transformations will cause the element to grow larger and larger.

js
/* Vue boilerplate removed, js modified.*/

const rotateKey = {
  transform: "rotate(0deg)", transform: "rotate(360deg)"
}

const rotateOptions = {
  id: "rotate",
  duration: 2800,
  iterations: 1,
  easing: "ease-in-out",
  composite: "add"       // stack animation effects
}

let waapiBox = document.querySelector("#waapiBox")
let boxAnimations = []

function rotate() {
   let rotateAnim = waapiBox.animate(rotateKey, rotateOptions);

   boxAnimations.push(rotateAnim.id)
   // event handler after animation is finished
   rotateAnim.onfinish = () => {
      let index = boxAnimations.indexOf(rotateAnim.id)
      if (index > -1 ) {
         boxAnimations.splice(index, 1)
      }
   }
},

The program is modified slightly from the Vue component so that the javascript logic is more clear. We call Element.animate with our keyframe options, which includes composite: "add". This allows us to animate the same transform property, and specifies how they should composite with one another. The details of other compositing effects can be found on the MDN docs. For compositing of color, you might have noticed that when the box is yellow, the color does not change when adding more color animations. That’s because we’re adding green to the red, which is why we see yellow at a saturation. Since the range is from 0-255, despite adding “more green” with more button presses, the yellow is already saturated with rgb(255, 255, 0).

Here the animation will also emit an event finish, and we can specify an event handler of what to do when the animation finishes. In this case, we’re simply removing the name of the animation from the list that’s shown.

Conclusion

There are plenty of cautions to take when creating animations, especially when working with SVG elements. I’ve learned that the intersection of SVG and CSS is not as crisp, but still remarkable how much overlap there is. There are a few tricks that I routinely try to get consistent results, and perhaps as the browser implementations change, I’ll need to update my approach.

The Web Animations API provides a powerful, native way of controlling animations with a similar philosophy as CSS animations. I think the API shines when coordinating many animations, and hooking into already running animations. For our Latin Square design animation, we can provide just a few more interactive elements for the user, but there are many more possibilities for future animations.