SVG不规则圆形动画

12 Apr 2019

前言:

记得第一次听说SVG还是刚毕业时听同事说他们在网易的时候研究过SVG,还以为是很古老的技术以为会被canvas淘汰呢

当年还是太年轻,SVG作为一门描述性语言可能在有逻辑处理的需求中力不从心,但是有这毕竟是极少的需求,在大多数情况下SVG的丰富的表意已经完全可以满足我们的需求。

本文记录了使用SVG实现一个动画需求的过程,希望可以帮助大家了解到SVG的一些作用

一、需求

最终的效果是这样一个形状不规则,并且随着时间连续变化的圆形的动画

二、分析

想法1:

这个似乎可以定义一个函数 R = f(A) 即半径R随着角度A变化。那么这个函数满足

1. 连续  
2. f(0) = f(2π)  
3. 高阶可导(有足够的平滑度,不能画出来的像刺猬)  

如果我们找到这样一个函数,就可以生成这样一个不规则的原形了,哈哈似乎目标很明确了

等等,如果我们希望这个圆形扭动起来,那么这个函数得是R = f(A,t) 即半径R随着角度A和时间t变化,并且依然是连续的。这时冥冥之中仿佛看到了数学老师轻蔑的眼神。是的,我的数学知识不允许我再继续任性下去了。

想法2:

这个图形是一个封闭曲线填充构成的,提到封闭曲线,就自然会想到贝塞尔曲线(一个法国车企的工程师发明的)。简单来说,贝塞尔曲线就是由节点练成的曲线,每两个节点之间通过1个或多个控制点来控制曲线的变化。

那我们这个圆形用贝塞尔曲线画应该怎么画呢,这里使用Sketch来画一下:

到这里我们的思路就明朗起来了,只要定义好这样的路径,然后随机对路径上的节点和控制节点进行微调,就可以实现不规则圆形。

需要这个圆形动起来,也很简单了,只需要按照时间改变节点和控制节点的位置即可。

三、技术调研

四、动手实现

我们首先尝试生成一个节点均匀分布路径:

function Node(center, angle, radius) {
  /*
  * 计算每个节点的坐标和2个控制点的坐标
  * 取控制节点距离为 1/4 半径
  */
  let PI_2 = Math.PI / 2

  var dx = radius * Math.cos(angle)
  var dy = radius * Math.sin(angle)
  var node = [center[0] + dx, center[1] + dy]
  var contoller_radius = radius / 4
  
  dx = contoller_radius * Math.cos(angle - PI_2)
  dy = contoller_radius * Math.sin(angle - PI_2)
  var controller1 = [node[0] + dx, node[1] + dy]
  
  dx = contoller_radius * Math.cos(angle + PI_2)
  dy = contoller_radius * Math.sin(angle + PI_2)
  var controller2 = [node[0] + dx, node[1] + dy]
  
  return {
    node: node,
    controller1: controller1,
    controller2: controller2
  }
}

function generateNodes(width, node_count, radius){
  // calc
  var angle = Math.PI * 2 / node_count
  var center = [width / 2, width / 2]
  var nodes = []
  for (var i = 0; i < node_count; ++i) {
    nodes.push(new Node(center, angle * i, radius))
  }
  document.querySelector("#output").append(nodes)
  return nodes
}

function debugNodes(selector, nodes) {
  var s = Snap(selector);
  var d = `M ${nodes[0].node[0]} ${nodes[0].node[1]} `
  for(i = 0; i < nodes.length; ++i) {
    var node = nodes[(i + 1) % nodes.length]
    d += `L ${node.node[0]} ${node.node[1]} `
    s.line(node.controller1[0], node.controller1[1], node.controller2[0], node.controller2[1]).attr({stroke: "#f82653",strokeWidth: 1 });
  }
  s.path(d).attr({fill: "#cccccc", stroke: "#339ce1", strokeWidth: 2});
}

function generateSVG(selector, nodes) {
  /* 根据所有节点以及控制点生成三次贝塞尔曲线
  * 首先 M到第一个节点,然后依次 按照 "C 当前节点的控制节点2 下个节点的控制节点1 下个节点"
  * 参数 生成 Path 的路径
  */
  var s = Snap(selector);
  var d = `M ${nodes[0].node[0]} ${nodes[0].node[1]} `
  for(i = 0; i < nodes.length; ++i) {
    var node1 = nodes[i]
    var node2 = nodes[(i + 1) % nodes.length]
    d += `C ${node1.controller2[0]} ${node1.controller2[1]} ${node2.controller1[0]} ${node2.controller1[1]} ${node2.node[0]} ${node2.node[1]} `
  }
  s.path(d).attr({fill: "#339ce1",stroke: "#339ce1",strokeWidth: 2});
  return s
}

var nodes = generateNodes(200, 7, 100)
debugNodes("#debug", nodes)
generateSVG("#svg", nodes)

代码+在线Demo

如图所示,这是一个7个节点的 Path参数示意图 和实际生成的贝塞尔曲线路径

好了基本的形状有了,要生成随机的不规则圆形,我们通过随机变换节点的位置以及控制节点的位置来实现。 修改如下:

function Node(center, angle, radius, random_adjust, controller_scale) {
  /*
  * 计算每个节点的坐标和2个控制点的坐标
  * random_adjust 随机范围, 一般在 0-0.2
  * controller_scale 控制节点长度和半径的比例,这个参数控制曲线平滑度
  */
  let PI_2 = Math.PI / 2

  var dx = radius * (1 + Math.random() * random_adjust) * Math.cos(angle)
  var dy = radius * (1 + Math.random() * random_adjust) * Math.sin(angle)
  var node = [center[0] + dx, center[1] + dy]
  var contoller_radius = radius * controller_scale
  
  dx = contoller_radius * Math.cos(angle - PI_2)
  dy = contoller_radius * Math.sin(angle - PI_2)
  var controller1 = [node[0] + dx, node[1] + dy]
  
  dx = contoller_radius * Math.cos(angle + PI_2)
  dy = contoller_radius * Math.sin(angle + PI_2)
  var controller2 = [node[0] + dx, node[1] + dy]
  
  return {
    node: node,
    controller1: controller1,
    controller2: controller2
  }
}

看看效果如何

任务基本上完成了大半,剩下的就是让这个圆形动起来, 这个反而是最简单的一步了,使用snapsvg的animate 方法既可以自动创建补间动画,只要生成新的Path路径,就可以按照d属性动起来了

function animate() {
  let d = generatePath(generateNodes(300, 7, 100, 0.2, 0.3))
  circle.animate({d: d}, 2000, null, animate)
}

animate()

效果如图所示:

最终效果

大功告成!

完整代码



Back