Die Entstehung von: Netlify’s Million Devs SVG Animation Site

Avatar of Sarah Drasner
Sarah Drasner on

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Der folgende Artikel beschreibt den Prozess des Aufbaus der Million Developers Microsite für Netlify. Dieses Projekt wurde von ein paar Leuten erstellt, und wir haben einige Teile des Entstehungsprozesses hier festgehalten – hauptsächlich mit Fokus auf die Animation, falls es für andere, die ähnliche Erlebnisse schaffen, hilfreich sein mag.

Eine Vue App aus einem SVG erstellen

Die Schönheit von SVG ist, dass man es und das Koordinatensystem wie ein großes Schiffe versenken-Spiel betrachten kann. Man denkt wirklich in Begriffen von x, y, Breite und Höhe.

<div id="app">
   <app-login-result-sticky v-if="user.number" />
   <app-github-corner />

   <app-header />

   <!-- this is one big SVG -->
   <svg id="timeline" xmlns="http://www.w3.org/2000/svg" :viewBox="timelineAttributes.viewBox">
     <!-- this is the desktop path -->
     <path
       class="cls-1 timeline-path"
       transform="translate(16.1 -440.3)"
       d="M951.5,7107..."
     />
     <!-- this is the path for mobile -->
     <app-mobilepath v-if="viewportSize === 'small'" />

     <!-- all of the stations, broken down by year -->
     <app2016 />
     <app2017 />
     <app2018 />
     <app2019 />
     <app2020 />

     <!-- the 'you are here' marker, only shown on desktop and if you're logged in -->
     <app-youarehere v-if="user.number && viewportSize === 'large'" />
   </svg>
 </div>

Innerhalb der größeren App-Komponente haben wir den großen Header, aber der Rest ist ein riesiges SVG. Von dort haben wir den Rest des riesigen SVGs in mehrere Komponenten zerlegt

  • Candyland-ähnliche Pfade für Desktop und Mobilgeräte, bedingt durch einen Zustand im Vuex-Store angezeigt
  • Es gibt 27 Stationen, ihre Textgegenstücke nicht mitgerechnet, und viele dekorative Komponenten wie Büsche, Bäume und Laternen, was in einer Komponente viel zu verfolgen ist, daher sind sie nach Jahren aufgeteilt
  • Der „Sie sind hier“-Marker, nur auf dem Desktop angezeigt, wenn Sie angemeldet sind

SVG ist wunderbar flexibel, da wir nicht nur absolute und relative Formen und Pfade innerhalb dieses Koordinatensystems zeichnen können, sondern auch SVGs innerhalb von SVGs. Wir müssen nur x, y, width und height dieser SVGs definieren und können sie in das größere SVG einbetten, was genau das ist, was wir mit all diesen Komponenten tun werden, damit wir ihre Platzierung bei Bedarf anpassen können. Das <g> innerhalb der Komponenten steht für group (Gruppe), man kann sie sich ein wenig wie divs in HTML vorstellen.

So sieht das in den Jahreskomponenten aus

<template>
 <g>
   <!-- decorative components -->
   <app-tree x="650" y="5500" />
   <app-tree x="700" y="5550" />
   <app-bush x="750" y="5600" />

   <!-- station component -->
   <app-virtual x="1200" y="6000" xSmall="50" ySmall="15100" />
   <!-- text component, with slots -->
   <app-text
     x="1400"
     y="6500"
     xSmall="50"
     ySmall="15600"
     num="20"
     url-slug="jamstack-conf-virtual"
   >
     <template v-slot:date>May 27, 2020</template>
     <template v-slot:event>Jamstack Conf Virtual</template>
   </app-text>

   ...
 </template>

<script>
...

export default {
 components: {
   // loading the decorative components in syncronously
   AppText,
   AppTree,
   AppBush,
   AppStreetlamp2,
   // loading the heavy station components in asyncronously
   AppBuildPlugins: () => import("@/components/AppBuildPlugins.vue"),
   AppMillion: () => import("@/components/AppMillion.vue"),
   AppVirtual: () => import("@/components/AppVirtual.vue"),
 },
};
...
</script>

Innerhalb dieser Komponenten sehen Sie eine Reihe von Mustern

  • Wir haben Büsche und Bäume zur Dekoration, die wir über x- und y-Werte per Props verteilen können
  • Wir können einzelne Stationskomponenten haben, die ebenfalls zwei verschiedene Positionierungswerte haben, einen für große und einen für kleine Geräte
  • Wir haben eine Textkomponente, die drei verfügbare Slots hat, einen für das Datum und zwei für zwei verschiedene Textzeilen
  • Wir laden auch die dekorativen Komponenten synchron und die schwereren SVG-Stationen asynchron.

SVG-Animation

Header-Animation für Million Devs

Die SVG-Animation wird mit GreenSock (GSAP) und deren neuem ScrollTrigger-Plugin durchgeführt. Ich habe Anfang des Jahres einen Leitfaden zur Arbeit mit GSAP für deren neueste 3.0-Version geschrieben. Wenn Sie diese Bibliothek nicht kennen, könnte das ein guter Anfang sein.

Die Arbeit mit dem Plugin ist dankenswerterweise unkompliziert, hier ist die Basis der Funktionalität, die wir benötigen werden

import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger.js";
import { mapState } from "vuex";

gsap.registerPlugin(ScrollTrigger);

export default {
 computed: {
   ...mapState([
     "toggleConfig",
     "startConfig",
     "isAnimationDisabled",
     "viewportSize",
   ]),
 },
 ...
 methods: {
   millionAnim() {
     let vm = this;
     let tl;
     const isScrollElConfig = {
       scrollTrigger: {
         trigger: `.million${vm.num}`,
         toggleActions: this.toggleConfig,
         start: this.startConfig,
       },
       defaults: {
         duration: 1.5,
         ease: "sine",
       },
     };
   }
 },
 mounted() {
   this.millionAnim();
 },
};

Zuerst importieren wir gsap und das benötigte Paket sowie den State aus dem Vuex-Store. Ich habe die toggleActions und start Konfigurationseinstellungen im Store hinterlegt und sie in jede Komponente übergeben, da ich während der Arbeit experimentieren musste, welcher Punkt in der UI die Animationen auslösen sollte. Das ersparte mir die separate Konfiguration jeder Komponente.

Diese Konfigurationen im Store sehen so aus

export default new Vuex.Store({
  state: {
    toggleConfig: `play pause none pause`,
    startConfig: `center 90%`,
  }
}

Diese Konfiguration lässt sich wie folgt aufschlüsseln:

  • toggleConfig: Die Animation wird abgespielt, wenn sie die Seite nach unten durchläuft (eine andere Option ist restart, dann wird sie erneut ausgelöst, wenn man sie wieder sieht), sie pausiert, wenn sie außerhalb des Viewports ist (das kann der Performance leicht helfen), und sie löst beim Zurückgehen nach oben nicht erneut in umgekehrter Richtung aus.
  • startConfig gibt an, dass die Animation beginnen soll, wenn die Mitte des Elements 90% der Viewport-Höhe nach unten erreicht hat.

Dies sind die Einstellungen, die wir für dieses Projekt gewählt haben, es gibt noch viele weitere! Sie können alle Optionen mit diesem Video verstehen.

Für diese spezielle Animation mussten wir sie etwas anders behandeln, wenn es sich um eine Banneranimation handelte, die nicht durch Scrollen ausgelöst werden musste, oder wenn sie später im Zeitplan lag. Wir übergaben einen Prop und nutzten diesen, um die Konfiguration je nach Anzahl der Props zu übergeben.

if (vm.num === 1) {
  tl = gsap.timeline({
    defaults: {
      duration: 1.5,
      ease: "sine",
    },
  });
} else {
  tl = gsap.timeline(isScrollElConfig);
}

Dann verwende ich für die Animation selbst das, was als Label auf der Timeline bezeichnet wird. Man kann es sich wie die Markierung eines Zeitpunkts auf dem Playhead vorstellen, an dem man Animationen oder Funktionalitäten anbringen möchte. Wir müssen auch hier die Nummern-Props für das Label verwenden, damit wir die Timelines für die Header- und Footer-Komponenten getrennt halten.

tl.add(`million${vm.num}`)
...
.from(
  "#front-leg-r",
  {
    duration: 0.5,
    rotation: 10,
    transformOrigin: "50% 0%",
    repeat: 6,
    yoyo: true,
    ease: "sine.inOut",
  },
  `million${vm.num}`
)
.from(
  "#front-leg-l",
  {
    duration: 0.5,
    rotation: 10,
    transformOrigin: "50% 0%",
    repeat: 6,
    yoyo: true,
    ease: "sine.inOut",
  },
  `million${vm.num}+=0.25`
);

In der Million Devs-Animation passiert viel, daher isoliere ich nur ein Bewegungselement, um es aufzuschlüsseln: Oben sehen wir die schwingenden Beine der Mädchen. Wir haben beide Beine separat schwingen, beide wiederholen sich mehrmals, und yoyo: true teilt GSAP mit, dass ich möchte, dass die Animation bei jeder zweiten Änderung rückgängig gemacht wird. Wir drehen die Beine, aber was sie realistisch macht, ist, dass der transformOrigin oben in der Mitte des Beins beginnt, sodass sie sich beim Drehen um die Knieachse drehen, so wie Knie es tun :)

Hinzufügen eines Animations-Toggles

animation toggle

Wir wollten den Benutzern die Möglichkeit geben, die Seite ohne Animation zu erkunden, falls sie eine vestibuläre Störung haben. Daher haben wir einen Schalter für den Animations-Wiedergabezustand erstellt. Der Schalter ist nichts Besonderes – er aktualisiert den Zustand im Vuex-Store über eine Mutation, wie Sie es erwarten würden.

export default new Vuex.Store({
  state: {
    ...
    isAnimationDisabled: false,
  },
  mutations: {
    updateAnimationState(state) {
      state.isAnimationDisabled = !state.isAnimationDisabled
    },
  ...
})

Die eigentlichen Aktualisierungen finden in der obersten App-Komponente statt, wo wir alle Animationen und Trigger sammeln und sie dann basierend auf dem Zustand im Store anpassen. Wir watchen die Eigenschaft isAnimationDisabled auf Änderungen und wenn eine auftritt, greifen wir alle Instanzen von Scrolltrigger-Animationen in der App. Wir kill()en die Animationen nicht, obwohl das eine Option ist, denn wenn wir das täten, könnten wir sie nicht neu starten.

Stattdessen setzen wir ihren Fortschritt entweder auf das letzte Bild, wenn Animationen deaktiviert sind, oder wenn wir sie neu starten, setzen wir ihren Fortschritt auf 0, damit sie neu starten können, wenn sie auf der Seite ausgelöst werden sollen. Wenn wir hier .restart() verwendet hätten, wären alle Animationen abgespielt worden und wir hätten sie nicht beim Weiterblättern auf der Seite ausgelöst gesehen. Das Beste aus beiden Welten!

watch: {
   isAnimationDisabled(newVal, oldVal) {
     ScrollTrigger.getAll().forEach((trigger) => {
       let animation = trigger.animation;
       if (newVal === true) {
         animation && animation.progress(1);
       } else {
         animation && animation.progress(0);
       }
     });
   },
 },

SVG-Barrierefreiheit

Ich bin keineswegs ein Experte für Barrierefreiheit, also lassen Sie mich bitte wissen, wenn ich hier Fehler gemacht habe – aber ich habe viel recherchiert und getestet auf dieser Seite und war ziemlich aufgeregt, dass, als ich auf meinem Macbook über Voiceover getestet habe, die relevanten Informationen der Seite traversierbar waren. Daher teile ich mit, was wir getan haben, um dorthin zu gelangen.

Für das initiale SVG, das alles umschloss, haben wir keine Rolle angewendet, damit der Screenreader darin navigieren kann. Für die Bäume und Büsche haben wir role="img" angewendet, damit der Screenreader sie überspringt, und für die detaillierteren Stationen haben wir eine eindeutige id und title angewendet, was das erste Element innerhalb des SVG war. Wir haben auch role="presentation" angewendet.

<svg
   ...
   role="presentation"
   aria-labelledby="analyticsuklaunch"
 >
   <title id="analyticsuklaunch">Launch of analytics</title>

Vieles davon habe ich aus diesem Artikel von Heather Migliorisi und diesem großartigen Artikel von Leonie Watson gelernt.

Der Text innerhalb des SVG wird beim Tabben durch die Seite angekündigt, und der Link wird gefunden, der gesamte Text wird gelesen. So sieht die Textkomponente aus, mit den oben erwähnten Slots.

<template>
 <a
   :href="`https://www.netlify.com/blog/2020/08/03/netlify-milestones-on-the-road-to-1-million-devs/#${urlSlug}`"
 >
   <svg
     xmlns="http://www.w3.org/2000/svg"
     width="450"
     height="250"
     :x="svgCoords.x"
     :y="svgCoords.y"
     viewBox="0 0 280 115.4"
   >
     <g :class="`textnode text${num}`">
       <text class="d" transform="translate(7.6 14)">
         <slot name="date">Jul 13, 2016</slot>
       </text>
       <text class="e" transform="translate(16.5 48.7)">
         <slot name="event">Something here</slot>
       </text>
       <text class="e" transform="translate(16.5 70)">
         <slot name="event2" />
       </text>
       <text class="h" transform="translate(164.5 104.3)">View Milestone</text>
     </g>
   </svg>
 </a>
</template>

Hier ist ein Video davon, wie sich das anhört, wenn ich auf meinem Mac durch das SVG tabbiere

Wenn Sie weitere Verbesserungsvorschläge haben, lassen Sie es uns bitte wissen!

Das Repository ist auch Open Source, wenn Sie den Code einsehen oder einen PR einreichen möchten.

Vielen Dank (Wortspiel beabsichtigt) an meine Kollegen Zach Leatherman und Hugues Tennier, die mit mir daran gearbeitet haben; ihr Input und ihre Arbeit waren für das Projekt von unschätzbarem Wert. Es existiert nur durch Teamwork, um es zum Abschluss zu bringen! Und viel Respekt an Alejandro Alvarez, der das Design gemacht hat und eine spektakuläre Arbeit geleistet hat. Hoch fünf an alle. 🙌