Our Writing

Building an animated SVG logo with animejs.

Jozef

Jozef /

This is the second part of our tutorial series, where we create a real-time game using supabase and Vue. In this part, we will learn how to create a fancy cyberpunk-style animated logo using SVGs and anime.js. If you haven't already, check out the first post in the series, so you know what we are making!

Dodgy cyberpunk style neon goodness

Here's a little vid of what we will be making. As you can see, we have a cool-looking logo for our game. It has a fancy build-in animation and then a secondary idle animation where the letters flicker to give it that dodgy neon sign effect.

Designing an "animatable" logo in Figma

Before we dive into the code, a few considerations need to be considered when designing a logo like this in Figma that make it much easier to animate once you have exported it.

Use paths where possible.

SVG paths give you more animation options than using a combination of rectangles.

When we originally designed the logo, we used a combination of rectangles to create the orange arrow and the blue lines above the logo. However, we realised this was a bad idea when we thought about how the logo should animate.

If we use paths rather than rectangles, we can use stroke-dashoffset to create a "line drawing" effect where the path is gradually "drawn" from one end to the other. Unfortunately, we can't do this with different SVG element types.

This means you will need to pick up the pen tool in Figma to recreate the shape as a single path using stroke to fill in the colour.

Using path makes it possible to animate a "draw-in" effect

Export text as individual layers in Figma

By default, when you write text in Figma and export it as an SVG, you will be presented with a single path element with all the letters combined. Of course, this is usually fine, but it means you are stuck if you want to animate the letters individually.

Unfortunately, Figma doesn't make this as easy as it should be. There's no native way to take a text layer and split it into multiple new layers for each letter. You could create a new layer manually for each letter, but then you have to space them manually, and no one wants to do that!

Luckily we found a Figma hack that makes this manageable.

  1. Flatten the text layer.
  2. Export only the text layer as an SVG.
  3. Import the SVG you just created back into Figma and use it to replace the original text layer.
  4. For some reason, the SVG you imported will have multiple layers grouped by letters! How handy!

Name layers sensibly and export with IDs

When you export your logo as an SVG, it will be a big file with many elements. To help us animate each layer, it's helpful to know what each SVG element is. To do this, you should give each layer in Figma a sensible name and select the "Include "id" attribute" option in the SVG export options.

Instead of randomly generated IDs, each layer will have an ID, including your layer name, making it much easier to know what you are looking at.

For example:

HTML
<g id="thick-line-1" filter="url(#filter4_d_802_746)">
    <path d="M775.5 29H559" stroke="#02FFFF" stroke-width="10"/>
</g>
<g id="thick-line-2" filter="url(#filter5_d_802_746)">
    <path d="M334.5 29H118" stroke="#02FFFF" stroke-width="10"/>
</g>
    <path id="thin-line" d="M808 70.5V29H118.5" stroke="#02FFFF" stroke-opacity="0.32" stroke-width="4"/>
</g>

Turning your SVG logo into a Vue component

Okay, so now that we've talked enough about Figma, it's time to take our logo and bring it to life as a Vue component.

First things first, export the logo as an SVG from Figma. Once you have the .svg file, open it in VSCode to see its markup. We are now going to plop that markup into a new Vue component in src/components/Logo.vue; simply dump all the SVG markup into the template tag. Because we have ESLint and Prettier setup, it should be auto-formatted nicely when you save too! How handy! It should look something like this once you are done:

src/components/Logo.vue
HTML

<script setup lang="ts">

</script>

<template>
  <svg width="950" height="182" viewBox="0 0 950 182" fill="none" xmlns="http://www.w3.org/2000/svg">
    <g id="Logo">
      <g id="text" clip-path="url(#clip0_4_39)">
        <g id="letters" filter="url(#filter0_d_4_39)">
          <path id="letter" d="M147.274 124V74.932H122.74V61.36H185.38V74.932H160.846V124H147.274Z" fill="white" />
          <path id="letter_2" d="M211.462 124V61.36H224.773V124H211.462Z" fill="white" />
          <path
            id="letter_3"
            d="M266.869 124C264.491 124 262.316 123.42 260.344 122.26C258.43 121.1 256.893 119.563 255.733 117.649C254.573 115.677 253.993 113.502 253.993 111.124V74.236C253.993 71.858 254.573 69.712 255.733 67.798C256.893 65.826 258.43 64.26 260.344 63.1C262.316 61.94 264.491 61.36 266.869 61.36H316.459V74.932H270.349C269.421 74.932 268.696 75.164 268.174 75.628C267.71 76.092 267.478 76.817 267.478 77.803V107.557C267.478 108.485 267.71 109.21 268.174 109.732C268.696 110.196 269.421 110.428 270.349 110.428H316.459V124H266.869Z"
            fill="white"
          />
          <path id="letter_4" d="M368.682 124V74.932H344.148V61.36H406.788V74.932H382.254V124H368.682Z" fill="white" />
          <path
            id="letter_5"
            d="M435.219 124V74.236C435.219 71.858 435.799 69.712 436.959 67.798C438.119 65.826 439.685 64.26 441.657 63.1C443.629 61.94 445.775 61.36 448.095 61.36H484.896C487.274 61.36 489.42 61.94 491.334 63.1C493.306 64.26 494.872 65.826 496.032 67.798C497.25 69.712 497.859 71.858 497.859 74.236V124H484.2V103.816H448.704V124H435.219ZM448.704 90.244H484.2V74.932H448.704V90.244Z"
            fill="white"
          />
          <path
            id="letter_6"
            d="M542.397 124C540.019 124 537.844 123.42 535.872 122.26C533.958 121.1 532.421 119.563 531.261 117.649C530.101 115.677 529.521 113.502 529.521 111.124V74.236C529.521 71.858 530.101 69.712 531.261 67.798C532.421 65.826 533.958 64.26 535.872 63.1C537.844 61.94 540.019 61.36 542.397 61.36H591.987V74.932H545.877C544.949 74.932 544.224 75.164 543.702 75.628C543.238 76.092 543.006 76.817 543.006 77.803V107.557C543.006 108.485 543.238 109.21 543.702 109.732C544.224 110.196 544.949 110.428 545.877 110.428H591.987V124H542.397Z"
            fill="white"
          />
          <path
            id="letter_7"
            d="M635.685 124C633.365 124 631.219 123.42 629.247 122.26C627.275 121.1 625.709 119.534 624.549 117.562C623.389 115.59 622.809 113.444 622.809 111.124V74.236C622.809 71.858 623.389 69.712 624.549 67.798C625.709 65.826 627.275 64.26 629.247 63.1C631.219 61.94 633.365 61.36 635.685 61.36H672.486C674.864 61.36 677.01 61.94 678.924 63.1C680.896 64.26 682.462 65.826 683.622 67.798C684.84 69.712 685.448 71.858 685.448 74.236V80.152H671.789V74.932H636.294V110.428H671.789V101.032H658.218V87.46H685.448V111.124C685.448 113.444 684.84 115.59 683.622 117.562C682.462 119.534 680.896 121.1 678.924 122.26C677.01 123.42 674.864 124 672.486 124H635.685Z"
            fill="white"
          />
          <path
            id="letter_8"
            d="M729.477 124C727.157 124 725.011 123.42 723.039 122.26C721.067 121.1 719.501 119.534 718.341 117.562C717.181 115.59 716.601 113.444 716.601 111.124V74.236C716.601 71.858 717.181 69.712 718.341 67.798C719.501 65.826 721.067 64.26 723.039 63.1C725.011 61.94 727.157 61.36 729.477 61.36H766.365C768.685 61.36 770.802 61.94 772.716 63.1C774.688 64.26 776.254 65.826 777.414 67.798C778.632 69.712 779.241 71.858 779.241 74.236V111.124C779.241 113.444 778.632 115.59 777.414 117.562C776.254 119.534 774.688 121.1 772.716 122.26C770.802 123.42 768.685 124 766.365 124H729.477ZM730.086 110.428H765.582V74.932H730.086V110.428Z"
            fill="white"
          />
        </g>
      </g>
      <rect id="logo-end-1" x="96" y="23" width="10" height="133" fill="#FF7615" fill-opacity="0.27" />
      <rect id="logo-end-2" x="69" y="38" width="10" height="103" fill="#FF7615" fill-opacity="0.27" />
      <rect id="logo-end-3" x="42" y="62" width="10" height="55" fill="#FF7615" fill-opacity="0.27" />
      <g id="logo-end-dot" filter="url(#filter1_d_4_39)">
        <rect x="12" y="81" width="16" height="16" fill="#02FFFF" />
      </g>
      <g id="arrow" filter="url(#filter2_d_4_39)">
        <rect
          id="arrow-tip-top"
          x="833"
          y="64.598"
          width="56"
          height="10"
          transform="rotate(-45 833 64.598)"
          fill="#FF7615"
        />
        <rect
          id="arrow-tip-bottom"
          x="840.071"
          y="57.4731"
          width="56"
          height="10"
          transform="rotate(45 840.071 57.4731)"
          fill="#FF7615"
        />
        <path id="arrow-path" d="M118 151.5H919.5V65H846.5" stroke="#FF7615" stroke-width="10" />
      </g>
      <g id="blue-line">
        <g id="square-1" filter="url(#filter3_d_4_39)">
          <rect x="800" y="56" width="16" height="16" fill="#02FFFF" />
        </g>
        <g id="thick-line-1" filter="url(#filter4_d_4_39)">
          <path d="M775.5 29H559" stroke="#02FFFF" stroke-width="10" />
        </g>
        <g id="thick-line-2" filter="url(#filter5_d_4_39)">
          <path d="M334.5 29H118" stroke="#02FFFF" stroke-width="10" />
        </g>
        <path id="thin-line" d="M808 70.5V29H118.5" stroke="#02FFFF" stroke-opacity="0.32" stroke-width="4" />
      </g>
    </g>
    <defs>
      <filter
        id="filter0_d_4_39"
        x="97.74"
        y="36.36"
        width="706.501"
        height="112.64"
        filterUnits="userSpaceOnUse"
        color-interpolation-filters="sRGB"
      >
        <feFlood flood-opacity="0" result="BackgroundImageFix" />
        <feColorMatrix
          in="SourceAlpha"
          type="matrix"
          values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
          result="hardAlpha"
        />
        <feOffset />
        <feGaussianBlur stdDeviation="12.5" />
        <feComposite in2="hardAlpha" operator="out" />
        <feColorMatrix type="matrix" values="0 0 0 0 0.992157 0 0 0 0 0.466667 0 0 0 0 0.0862745 0 0 0 1 0" />
        <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4_39" />
        <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4_39" result="shape" />
      </filter>
      <filter
        id="filter1_d_4_39"
        x="0"
        y="69"
        width="40"
        height="40"
        filterUnits="userSpaceOnUse"
        color-interpolation-filters="sRGB"
      >
        <feFlood flood-opacity="0" result="BackgroundImageFix" />
        <feColorMatrix
          in="SourceAlpha"
          type="matrix"
          values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
          result="hardAlpha"
        />
        <feOffset />
        <feGaussianBlur stdDeviation="6" />
        <feComposite in2="hardAlpha" operator="out" />
        <feColorMatrix type="matrix" values="0 0 0 0 0.00784314 0 0 0 0 1 0 0 0 0 1 0 0 0 0.84 0" />
        <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4_39" />
        <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4_39" result="shape" />
      </filter>
      <filter
        id="filter2_d_4_39"
        x="93"
        y="0"
        width="856.5"
        height="181.5"
        filterUnits="userSpaceOnUse"
        color-interpolation-filters="sRGB"
      >
        <feFlood flood-opacity="0" result="BackgroundImageFix" />
        <feColorMatrix
          in="SourceAlpha"
          type="matrix"
          values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
          result="hardAlpha"
        />
        <feOffset />
        <feGaussianBlur stdDeviation="12.5" />
        <feComposite in2="hardAlpha" operator="out" />
        <feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.462745 0 0 0 0 0.0823529 0 0 0 1 0" />
        <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4_39" />
        <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4_39" result="shape" />
      </filter>
      <filter
        id="filter3_d_4_39"
        x="788"
        y="44"
        width="40"
        height="40"
        filterUnits="userSpaceOnUse"
        color-interpolation-filters="sRGB"
      >
        <feFlood flood-opacity="0" result="BackgroundImageFix" />
        <feColorMatrix
          in="SourceAlpha"
          type="matrix"
          values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
          result="hardAlpha"
        />
        <feOffset />
        <feGaussianBlur stdDeviation="6" />
        <feComposite in2="hardAlpha" operator="out" />
        <feColorMatrix type="matrix" values="0 0 0 0 0.00784314 0 0 0 0 1 0 0 0 0 1 0 0 0 0.84 0" />
        <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4_39" />
        <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4_39" result="shape" />
      </filter>
      <filter
        id="filter4_d_4_39"
        x="551"
        y="16"
        width="232.5"
        height="26"
        filterUnits="userSpaceOnUse"
        color-interpolation-filters="sRGB"
      >
        <feFlood flood-opacity="0" result="BackgroundImageFix" />
        <feColorMatrix
          in="SourceAlpha"
          type="matrix"
          values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
          result="hardAlpha"
        />
        <feOffset />
        <feGaussianBlur stdDeviation="4" />
        <feComposite in2="hardAlpha" operator="out" />
        <feColorMatrix type="matrix" values="0 0 0 0 0.00784314 0 0 0 0 1 0 0 0 0 1 0 0 0 0.84 0" />
        <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4_39" />
        <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4_39" result="shape" />
      </filter>
      <filter
        id="filter5_d_4_39"
        x="110"
        y="16"
        width="232.5"
        height="26"
        filterUnits="userSpaceOnUse"
        color-interpolation-filters="sRGB"
      >
        <feFlood flood-opacity="0" result="BackgroundImageFix" />
        <feColorMatrix
          in="SourceAlpha"
          type="matrix"
          values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
          result="hardAlpha"
        />
        <feOffset />
        <feGaussianBlur stdDeviation="4" />
        <feComposite in2="hardAlpha" operator="out" />
        <feColorMatrix type="matrix" values="0 0 0 0 0.00784314 0 0 0 0 1 0 0 0 0 1 0 0 0 0.84 0" />
        <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4_39" />
        <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4_39" result="shape" />
      </filter>
      <clipPath id="clip0_4_39">
        <rect width="708" height="113" fill="white" transform="translate(97 36)" />
      </clipPath>
    </defs>
  </svg>
</template>

<style scoped>

</style>

Now that we have our Logo component, let's temporarily add it to our page so we can see what we are dealing with.

Update src/pages/PageHome.vue to the following:

src/pages/PageHome.vue
HTML
<script lang="ts" setup>
import Logo from '../components/Logo.vue';
</script>

<template>
  <div>
    <Logo />
  </div>
</template>

<style scoped></style> 

and you should see our wonderful logo appearing on the page, although huge and off-centre!

To fix the logo sizing, we can head back to logo.vue and delete the width and height attributes on the root SVG element. This will allow the SVG to adjust its size to its container.

src/components/Logo.vue
HTML
<!-- Logo.vue -->
<svg viewBox="0 0 950 182" fill="none" xmlns="http://www.w3.org/2000/svg">
...

Finally, in the designs, to really push home that dodgy neon sign story, the eagle-eyed amongst you may have noticed the logo is always a bit wonky. We can do this easily by adding a Tailwind rotate class:

src/components/Logo.vue
HTML
<svg class="rotate-3" viewBox="0 0 950 182" fill="none" xmlns="http://www.w3.org/2000/svg">

Lovely! Our static logo looks good and is in a nice reusable component we can splash around all over the place. It's time to get animating!

Adding animation to the SVG logo with Anime.js

While you can get a long way with CSS animations, you will still need to reach for a library for more complex animations. Luckily we are spoilt for choice, and we have excellent options like GSAP, Motion One and our favourite Anime.js.

Why do we like Anime.js so much? Well, it is really powerful and can animate essentially anything you can think of, but it remains a tiny bundle size of 6.9kb minified compared to GSAP's 26.13kb.

You could achieve the same effect with most animation libraries, but we like keeping our projects lean, so the small bundle size is an excellent plus for us.

Anyway, let us get Anime.js installed:

BASH
pnpm i animejs --save

and let us get it's types installed too

BASH
pnpm i @types/animejs --save-dev

Now that we have anime.js installed let's prepare to use in Logo.vue

We need to import anime.js and the Vue onMounted hook to trigger animations when the logo is mounted.

src/components/Logo.vue
HTML
<script setup lang="ts">
import anime from 'animejs/lib/anime.es.js';
import { onMounted } from 'vue';
</script>

Creating the build-in animation

When the logo first appears, we want it to "build in" where each part of it pleasingly reveals itself!

Let's start by creating a function called animateIn and then calling it in the onMounted hook.

You might be wondering why we are making a dedicated function and not just doing it all in the onMounted hook, but all will become clear in a bit.

src/components/Logo.vue
HTML
// src/components/Logo.vue
<script setup lang="ts">
import anime from 'animejs/lib/anime.es.js';
import { onMounted } from 'vue';

function animateIn() {
    console.log('Animate in');
}

onMounted(() => {
    animateIn();
});
</script>

Animating the letters

First, let's create an animation for each of the letters. If you followed the Figma advice earlier, you should see that each letter for "TICTACGO" has its own path. This will allow us to create a nice staggered animation where the animation for each letter is slightly delayed.

Add the following to the animateIn function:

src/components/Logo.vue
JAVASCRIPT
function animateIn() {
	console.log('Animate in');

	anime({
	    targets: '#letters path',
	    easing: 'easeOutCubic',
	    delay(el: any, i: number) {
	        return i * 100;
	    },
	    scale: [0, 1],
	    opacity: [0, 1],
	    duration: 300,
	});
}

Here we create a new anime.js animation targeting all letters in the letters group. We have set up animations for their scale and opacity to go from 0 to 1, and by setting the delay using a function, we incrementally delay the animation of each letter by 100ms.

You'll end up with something like this:

It's pretty cool but different from what we were going for! The letters fly in from the left-hand side rather than fading in and scaling in place. This is because, by default, transforms for SVGs are performed relative to the parent SVG rather than the individual element. Luckily we can adjust this using the CSS transform-box property. Here's a tweet we made explaining it a while ago:

With that in mind, add the following to the Logo.vue style block:

src/components/Logo.vue
HTML
<style scoped>
#letters path {
  transform-box: fill-box;
  transform-origin: center;
}
</style>

Now your animation should look like this! Better huh?

Animating the orange arrow

Now it's time to begin animating some of the other elements. Let's start with the orange arrow by adding the following to the animateIn function:

src/components/Logo.vue
JAVASCRIPT
  anime({
    targets: '#arrow-path',
    strokeDashoffset: [anime.setDashoffset, 0],
    easing: 'easeOutCubic',
    duration: 800,
    delay: 200,
  });

  anime({
    targets: ['#arrow-tip-top', '#arrow-tip-bottom'],
    opacity: [0, 1],
    easing: 'easeOutCubic',
    duration: 300,
    delay: 900,
  });

Here we create two new anime.js animations. The first targets the main orange line path and creates a "draw in" effect by animating its stroke-dashoffset property. Here we use the anime.setDashoffset helper to return the exact length of the path so we can go from having a completely blank path to a completely filled in path. If you want to learn more about this technique, check out this great CSS Tricks Article: https://css-tricks.com/svg-line-animation-works/

The second animation targets the arrow tips of the orange arrow. These are separate from the main path element, so they needed to be animated in after the path was fully drawn. We use the delay property to make sure they appear at the right time. When creating animations like this, you will spend much of your time tweaking timing values to make things look just right!

Here's the result:

Animating the top blue lines

It will be cool if the blue lines above the main logo text draw in next, following on from the orange arrow.

The blue line consists of multiple parts that need to be animated one after the other. Anime.js provides a timeline function that can make this sort of animation easier by chaining different animations onto each other.

Add the following to your animateIn function:

src/components/Logo.vue
JAVASCRIPT
anime
    .timeline({
      duration: 300,
      easing: 'linear',
    })
    .add({
      targets: '#blue-line #square-1',
      opacity: [0, 1],
      delay: 600,
    })
    .add({
      targets: '#blue-line #thin-line',
      strokeDashoffset: [anime.setDashoffset, 0],
      easing: 'easeOutCubic',
      duration: 600,
    })
    .add(
      {
        targets: '#blue-line #thick-line-1 path',
        strokeDashoffset: [anime.setDashoffset, 0],
        easing: 'easeOutCubic',
      },
      '-=500'
    )
    .add(
      {
        targets: '#blue-line #thick-line-2 path',
        strokeDashoffset: [anime.setDashoffset, 0],
        easing: 'easeOutCubic',
      },
      '-=200'
    );

Here we create a new timeline with some default properties duration: 300 and easing: 'linear'. These will be inherited by each of the following animations created using the add function.

Next, we create the first animation in the chain:

src/components/Logo.vue
JAVASCRIPT
.add({
      targets: '#blue-line #square-1',
      opacity: [0, 1],
      delay: 600,
    })

This fades in the first blue square after a delay of 600ms. We can then create the next animation, which will be triggered as soon as the first is finished to draw in the thinner part of the blue line:

src/components/Logo.vue
JAVASCRIPT
    .add({
      targets: '#blue-line #thin-line',
      strokeDashoffset: [anime.setDashoffset, 0],
      easing: 'easeOutCubic',
      duration: 600,
    })

Notice the stroke-dashoffset trick again to draw the line in. Also, notice how we overwrite the default duration and easing of the timeline.

Finally, we have the two thicker blue lines that sit on top to animate in too.

src/components/Logo.vue
JAVASCRIPT
   .add(
      {
        targets: '#blue-line #thick-line-1 path',
        strokeDashoffset: [anime.setDashoffset, 0],
        easing: 'easeOutCubic',
      },
      '-=500'
    )
    .add(
      {
        targets: '#blue-line #thick-line-2 path',
        strokeDashoffset: [anime.setDashoffset, 0],
        easing: 'easeOutCubic',
      },
      '-=200'
    )

You should recognise most of this by now, but you may have spotted the second parameter passed into the add function. This offset allows you to adjust when this part of the timeline starts playing. For example, for the first thick line, we pass a value of -=500. This says, rather than starting exactly when the previous animation finishes, start 500ms before it's finished. Again, this provides a way of tuning your timeline to make it look perfect.

To be honest, we find that when you start using offsets with timelines, it can get pretty hard to wrap your head around, and often you are better off just creating several individual animations and adjusting the delay manually yourself. It was worth demonstrating the timeline functionality though!

Your logo should be looking pretty funky now!

Animating the tip of the logo

So far, we have a logo that builds in, starting with the letters and following around the edge. The last part missing animation are the three lines and dot to the left of "TICTACGO". Let's sort that out! Update your animateIn function again with this:

src/components/Logo.vue
JAVASCRIPT
anime({
    targets: '#logo-end-1',
    'fill-opacity': [0.27, 1, 0.27],
    easing: 'easeOutCubic',
    duration: 600,
    delay: 1400,
  });

  anime({
    targets: '#logo-end-2',
    'fill-opacity': [0.27, 1, 0.27],
    easing: 'easeOutCubic',
    duration: 600,
    delay: 1500,
  });

  anime({
    targets: '#logo-end-3',
    'fill-opacity': [0.27, 1, 0.27],
    easing: 'easeOutCubic',
    duration: 600,
    delay: 1600,
  });

  anime({
    targets: '#logo-end-dot',
    opacity: [1, 0.27, 1],
    easing: 'easeOutCubic',
    duration: 800,
    delay: 1700,
  });

Here we create four new animations to create a staggered "flash" effect, starting with the rightmost rectangle and ending with the little left square. We achieve this by animating the fill-opacity property from 0.27, to 1, and back to 0.27 again. Here, instead of using the timeline, create the stagger effect by manually setting the delay of each animation.

Your build-in animation will now be complete!

Creating an idle animation

We want the logo to look like an old flickering neon sign, so after the build-in animation happens, we should make a few of the letters flicker. Create a new function in the Logo.vue called flicker and with the following:

src/components/Logo.vue
JAVASCRIPT
function flicker() {
  anime({
    targets: ['#letter_8', '#letter_4'],
    opacity: [1, 0, 1, 0.2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    easing: 'linear',
    direction: 'alternate',
    duration: 2000,
    delay: 0,
    loop: true,
  });
}

function animateIn() {
   ...
}

Here we target two letters and create a flicker effect by animating the opacity of the letters very quickly. Then, we add loads of keyframes into the opacity array to ensure it doesn't look like a fade and is more of a flicker.

Now let's update the last animation in our animateIn function to start our flicker animation when it's finished.

src/components/Logo.vue
JAVASCRIPT

anime({
    targets: '#logo-end-dot',
    opacity: [1, 0.27, 1],
    easing: 'easeOutCubic',
    duration: 800,
    delay: 1700,
    complete: () => {
      flicker();
    },
  });

When the #logo-end-dot animation is complete, it will start our flicker animation. Let's see it:

Making the animations optional with props

Yay, our fantastic animated logo component is almost there! There is just one more step we need to take before we are finished. It would be useful to toggle the build animation on and off because we are unlikely to want the logo to animate in like that every time it's used. Let's add a prop that will allow us to turn it on and off:

src/components/Logo.vue
JAVASCRIPT
const props = withDefaults(
  defineProps<{
    animateIn?: boolean;
  }>(),
  {
    animateIn: false,
  }
);

Here we create a new prop called animateIn and default it to false. Currently, this won't do anything. We need to add a bit more logic to the onMounted hook to make use of this new prop:

src/components/Logo.vue
JAVASCRIPT
onMounted(() => {
  if (props.animateIn) {
    animateIn();
  } else {
    flicker();
  }
});

Perfect! If the animateIn prop is not true, the logo will be static and start the flicker animation immediately!

And we're done!

We initially thought this part of the blog series would be pretty brief, but as you can see, quite a lot of work goes into making a little animated logo like the one we just created.

Hopefully, you learned a few tips on how to make SVG's easier to work with when exporting from Figma, Anime.js and Vue.

Now we have an awesome logo, we are ready to start putting together some of the screens for our game. Watch out for the next part of this series, where we will create the home screen and its components!

Remember to subscribe or follow us, so you don't miss out on the next part of the tutorial!

Related posts.