HTML5 SVG uses CSS3 and Vanilla JavaScript to populate animations

Translation: The First Order

Original: www.smashingmagazine.com/2019/01/htm…


In this article you will learn how Awwwards implements animation. This article introduces the circle element in HTML5 SVG, its stroke property, and how to use CSS variables and animate them with Vanilla JavaScript.

SVG is an XML-based markup language for defining scaled vector graphics. It allows you to draw paths, curves, and shapes through a set of points identified in a 2D plane. You can also animate these paths by adding dynamic properties (stroke, color, thickness, fill, etc.).

Starting in April 2017, the CSS Level 3 Fill and Stroke module began to support setting SVG colors and fill patterns from external stylesheets, rather than setting properties on each element. In this tutorial, we will use simple pure hexadecimal colors, but the fill and stroke properties also support patterns, gradients, and images as values.

Note: When visiting the Awwwards website, you need to set the browser width to 1024px or higher to see the animations better.

  • The demo link
  • The source code

File structure

Let’s start by creating a file in the terminal:

🌹 mkdir note - display 🌹cdNote -display 🌹 touch index.html styles.css scripts.jsCopy the code

This is the initial template for connecting CSS and JS files:

<html lang="en">
<head>
  <meta charset="UTF-8">

  <title>Note Display</title>

  <link rel="stylesheet" href="./styles.css">
</head>
<body>
  <script src="./scripts.js"></script>
</body>
</html>
Copy the code

Each note element contains a list item: li holds the circle, note value, and label.

Figure: Lists item elements and their immediate children:.circle,.percent, and.label

Circle_svg is an SVG element that contains two elements. The first is the path to fill, and the second is used to prepare the animation.

Figure: SVG elements: SVG wrappers and circular labels

Comments are divided into integers and decimals, so they can be set to different font sizes. Label is a simple . Putting all these elements together looks like this:

<li class="note-display">
  <div class="circle">
    <svg width="84" height="84" class="circle__svg">
      <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle>
      <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle>
    </svg>

    <div class="percent">
      <span class="percent__int">0.</span>
      <span class="percent__dec">00</span>
    </div>
  </div>

  <span class="label">Transparent</span>
</li>
Copy the code

The Cx and cy attributes define the x – and Y-axis centers of the circle. The r property defines its radius.

You may have noticed the underscore/dash pattern in class names. This is BEM (Block Element Modifier), which stands for Block, Element, and Modifier respectively. It is a way to make element naming more structured, logical, and semantic.

Recommended reading: What is BEM and why is it needed

To complete the template structure, let’s wrap four list items in unordered list elements:

Figure: The unordered list wrapper has four LI child elements

<ul class="display-container">
  <li class="note-display">
    <div class="circle">
      <svg width="84" height="84" class="circle__svg">
        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle>
        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle>
      </svg>

      <div class="percent">
        <span class="percent__int">0.</span>
        <span class="percent__dec">00</span>
      </div>
    </div>

    <span class="label">Transparent</span>
  </li>

  <li class="note-display">
    <div class="circle">
      <svg width="84" height="84" class="circle__svg">
        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle>
        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle>
      </svg>

      <div class="percent">
        <span class="percent__int">0.</span>
        <span class="percent__dec">00</span>
      </div>
    </div>

    <span class="label">Reasonable</span>
  </li>

  <li class="note-display">
    <div class="circle">
      <svg width="84" height="84" class="circle__svg">
        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle>
        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle>
      </svg>

      <div class="percent">
        <span class="percent__int">0.</span>
        <span class="percent__dec">00</span>
      </div>
    </div>

    <span class="label">Usable</span>
  </li>

  <li class="note-display">
    <div class="circle">
      <svg width="84" height="84" class="circle__svg">
        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle>
        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle>
      </svg>

      <div class="percent">
        <span class="percent__int">0.</span>
        <span class="percent__dec">00</span>
      </div>
    </div>

    <span class="label">Exemplary</span>
  </li>
</ul>
Copy the code

You must first ask yourself what the Transparent, Reasonable, Usable and Exemplary labels mean. As you get more familiar with programming, you’ll realize that writing code isn’t just about making your program work, it’s also about making sure it can be maintained and extended over time. This is only possible if your code is easily modified.

“The TRUE acronym should help you determine if the code you’re writing will adapt to future changes.”

Well, next time ask yourself:

Transparency: Are the consequences of code changes clear? Reasonable: Is the cost benefit worth it? Available: Can I reuse it in unexpected situations? Example: Does it use high quality as an example of future code?

  • Transparent:Is it clear if the code is changing?
  • A) Reasonable B) Reasonable C) Reasonable D) Reasonable: Is the cost benefit worth it?
  • Usable: Can I reuse it in different scenarios?
  • Exemplary (example): Can it be used as a high-quality code template in the future?

Note: Sandi Metz explains TRUE and other principles and how to implement them through design patterns in her book Object-oriented Design Practice Guide: A Description of the Ruby Language. If you haven’t already started researching design patterns, consider putting this book on your desk.

CSS

Let’s import the font and make it work for everything:


@import url('https://fonts.googleapis.com/css?family=Nixie+One|Raleway:200');

* {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}

Copy the code

Box-sizing: Border-box property includes padding and border values to the element’s total width and height, so it’s easier to calculate the scope of the graph.

Note: For a description of *box-sizing*, see “Using CSS Box to Make You Easier” _.

body {
  height: 100vh;
  color: #fff;
  display: flex;
  background: #3E423A;
  font-family: 'Nixie One', cursive;
}

.display-container {
  margin: auto;
  display: flex;
}
Copy the code

By combining the rules shown: flex in body and margin-Auto in.display-container, you can center child elements vertically and horizontally. The.display-container element will also be used as a flex-container; In this way, its children will be placed on the same line along the main axis.

The. Note-display list item will also be a flex-container. Since many of the children are centered, we can do this with the context-content and align-items properties. All Flex-items are vertically and horizontally centered. If you’re not sure what they are, check out the alignment section in the CSS Flexbox Visualization Guide.

.note-display {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 0 25px;
}
Copy the code

Let’s apply strokes to circles by setting ‘stroke-width, stroke-opacity, and stroke-linecap’, which will make the picture move. Next, we add a color to each circle:

.circle__progress {
  fill: none;
  stroke-width: 3;
  stroke-opacity: 0.3;
  stroke-linecap: round;
}

.note-display:nth-child(1) .circle__progress { stroke: #AAFF00; }
.note-display:nth-child(2) .circle__progress { stroke: #FF00AA; }
.note-display:nth-child(3) .circle__progress { stroke: #AA00FF; }
.note-display:nth-child(4) .circle__progress { stroke: #00AAFF; }
Copy the code

In order to absolutely locate percentage elements, you must know exactly what these concepts are. The.circle element should be a reference, so let’s add position: relative to it.

Note: For a more in-depth and intuitive explanation of absolute positioning, see “Understanding CSS Position once and for all.”

Another way to centralize elements is to combine top: 50%, left: 50% and transform: translate(-50%, -50%); Group together to locate the element’s center at its parent’s center.

.circle {
  position: relative;
}

.percent {
  width: 100%;
  top: 50%;
  left: 50%;
  position: absolute;
  font-weight: bold;
  text-align: center;
  line-height: 28px;
  transform: translate(50%, 50%); }.percent__int { font-size: 28px; }
.percent__dec { font-size: 12px; }

.label {
  font-family: 'Raleway', serif;
  font-size: 14px;
  text-transform: uppercase;
  margin-top: 15px;
}
Copy the code

By now, the template should look like this:

Figure: Completed template elements and styles

Fill in the transition

Circular animations can be created with the help of two circular SVG properties: stroke-Dasharray and stroke-dashoffset.

“Stroke-da Harray defines the pattern of dotted line gaps in strokes.”

It may require up to four values:

When it is set to a unique integer (stroke-Dasharray :10), dashes and gaps have the same size; For two values (stroke-Dasharray :10 5), the first is applied to the dash and the second to the gap; The third and fourth forms (stroke-dasharray:10 5 2 and stroke-dasharray:10 5 2 3) will produce various styles of dashed lines and gaps.

Figure: Stroke-dasharray property values

The image on the left shows the stroke-Dasharray property set to 0 to a circumference of 238px.

The second image represents the stroke-Dashoffset property, which cancels out the beginning of the DASH array. It also ranges from 0 to the circumference.

Figure: Stroke-Dasharray and stroke-Dashoffset properties

To create a filling effect, we set stroke-Dasharray to the circumference so that all of its lengths can fill its sprint range without leaving a gap. We will also cancel it out with the same value, which will make it “hidden”. Stroke-dashoffset is then updated with the corresponding caption to fill its stroke based on transition duration.

Property updates will be done in the script via CSS Variables. Let’s declare variables and set properties:

.circle__progress--fill {
  --initialStroke: 0;
  --transitionDuration: 0;
  stroke-opacity: 1;
  stroke-dasharray: var(--initialStroke);
  stroke-dashoffset: var(--initialStroke);
  transition: stroke-dashoffset var(--transitionDuration) ease;
}
Copy the code

In order to set the initial value and updates the variables, let us from the use of the document. Choose all querySelectorAll. Note – the display element. Also set transitionDuration to 900 milliseconds.

We then iterate through the display array, selecting its.circle__progress.circle__progress–fill and extracting the SET of R attributes in the HTML to calculate the perimeter. With this, we can set the initial –dasharray and –dashoffset values.

Animation occurs when the –dashoffset variable is updated by setTimeout:

const displays = document.querySelectorAll('.note-display');
const transitionDuration = 900;

displays.forEach(display= > {
  let progress = display.querySelector('.circle__progress--fill');
  let radius = progress.r.baseVal.value;
  let circumference = 2 * Math.PI * radius;

  progress.style.setProperty('--transitionDuration'.`${transitionDuration}ms`);
  progress.style.setProperty('--initialStroke', circumference);

  setTimeout((a)= > progress.style.strokeDashoffset = 50.100);
});
Copy the code

To transition from the top, you must rotate the.circle__svg element:

.circle__svg {
  transform: rotate(-90deg);
}

Copy the code

Figure: Stroke attribute conversion

Now, let’s calculate the Dashoffset value relative to note. The note value is inserted into each LI item via the data-* attribute. * You can replace it with any name that fits your needs and retrieve it in the metadata set from the element’s dataset: element.dataset.*.

Note: You can get more information about data-* properties on MDN Web Docs.

Our property will be named “data-note” :

<ul class="display-container">
+ <li class="note-display" data-note="7.50">
    <div class="circle">
      <svg width="84" height="84" class="circle__svg">
        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle>
        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle>
      </svg>

      <div class="percent">
        <span class="percent__int">0.</span>
        <span class="percent__dec">00</span>
      </div>
    </div>

    <span class="label">Transparent</span>
  </li>

+ <li class="note-display" data-note="9.27">
    <div class="circle">
      <svg width="84" height="84" class="circle__svg">
        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle>
        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle>
      </svg>

      <div class="percent">
        <span class="percent__int">0.</span>
        <span class="percent__dec">00</span>
      </div>
    </div>

    <span class="label">Reasonable</span>
  </li>

+ <li class="note-display" data-note="6.93">
    <div class="circle">
      <svg width="84" height="84" class="circle__svg">
        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle>
        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle>
      </svg>

      <div class="percent">
        <span class="percent__int">0.</span>
        <span class="percent__dec">00</span>
      </div>
    </div>

    <span class="label">Usable</span>
  </li>

+ <li class="note-display" data-note="8.72">
    <div class="circle">
      <svg width="84" height="84" class="circle__svg">
        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle>
        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle>
      </svg>

      <div class="percent">
        <span class="percent__int">0.</span>
        <span class="percent__dec">00</span>
      </div>
    </div>

    <span class="label">Exemplary</span>
  </li>
</ul>
Copy the code

The parseFloat method converts the string returned by display.dataset. Note to a floating point number. Offset represents the percentage missing when the maximum value is reached. Therefore, for 7.50 note, we would get (10-7.50) / 10 = 0.25, which means that the circumference length should be offset by 25% of its value:

let note = parseFloat(display.dataset.note);
let offset = circumference * (10 - note) / 10;
Copy the code

Update the scripts. Js:

const displays = document.querySelectorAll('.note-display');
const transitionDuration = 900;

displays.forEach(display= > {
  let progress = display.querySelector('.circle__progress--fill');
  let radius = progress.r.baseVal.value;
  let circumference = 2 * Math.PI * radius;
+ let note = parseFloat(display.dataset.note);
+ let offset = circumference * (10 - note) / 10;

  progress.style.setProperty('--initialStroke', circumference);
  progress.style.setProperty('--transitionDuration'.`${transitionDuration}ms`);

+ setTimeout((a)= > progress.style.strokeDashoffset = offset, 100);
});

Copy the code

The sroke property is converted to the note value

Before continuing, let’s extract the stoke transform into its own method:

const displays = document.querySelectorAll('.note-display');
const transitionDuration = 900;

displays.forEach(display= >{-let progress = display.querySelector('.circle__progress--fill');
- let radius = progress.r.baseVal.value;
- let circumference = 2 * Math.PI * radius;
  let note = parseFloat(display.dataset.note);
- let offset = circumference * (10 - note) / 10;

- progress.style.setProperty('--initialStroke', circumference);
- progress.style.setProperty('--transitionDuration'.`${transitionDuration}ms`);

- setTimeout((a)= > progress.style.strokeDashoffset = offset, 100);

+ strokeTransition(display, note);
});

+ function strokeTransition(display, note) {+let progress = display.querySelector('.circle__progress--fill');
+   let radius = progress.r.baseVal.value;
+   let circumference = 2 * Math.PI * radius;
+   let offset = circumference * (10 - note) / 10;

+   progress.style.setProperty('--initialStroke', circumference);
+   progress.style.setProperty('--transitionDuration'.`${transitionDuration}ms`);

+   setTimeout((a)= > progress.style.strokeDashoffset = offset, 100); +}Copy the code

Notice the growth

One more thing is to convert the note from 0.00 to the final note value. The first thing to do is separate integers from small values. You can use the string method split(). They are then converted to numbers and passed as arguments to the increaseNumber() function, which is properly displayed on the corresponding element by marking it with integers and decimals.

const displays = document.querySelectorAll('.note-display');
const transitionDuration = 900;

displays.forEach(display= > {
  let note = parseFloat(display.dataset.note);
+ let [int, dec] = display.dataset.note.split('. ');
+ [int, dec] = [Number(int), Number(dec)];

  strokeTransition(display, note);

+ increaseNumber(display, int, 'int');
+ increaseNumber(display, dec, 'dec');
});
Copy the code

In the increaseNumber() function, whether we choose the.percent__int or.percent__dec element depends on the className and whether the output should contain a decimal point. Next, set transitionDuration to 900 milliseconds. Now, the animation represents numbers from 0 to 7, and the duration must be divided by Note 900/7 = 128.57ms. The result is how long each incremental iteration will take. This means that setInterval will fire every 128.57ms.

With these variables set, you define setInterval. The counter variable is appended to the element as text and incremented with each iteration:

function increaseNumber(display, number, className) {
  let element = display.querySelector(`.percent__${className}`),
      decPoint = className === 'int' ? '. ' : ' ',
      interval = transitionDuration / number,
      counter = 0;

  let increaseInterval = setInterval((a)= > {
    element.textContent = counter + decPoint;
    counter++;
  }, interval);
}
Copy the code

Figure: Count growth

Cool! It does increase the count, but it plays in an infinite loop. When the note reaches the desired value, we also need to clear the setInterval. This can be done using the clearInterval function:

function increaseNumber(display, number, className) {
  let element = display.querySelector(`.percent__${className}`),
      decPoint = className === 'int' ? '. ' : ' ',
      interval = transitionDuration / number,
      counter = 0;

  let increaseInterval = setInterval((a)= >{+if (counter === number) { window.clearInterval(increaseInterval); }

    element.textContent = counter + decPoint;
    counter++;
  }, interval);
}
Copy the code

Photo: Finished

The number is now updated to the note value and cleared using the clearInterval() function.

That’s the end of the tutorial, hope you enjoyed it!

If you want to develop something more interactive, check out the Memory Game Tutorial created using Vanilla JavaScript. It covers basic HTML5, CSS3, and JavaScript concepts such as positioning, perspective, transformation, Flexbox, event handling, timeouts, and triples.

Happy Valentine’s Day 🌹