Netlify 百万开发者 SVG 动画网站制作

Avatar of Sarah Drasner
Sarah Drasner

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费积分!

以下文章记录了为 Netlify 构建百万开发者微型网站的过程。 这个项目是由几个人完成的,我们在这里记录了一些构建过程的一部分——主要关注动画方面,以防对构建类似体验的其他人有所帮助。

从 SVG 构建 Vue 应用程序

SVG 的妙处在于你可以把它想象成一个巨大的战舰游戏,以及它的坐标系。 你实际上是在考虑 x、y、宽度和高度。

<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>

在更大的应用程序组件中,我们有大型标题,但正如您所看到的,其余部分是一个巨大的 SVG。 从那里,我们将剩余的巨大 SVG 分解成几个组件

  • 桌面和移动设备的“糖果乐园”式路径,由 Vuex 存储中的状态有条件地显示
  • 有 27 个站点,不包括它们对应的文本,还有许多装饰性组件,例如灌木、树木和路灯,在一个组件中很难跟踪这么多内容,因此它们按年份进行分解
  • “您在此处”标记,仅在桌面且您已登录时显示

SVG 非常灵活,因为我们不仅可以在该坐标系中 绘制绝对和相对形状和路径,我们还可以在 SVG 中绘制 SVG。 我们只需要定义这些 SVG 的 xywidthheight,我们就可以将它们安装在更大的 SVG 中,这正是我们将对所有这些组件所做的事情,以便我们可以在需要时调整它们的位置。 组件内的 <g> 代表 group,你可以将其视为 HTML 中的 div

因此,在年份组件中,它看起来像这样

<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>

在这些组件中,您可以看到许多模式

  • 我们有灌木和树木用于装饰,我们可以通过 props 使用 xy 值将它们撒在周围
  • 我们可以拥有独立的站点组件,它们也有两个不同的定位值,一个用于大型设备,另一个用于小型设备
  • 我们有一个文本组件,它有三个可用的 slots,一个用于日期,两个用于两行不同的文本
  • 我们还在同步加载装饰性组件,并异步加载那些更重的 SVG 站点

SVG 动画

百万开发者标题动画

SVG 动画使用 GreenSock (GSAP) 完成,以及他们的新 ScrollTrigger 插件。 我写了一篇关于 如何使用 GSAP 进行动画制作 的指南,用于今年早些时候发布的最新 3.0 版本。 如果你不熟悉这个库,这可能是一个不错的起点。

使用该插件很直观,以下是我们需要的基本功能

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();
 },
};

首先,我们导入 gsap 和我们需要的包,以及来自 Vuex 存储的状态。 我将 toggleActionsstart 配置设置放在存储中,并将它们传递到每个组件中,因为在工作时,我需要试验要触发动画的 UI 位置,这使我无需分别配置每个组件。

存储中的那些配置看起来像这样

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

此配置分解为

  • toggleConfig:在页面向下滚动时播放动画(另一个选项是说重新启动,如果再次看到它,它将重新触发),当它超出视窗范围时暂停(这可能略微有助于性能),并且在向上滚动页面时不会反向重新触发。
  • startConfig 表示当元素的中心向下从视窗高度的 90% 开始时,触发动画开始。

这些是我们为这个项目确定的设置,还有很多其他设置! 你可以通过 这段视频 了解所有选项。

对于这个特定的动画,如果它是横幅动画,不需要在滚动时触发,或者如果它在时间线上稍后出现,我们需要对它进行不同的处理。 我们传入一个 prop,并使用它根据 props 中的数字传入该配置

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

然后,对于动画本身,我使用的是所谓的 时间线上的标签,你可以将其视为识别播放头上你可能想要挂起动画或功能的时间点。 我们必须确保也将数字 prop 用于标签,这样我们才能使标题和页脚组件的时间线保持分离。

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`
);

百万开发者动画中有很多事情要做,所以我只隔离一部分动作来分解:上面是女孩摆动双腿。 我们有两只腿分别摆动,两只腿都重复多次,并且 yoyo: true 使 GSAP 知道我希望动画在每次交替时反转。 我们正在旋转双腿,但使它看起来更逼真的是 transformOrigin 从腿的中心顶部开始,因此当它旋转时,它围绕膝盖轴旋转,就像膝盖那样:)

添加动画切换

animation toggle

我们希望让用户能够在没有动画的情况下浏览网站,如果他们有前庭疾病,因此我们创建了一个动画播放状态的切换。 切换本身没有什么特别之处——它通过 mutation 更新 Vuex 存储中的状态,正如您所料

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

真正的更新发生在最顶层的 App 组件中,我们在那里收集所有动画和触发器,然后根据存储中的状态调整它们。 我们 watch isAnimationDisabled 属性以查看是否有变化,当发生变化时,我们获取应用程序中所有 scrolltrigger 动画的实例。 我们没有 .kill() 动画,这是一个选项,因为如果我们这样做,我们就无法重新启动它们。

相反,我们要么将它们的进度设置为最后一帧(如果动画已禁用),要么如果我们正在重新启动它们,我们将它们的进度设置为 0,这样它们在设置为在页面上触发时就可以重新启动。 如果我们在这里使用 .restart(),所有动画都会播放,并且我们不会看到它们在我们继续向下滚动页面时触发。 两全其美!

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 可访问性

我绝不是可访问性方面的专家,所以如果我在此犯了错误,请告诉我——但我对这个网站进行了相当多的研究和测试,并且很兴奋的是,当我通过语音朗读在 Macbook 上进行测试时,该网站的相关信息是可以遍历的,因此我将分享我们为实现这一点所做的事情。

对于包装所有内容的初始 SVG,我们没有应用角色,以便屏幕阅读器可以在其中进行遍历。 对于树木和灌木,我们应用了 role="img",以便屏幕阅读器跳过它,而对于我们应用了唯一 idtitle 的更详细的站点,它是 SVG 中的第一个元素。 我们还应用了 role="presentation"

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

我从 Heather Migliorisi 的这篇文章Leonie Watson 的这篇很棒的文章 中学到了很多东西。

SVG 中的文本会在您浏览页面时宣布它本身,并且会找到链接,所有文本都会被朗读。 这是上面提到的那些插槽的文本组件的样子。

<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>

这是一段视频,演示了如果我在 Mac 上浏览 SVG 时会听到什么声音

如果您有其他改进建议,请告诉我们!

如果您想查看代码或提交 PR,该仓库也是开源的。

感谢我的同事 Zach LeathermanHugues Tennier 与我一起完成了这项工作,他们的投入和工作对这个项目至关重要,它只有通过团队合作才能完成! 对 Alejandro Alvarez 的设计表示感谢,他做得非常出色。 向大家致敬。 🙌