使用 SVG 和 GreenSock 重现 Google Logo 动画

Avatar of Petr Tichy
Petr Tichy 发表

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

以下是 Petr Tichy 的客座文章。 Petr 撰写了许多关于动画、交互和 SVG 的文章,因此我很高兴能邀请他来分享一些他的专业知识,并展示一个如此有趣的演示。 我相信很多人以前都见过这个动画,但它可能感觉有点神奇,超出了我们在网络上可以做到的范围。 就像由视频艺术家/视频编辑软件创建的。 但不,SVG 可以做到这一点。

不久前,Google 公布了其 更新的品牌形象,并在一个带有创意动画的页面上推出了新的标识。

我尝试使用 SVG 和 GreenSock 重现其中一些动画。 在本文中,我将逐步引导您完成我的过程。 希望您能在此过程中有所收获。

这是最终的动画

查看 CodePen 上 Petr TIchy (@ihatetomatoes) 的作品 Google SVG Logo – GreenSock Animation

在 Illustrator 中设置 SVG

第一步是在 SVG 中创建所有需要动画化的部分。 我在 Illustrator 中描摹了一个 .png 屏幕截图,并创建了一个简单的圆形轮廓。

为了帮助我正确对齐,我将这个圆圈分成 4 个相等的部分。 然后,我创建了 4 个彼此叠加的圆圈和 4 个点,尽量做到精确。

我知道我将使用 GreenSock 进行动画制作,并且我知道使用 DrawSVGPlugin 可以轻松地对圆圈进行动画制作。

为了在“G”的直线和圆圈的顶部之间创建间隙,我创建了一个不可见的白色元素 #gMask(下图中的 #1 和 #2)。 说实话,这有点丑,但它确实完成了创建最终“G”形状的任务。

直线动画是使用 3 个部分创建的(下图中的 #3 和 #4)。

  • A – 带圆角的直线 – 用于线动画的开始
  • B – clipPath 蒙版 – 裁剪区域
  • C – 直线 – 在动画结束时显示

仍在 Illustrator 中时,我将所有图层分组并将其命名为逻辑名称。 这为我节省了以后在代码编辑器中的时间。 在 HTML 中,对 SVG 元素进行分组和重命名可能是一个相当缓慢的过程。 在 Illustrator 中直接执行此操作要容易得多。

然后,我导出了 SVG 并将其通过 Peter Collingridge 的 SVG 在线编辑器 以减少一些千字节。

整个动画序列可以分解成 3 个部分

  1. 点的波浪动画
  2. 点变成字母“G”
  3. 从“G”回到点

让我们更详细地分解各个部分。

1. 点的波浪动画

这是动画的第一部分,也是最容易创建的部分。

查看 CodePen 上 Petr TIchy (@ihatetomatoes) 的作品 Google SVG Logo – GreenSock Animation

我们只是使用 GreenSock 的 .to() 方法 对点进行上下动画。

每个点都有自己的时间轴 dotWaveTl,它将其移动到 y: -7, y: 7,然后返回到 y: 0

然后,所有四个时间轴都以轻微的延迟插入到主时间轴 dotsWaveTl 中。

这会创建所有四个点的波浪效果。

function getDotsWaveTl(){
  var dotsWaveTl = new TimelineMax();

  $dots.each(function(index, element){

    var dotWaveTl = new TimelineMax(),
        delay = 0.15;

    dotWaveTl
      .to(element, 0.4, {y: -7, ease:Power1.easeOut})
      .to(element, 0.8, {y: 7, ease:Power1.easeInOut})
      .to(element, 0.4, {y: 0, ease:Power1.easeIn})
      .to(element, 0.4, {y: -7, ease:Power1.easeOut})
      .to(element, 0.8, {y: 7, ease:Power1.easeInOut})
      .to(element, 0.4, {y: 0, ease:Power1.easeIn});

    dotsWaveTl.add(dotWaveTl, delay*index);

  });

  return dotsWaveTl;
}

在这个 getDotsWaveTl 函数中,我们循环遍历所有点,为每个点创建一个时间轴,然后将这些时间轴插入到此函数返回的 dotsWaveTl 中。

各个补间动画具有不同的缓动效果,使此波浪感觉更自然。

然后,我们只需将此时间轴添加到主时间轴 tl 中。

/* Main timeline */
tl.add(getDotsWaveTl());

我们将把动画序列的其他部分添加到同一个主时间轴中。

如果您想了解有关 GreenSock 时间轴如何工作的更多信息,请查看我的 GreenSock TimelineLite 教程

2. 点变成字母“G”

这是迄今为止动画中最难的部分。

查看 CodePen 上 Petr TIchy (@ihatetomatoes) 的作品 Google SVG Logo – GreenSock Animation

是什么让它如此困难? 有几个原因。 我必须

  • 为每个点计算一条路径
  • 为每个点计算精确的时间
  • 计算直蓝线的时机和动画
  • 同步点和线动画

当我们查看代码时,您会看到动画的这部分是使用两个时间轴创建的。

/* Dots rotation */
function getDotsRotateTl(){
  var dotsRotateTl = new TimelineMax();

  dotsRotateTl
    .to($dotRed, 0.9, {bezier:{curviness: 1.5, values: pathRed, ease:Power2.easeInOut}}, 0)
    .to($dotYellow, 1.2, {bezier:{curviness: 1, values: pathYellow, ease:Power2.easeInOut}}, 0)
    .to($dotGreen, 1.5, {bezier:{curviness: 1, values: pathGreen, ease:Power2.easeInOut}}, 0);

  return dotsRotateTl;
}

dotsRotateTl 是一个时间轴,我在其中沿着贝塞尔曲线对红色、黄色和绿色点进行动画处理。 我知道起点在哪里以及我想让点在哪里结束,我只需要计算出中间的点。

pathRed 是一个定义红色点动画路径的点数组。

黄色和绿色点也使用了相同的技术。 我为每个点定义了 4 个点,并计算出了正确的持续时间(黄色为 1.2 秒,绿色点为 1.5 秒)。

第二个时间轴稍微复杂一些。

/* Draw G */
function getDrawGTl(){
  var drawGTl = new TimelineMax();

  drawGTl
    .to($dotBlue, 0.6, {x: 47, ease:Power2.easeIn})
    .set($gLineAnim, {autoAlpha: 1, immediateRender: false})
    .set($dotBlue, {autoAlpha: 0, immediateRender: false}, '+=0.1')
    .from($gLineAnim, 0.8, {x: -120, ease:Power2.easeOut}, '-=0.2')

    /* draw red part */
    .add('startDrawingG', 1)
    .set($gRed, {autoAlpha: 1, immediateRender: false}, 'startDrawingG')
    .fromTo($gRed, 0.5, {drawSVG:"71% 88%"}, {drawSVG:"0% 26%", ease:Power1.easeOut}, '-=0.2')
    .set($dotRed, {autoAlpha: 0, immediateRender: false}, 'startDrawingG')

    /* draw yellow part */
    .set($gYellow, {autoAlpha: 1, immediateRender: false}, 'startDrawingG+=0.1')
    .fromTo($gYellow, 0.6, {drawSVG:"71% 88%"}, {drawSVG:"17% 36%", ease:Power2.easeOut}, '-=0.45')
    .set($dotYellow, {autoAlpha: 0, immediateRender: false}, 'startDrawingG+=0.1')

    /* draw green part */
    .set($gGreen, {autoAlpha: 1, immediateRender: false}, 'startDrawingG+=0.1')
    .fromTo($gGreen, 0.55, {drawSVG:"71% 88%"}, {drawSVG:"36% 61%", ease:Power2.easeOut}, '-=0.6')
    .set($dotGreen, {autoAlpha: 0, immediateRender: false}, 'startDrawingG+=0.4')

    /* draw blue part */
    .set($gLineAnim, {autoAlpha: 0, immediateRender: false}, 'startDrawingG+=0.3')
    .set($gLine, {autoAlpha: 1, immediateRender: false}, 'startDrawingG+=0.3')
    .set($gBlue, {autoAlpha: 1, immediateRender: false}, 'startDrawingG+=0.3')
    .fromTo($gBlue, 0.55, {drawSVG:"71% 88%"}, {drawSVG:"61% 78%", ease:Power2.easeOut}, '-=0.55')

    /* draw ending red part */
    .set($gRedb, {autoAlpha: 1, immediateRender: false}, 'startDrawingG+=0.25')
    .fromTo($gRedb, 0.7, {rotation: '-10', drawSVG:"100% 100%"}, {rotation: '0',drawSVG:"80% 100%", ease:Power2.easeOut}, '-=0.22');

  return drawGTl;
}

drawGTl 时间轴控制蓝点的移动。 当它向右移动 47 个像素时,我们隐藏它并显示带圆角右边的线 (A),如 SVG 分解的第二个图像所示。

drawGTl 时间轴播放 1 秒后,我添加了 startDrawingG 标签。 这使我能够在正确的时间添加其他补间动画,并通过调整其偏移量来精确控制它们,例如 startDrawingG+=0.3

一旦红色点到达交点,我们就隐藏它并显示红色圆圈,然后使用 GreenSock DrawSVGPlugin 对其进行动画处理。

.fromTo($gRed, 0.5, {drawSVG:"71% 88%"}, {drawSVG:"0% 26%", ease:Power1.easeOut}, '-=0.2')

在这一刻,以及在整个演示开发过程中,我大量使用了 jQuery UI 滑块来精确计算正确的数值和时间。

一旦红色段绘制得足够长,我们就对所有后续段进行同样的操作。

所有 71% 88% 段的起点相同,但每个段的终点都不同。

为了使用 DrawSVGPlugin 完成整个字母“G”的绘制,我必须复制红色圆圈并在正确的时间对新的 $gRedb 进行动画处理。

我在 HTML 中直接进行了此更新;无需返回 Illustrator。

.fromTo($gRedb, 0.7, {rotation: '-10', drawSVG:"100% 100%"}, {rotation: '0', drawSVG:"80% 100%", ease:Power2.easeOut}, '-=0.22');

旋转、drawSVG 值、时间和正确的偏移量的组合使我能够创建红色段的无缝动画。

3. 从“G”回到点

返回到点的动画更容易创建。 我使用了与上一个时间轴相同的技术,只是顺序相反。

查看 CodePen 上 Petr TIchy (@ihatetomatoes) 的作品 Google SVG Logo – GreenSock Animation

我首先使用 DrawSVGPlugin 使彩色段变短,然后开始旋转它们。

当彩色段足够小时,我隐藏它,显示相关的点并将其动画化到起始位置。

以下是时间轴上发生情况的示意图

以下是返回此时间轴的整个函数

/* Back to dots */
function getBackToDotsTl(){
  var backToDotsTl = new TimelineMax();

  backToDotsTl
    /* blue straight line out */
    .to($gLineMask, 0.3, {attr: {x: 365}, transformOrigin: 'right center', ease:Power0.easeNone})
    .set([$gLineMask, $gLine], {autoAlpha: 0})

    /* start moving colored segments (circles) */
    .add('rotateG')
    .to($gBlue, 0.3, {drawSVG:"56% 78%", ease:Power0.easeNone}, 'rotateG-=0.3')
    .to($gGreen, 0.3, {drawSVG:"31% 56%", ease:Power0.easeNone}, 'rotateG-=0.3')
    .to($gYellow, 0.3, {drawSVG:"12% 31%", ease:Power0.easeNone}, 'rotateG-=0.3')
    .to($gRed, 0.3, {drawSVG:"0% 21%", ease:Power0.easeNone}, 'rotateG-=0.3')

    /* start rotating colored segments (circles) */
    .add('rotateCircles')
    .to([$gBlue, $gRed, $gGreen], 0.4, {rotation:"+=50", transformOrigin: 'center center', ease:Power0.easeNone}, 'rotateCircles')

    .to($gGreen, 0.4, {drawSVG:"10% 20%", ease:Power0.easeNone}, 'rotateCircles')
    .to($gYellow, 0.4, {rotation:"+=40", transformOrigin: 'center center', drawSVG:"0% 10%", ease:Power0.easeNone}, 'rotateCircles')
    .to($gBlue, 0.4, {drawSVG:"50% 60%", ease:Power0.easeNone}, 'rotateCircles')
    .to($gRed, 0.1, {drawSVG:"0% 0%", ease:Power0.easeNone}, 'rotateCircles')
    .to($gRedb, 0.3, {rotation:"+=50", drawSVG:"80% 90%", ease:Power2.easeInOut}, 'rotateCircles')

    /* show red dot */
    .set($dotRed, {autoAlpha: 1, x: 60, y: -37}, 'rotateCircles+=0.1')
    .set($gRedb, {autoAlpha: 0}, 'rotateCircles+=0.1')
    .to($dotRed, 0.9, {bezier:{curviness: 1.5, values: pathRedBack, ease:Power2.easeOut}}, 'rotateCircles+=0.1')

    /* show blue dot */
    .set($dotBlue, {autoAlpha: 1, x: 51, y: 53}, 'rotateCircles+=0.3')
    .set($gBlue, {autoAlpha: 0}, 'rotateCircles+=0.3')
    .to($dotBlue, 0.6, {bezier:{curviness: 1.5, values: pathBlueBack, ease:Power2.easeOut}}, 'rotateCircles+=0.3')

    /* show yellow dot */
    .set($dotYellow, {autoAlpha: 1, x: -5, y: -44}, 'rotateCircles+=0.4')
    .set($gYellow, {autoAlpha: 0}, 'rotateCircles+=0.4')
    .to($dotYellow, 0.7, {bezier:{curviness: 1.5, values: pathYellowBack, ease:Power2.easeOut}}, 'rotateCircles+=0.4')

    /* show green dot */
    .set($dotGreen, {autoAlpha: 1, x: -108, y: -56}, 'rotateCircles+=0.4')
    .set($gGreen, {autoAlpha: 0}, 'rotateCircles+=0.4')
    .to($dotGreen, 0.6, {bezier:{curviness: 1.5, values: pathGreenBack, ease:Power2.easeOut}}, 'rotateCircles+=0.4')

    .to($gMask, 0.3, {rotation:"+=60", transformOrigin: '-9 58', ease:Power2.easeInOut}, 'rotateCircles');

  return backToDotsTl;
}

我使用了 rotateGrotateCircles 标签来将此时间轴上的大多数补间动画放置在所需的确切位置。

然后,我将此函数包含在主时间轴中,并调整了插入它的确切时间。

/* Main timeline */
tl.add(getDotsWaveTl())
  .add(getDotsRotateTl(), '-=0.35')
  .add(getDrawGTl(), '-=1.6')
  .add(getBackToDotsTl(), '+=2');

tl.timeScale(1.8);

getBackToDotsTl 以 2 秒的延迟 ('+=2') 插入,这使得“G”在动画返回到点之前保持完整 2 秒。

我还通过将 timeScale 设置为 1.8 而不是默认的 1 来调整动画的最终速度。 随意更改此值以查看不同速度下的动画。

最后一个要提到的函数是 init()

/* Init */
function init(){
  TweenLite.set([$gLine,$circles], {autoAlpha: 0});
  TweenLite.set($gBlue, {drawSVG:"61% 78%"}); /* start at 71% 78% */
  TweenLite.set($gGreen, {drawSVG:"36% 61%"});
  TweenLite.set($gYellow, {drawSVG:"17% 36%"});
  TweenLite.set($gRed, {drawSVG:"0% 26%"});
  TweenLite.set($gRedb, {drawSVG:"78% 100%", transformOrigin: 'center center'});
  TweenLite.set($gLineAnim, {autoAlpha: 0});
}
init();

这是一个我设置所有内容的函数,例如隐藏一些元素,使段具有正确的长度等。

我在一些更复杂的 GSAP 动画中使用了这个函数,例如这个 SVG GreenSock Lab,以便获得我需要的正确起点,因为大多数情况下我都是从 Illustrator 中导出包含所有素材的最终设计或帧。

换句话说,这个函数为动画的开始设置了正确的画布。

为什么我选择使用 GreenSock?

CSS-Tricks 上已经有一些关于 GreenSock 的文章了,所以我假设你可能已经了解它的优势。但是,以下是我选择这个动画库的一些原因:

  • GSAP 让你能够精确控制单个补间动画和时间轴的时序和持续时间。
  • GSAP API 提供了令人难以置信的灵活的时间轴嵌套功能。
  • GSAP 修复了许多在处理 SVG 时出现的跨浏览器不一致问题。
  • jQuery UI 滑块允许你浏览 GSAP 时间轴,从而加快开发速度。
  • GSAP 让你能够专注于交互式项目的创意部分。
  • 使用标签对于更复杂的时间轴来说非常棒。

总结

我知道我动画中的一些代码片段和时间轴可能看起来很复杂,所以请随时在下面的评论中提出任何问题,或在 Twitter 上联系我 @ihatetomatoes