您的位置:首页 > 其它

MorphSVGPlugin from GreenSock 的源码注释分析

2015-10-11 19:19 274 查看
不管是ActionScript(flash)还是JavaScript,GreenSock的GSAP几乎是公认的最强动画库。

常用的就是其中的TweenLite、TweenMax、TimelineLite、TimelineMax、CSSPlugin等。

几天前GreenSock发布了MorphSVGPlugin,是收费的,官方的demo真的很炫,效果如下:



可以在这里看到效果和修改代码:http://codepen.io/GreenSock/pen/WQjRXE

好奇心促使我去研究了下官方演示效果是如果实现的,边分析边在源码旁写下了注释,下面就来分享下我的分析。

首先index页面是这样的(svg的路径太多,用省略号表示):

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>MorphSVGPlugin from GreenSock</title>
<link rel="stylesheet" href="css/style.css">
</head>

<body>

<svg width="90%" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 804 340" style="enable-background:new 0 0 804 340;" xml:space="preserve">
<!--样式-->
<style type="text/css">
...
</style>

<!--背景-->
<rect id="background" width="804" height="340"/>

<!--披肩飘动流1-->
<g id="capeFlow1">
...
</g>

<!--披肩飘动流2-->
<g id="capeFlow2">
...
</g>

<!--GreenSock的logo-->
<g id="GUY">
...
</g>

<!--披肩(字母变体后的灰色形状)-->
<path id="cape" class="st8 hide" d="..."/>

<!--腿(字母变体后的灰色形状)-->
<path id="legs" class="st9 hide" d="..."/>

<!--袜子(字母变体后的绿色形状)-->
<path id="sock" class="st0 hide" d="..."/>

<!--头部(字母变体后的灰色形状)-->
<path id="head" class="st9 hide" d="..."/>

<!--身体躯干(字母变体后的灰色形状)-->
<path id="torso" class="st10 hide" d="..."/>

<!--开场用的Morph单词-->
<g id="Morph">
...
</g>

<!--字母SVG以及它们对应的圆-->
<g id="svg">
<span style="white-space:pre">	</span>...
</g>

<!--各种形状-->
<g id="Shapes">
<!--心-->
<path id="heart" class="shape" d="..."/>
<!--星星-->
<path id="star" class="shape" d="..."/>
<!--灯泡-->
<path id="light" class="shape" d="..."/>
<!--拇指-->
<path id="thumb" class="shape" d="..."/>
<!--插头-->
<path id="plug" class="shape" d="..."/>
<!--苹果-->
<path id="apple" class="shape" d="..."/>
<!--钟-->
<path id="clock" class="shape" d="..."/>
<!--火箭-->
<path id="rocket" class="shape" d="..."/>
</g>
<!--MorphSVGPlugin单词-->
<g id="plugin">
...
</g>
<!--fromGreenSock的单词-->
<g id="fromGreenSock">
...
</g>
<!--灯泡-->
<g id="bulb">
...
</g>
<g id="controls">
<line id="track" x1="62" y1="322" x2="744" y2="322"/>
<circle id="scrubber" cx="62" cy="322" r="9"/>
</g>

</svg>
<script src='js/TweenMax.min.js'></script>
<script src='js/MorphSVGPlugin.min.js'></script>
<script src='js/Draggable.min.js'></script>

<script src="js/index.js"></script>

</body>
</html>


然后它的JavaScript是这样的(index.js):

TweenLite.set("svg", {position:"absolute", left:"50%", xPercent:-50});//水平居中
TweenLite.defaultEase = Power4.easeInOut;//默认缓动函数
var tl = new TimelineMax({delay:0.3}),//整体时间轴
flap = new TimelineMax({repeat:10}), //披肩飘动动画时间轴
scrubTween = TweenLite.to("#scrubber", 10, {x:682, ease:Linear.easeNone, paused:true});//进度条动画

/**
* 第一步开场动画,Morph单词变体为greensock的logo
*/
tl.to("#M", 2, {morphSVG:{shape:"#cape", shapeIndex:-19}, fill:"#444"}, 0) //字母M变体成披肩
.to("#H", 2, {morphSVG:"#torso", fill:"#777"}, 0.1) //字母H变体成身体
.to("#P", 2, {morphSVG:{shape:"#legs", shapeIndex:-24}, fill:"#777"}, 0.1) //字母P变体成腿
.to("#O", 2, {morphSVG:"#head", fill:"#777"}, 0.05) //字母O变体成头
.to("#R", 1.5, {morphSVG:{shape:"#sock", shapeIndex:-11}, fill:"#88CE02", transformOrigin:"-100 60"}, 0.5) //字母R变体成袜子
.set("#GUY, #capeFlow1", {visibility:"visible", opacity:1}, 1.6) //将具体logo显示出来
.to("#M, #O, #R, #P, #H", 0.7, {autoAlpha:0, ease:Linear.easeNone}, 1.6); //渐渐隐藏所有字母,让greensock的logo显示出来

flap.add([
waveSVG(
document.getElementById("capeBottom1"),
{
taperEnd: 80,
taperStart:2,
loose:true,
length:120,
angle:-52,
magnitude:10,//量级
phase:110,//相位
duration:2,//
start:6,//贝塞尔曲线开始的点
end:15,//贝塞尔曲线结束的点
repeat:10//重复次数
}
),
waveSVG(
document.getElementById("shadowBottom1"),
{taperEnd: 80, taperStart:2, loose:true, length:120, angle:-32, magnitude:10, phase:110, duration:2, start:7, end:12, repeat:10}
),
waveSVG(
document.getElementById("shadowTop1"),
{loose:true, length:120, angle:-16, magnitude:20, phase:-140, duration:2, start:10, end:14, repeat:10}
),
waveSVG(
document.getElementById("capeTop1"),
{loose:true, angle: 70, length:120, magnitude:10, phase:20, duration:2, start:2, end:9, repeat:10}
)
]);

/**
* 余下的动画
*/
tl.add(flap, 2)

.addLabel("SVG", 2.3)
.staggerFrom("#S, #V, #G", 0.8, {x:-50, autoAlpha:0, rotation:-90, ease:Back.easeOut, transformOrigin:"center center"}, 0.15, "SVG") //第二步动画,SVG三个字母出来
.addLabel("circles", "SVG+=2")
//第三步动画,SVG字母变体成圆
.to("#S", 0.8, {morphSVG:{shape:"#circleOuter", shapeIndex:-1}, fill:"#444", ease:Power3.easeInOut}, "circles")//S变体成外圈圆
.to("#G", 0.8, {morphSVG:{shape:"#circleMid", shapeIndex:-8}, fill:"#777", ease:Power3.easeInOut}, "circles+=0.1")//G变体成中圈圆
.to("#V", 0.8, {morphSVG:{shape:"#circleInner", shapeIndex:10}, fill:"#000", ease:Power3.easeInOut}, "circles+=0.2")//V变体成内圈圆

//第四步动画,在圆内变体各种形状
.addLabel("shapes", 4.9)
.from("#heart", 0.4, {scale:0.1, autoAlpha:0, ease:Back.easeOut, transformOrigin:"center center"}, "shapes")//心形出现
.to("#heart", 1, {morphSVG:{shape:"#star"}, fill:"#FFFF66"}, "shapes+=1")//心形变体成星形
.to("#heart", 1, {morphSVG:{shape:"#thumb"}, fill:"#3399CC"}, "shapes+=2.5")//变体成拇指形
.to("#heart", 1, {morphSVG:{shape:"#rocket"}, fill:"orange"}, "shapes+=4")//变体成火箭形
.to("#heart", 1, {morphSVG:{shape:"#apple"}, fill:"#CC0000"}, "shapes+=5.5")//变体成苹果形
.to("#heart", 1, {morphSVG:{shape:"#plug"}, fill:"#9966CC"}, "shapes+=7")//变体成插头
.to("#heart", 1, {morphSVG:{shape:"#light", shapeIndex:[-19, -13]}, fill:"white"}, "shapes+=8.5")//变体成灯泡
.staggerTo("#S, #V, #G", 0.8, {scale:0, autoAlpha:0, ease:Back.easeIn, transformOrigin:"center center"}, 0.1, "shapes+=10")//三个圆缩小到消失
.set("#heart", {autoAlpha:0}, "shapes+=10")//将灯泡(是心形经过一系列变体形成的)隐藏
.set(".bulb", {autoAlpha:1}, "shapes+=10")//将灯泡分解成许多细节的svg给显示出来替换之前的没有分解的灯泡
//将分解了的灯泡的全部14个组件依次变体成“MorphSVGPlugin”14个字母
.staggerTo(".bulb", 1.5, {cycle:{morphSVG:["#morph1", "#morph2", "#morph3", "#morph4", "#morph5", "#morph6", "#morph7", "#morph8", "#morph9", "#morph10", "#morph11", "#morph12", "#morph13", "#morph14"]}}, 0.05, "shapes+=10.3")
//将fromGreenSock字母显示出来
.from("#fromGreenSock", 1, {x:50, autoAlpha:0, ease:Power2.easeOut}, "shapes+=12");

//利用Draggable工具控制进度条
Draggable.create("#scrubber", {
type:"x",
bounds:{maxX:682, minX:0},
onPress: function() {
tl.pause();
},
onDrag:function() {
tl.time(20 * this.x / 682);
},
onRelease:function() {
tl.resume();
}
});

function updateScrubber() {//边播放,边更新进度条
scrubTween.progress(Math.min(1, tl.time() / 20));
}
tl.time(20).restart(true); //强制最开始初始化,增强运行时性能
tl.eventCallback("onUpdate", updateScrubber);//监听播放进度

/**
* 结合三角正弦函数和MorphSVGPlugin处理披肩飘动
* the function below is a bit advanced and it handles the flapping cape.
* It leverages MorphSVGPlugin to do some heavy lifting, but ultimately relies
* on some custom triganometry applied in an onUpdate to manipulate the points on a sine wave.
*/
function waveSVG(e, vars) {
var _placeDot = function (x, y, vars) {
var _createSVG = function(type, attributes) {
var element = document.createElementNS("http://www.w3.org/2000/svg", type),
reg = /([a-z])([A-Z])/g,
p;
for (p in attributes) {
element.setAttributeNS(null, p.replace(reg, "$1-$2").toLowerCase(), attributes[p]);
}
return element;
},
dot = _createSVG("circle", {cx:x, cy:y, r:vars.size || 6, fill:vars.color || "red"}),
container = vars.container || document.querySelector("svg");
if (container) {
container.appendChild(dot);
}
return dot;
},
_getLength = function(x, y, x2, y2) {//得到两点间的距离
x = x2 - x;
y = y2 - y;
return Math.sqrt(x * x + y * y);
},
_getTotalLength = function(bezier, start, end) {//得到该条贝塞尔曲线的总长度
var x = bezier[start],
y = bezier[start+1],
length = 0,
i;
for (i = start; i < end; i += 2) {
length += _getLength(x, y, x=bezier[i], bezier[i+1]);
}
return length;
},
_DEG2RAD = Math.PI / 180,//角度转弧度所需的乘数
_RAD2DEG = 180 / Math.PI,//弧度转角度所需的乘数
bezier = MorphSVGPlugin.pathDataToRawBezier(e.getAttribute("d"))[0],//将形状路径转换成贝塞尔曲线
start = (vars.start || 0) * 2,//bezier数组的开始角标
end = (vars.end === 0) ? 0 : (vars.end * 2) || (bezier.length - 1),//bezier数组的结束角标
length = vars.length || 100,
magnitude = vars.magnitude || 50,
proxy = {a:0},
debug = !!vars.debug,//是否为开发模式
phase = (vars.phase || 0) * _DEG2RAD,//相位
taperStart = vars.taperStart || 0,
taperEnd = vars.taperEnd || 0,
startX = bezier[start],
startY = bezier[start + 1],
changes = [],
bezierLength = 0,
loose = !!vars.loose,//if true, we'll just make the points influence the current positions instead of forcing them strictly onto the wave.
tl = new TimelineMax({repeat:vars.repeat}),
bezierTotalLength, angle, i, x, y, dx, dy, sin, cos, sin2, cos2, m, pathStart, t, negCos, negSin, rotatedStartX;
if (end >= bezier.length-1) {
end = bezier.length - 2;
}
if (start >= bezier.length) {
start = bezier.length - 1;
}
bezierTotalLength = _getTotalLength(bezier, start, end);//得到该贝塞尔曲线片段的长度

dx = bezier[end] - startX;//起点与终点的x方向上的差值
dy = bezier[end+1] - startY;//起点与终点的y方向上的差值
if (vars.angle || vars.angle === 0) {
angle = vars.angle * _DEG2RAD;
} else {
angle = Math.atan2(dy, dx) - Math.PI / 2;
}
sin = Math.sin(angle);
cos = Math.cos(angle);
sin2 = Math.sin(angle + Math.PI / 2);
cos2 = Math.cos(angle + Math.PI / 2);
negCos = Math.cos(-angle);
negSin = Math.sin(-angle);
rotatedStartX = startX * negCos + startY * negSin;
console.log("negCos:"+negCos+",cos:"+cos);
if (debug) { //note: if debug is true, we drop a red dot at the beginning, yellow at the end, blue dots as control points, and purple as anchors.
_placeDot(bezier[start], bezier[start + 1], {container: e.parentNode, color:"red"});
_placeDot(bezier[end], bezier[end + 1], {container: e.parentNode, color:"yellow"});
console.log("waveSVG() angle: ", angle * _RAD2DEG, "degrees. RED dot is start, YELLOW is end.");
}

x = startX;
y = startY;
for (i = start; i < end; i += 2) {
bezierLength += _getLength(x, y, x=bezier[i], y=bezier[i+1]);
dx = x * negCos + y * negSin; //rotated in the opposite direction
dy = x * negSin + y * negCos;
t = (taperStart && bezierLength < taperStart) ? bezierLength / taperStart : (taperEnd && bezierLength > bezierTotalLength - taperEnd && bezierLength < bezierTotalLength) ? ((bezierTotalLength - bezierLength) / taperEnd) : 1; //taper
m = Math.sin((dx / length) * Math.PI * 2 + phase) * magnitude;
changes.push( {i: i - (start ? 2 : 0), p:dx, a: (dx / length) * Math.PI * 2 + phase, t:t, x: loose ? x - m * sin * t : startX + (dx - rotatedStartX) * cos2 * t, y: loose ? y - m * cos * t : startY + (dx - rotatedStartX) * sin2 * t, smooth: (i % 6 === 0 && i > start && i < end) ? Math.abs( Math.atan2(y - bezier[i-1], x - bezier[i-2]) - Math.atan2(bezier[i+3] - y, bezier[i+2] - x) ) < 0.01 : false} );
if (debug) {
changes[changes.length-1].dot = _placeDot(x, y, {container: e.parenNode, size:3, color: (i % 6) ? "blue" : "purple"});
}
}
//when we're not animating the first point, optimize things by taking out the first x/y and treat them independently so we can merely bezier.join(",") on each update.
if (start) {
pathStart = "M" + bezier.shift() + "," + bezier.shift() + " C";
}

tl.to(proxy, vars.duration || 3, {a:Math.PI * 2, ease:vars.ease || Linear.easeNone, onUpdate:function() {//将0逐渐变成2PI,监听变化,
var l = changes.length,
angle = proxy.a,
node, i, m, x, y, x2, y2, x1, y1, cp, dx, dy, d, a, cpCos, cpSin;
for (i = 0; i < l; i++) {
node = changes[i];
if (node.smooth || i === l - 1 || !changes[i + 1].smooth) {
m = Math.sin(node.a + angle) * magnitude * node.t;
bezier[node.i] = x = node.x + m * sin;
bezier[node.i + 1] = y = node.y + m * cos;

if (node.smooth) { //make sure smooth anchors stay smooth!
cp = changes[i - 1];
m = Math.sin(cp.a + angle) * magnitude * cp.t;
x1 = cp.x + m * sin;
y1 = cp.y + m * cos;

cp = changes[i + 1];
m = Math.sin(cp.a + angle) * magnitude * cp.t;
x2 = cp.x + m * sin;
y2 = cp.y + m * cos;

a = Math.atan2(y2 - y1, x2 - x1);
cpCos = Math.cos(a);
cpSin = Math.sin(a);

dx = x2 - x;
dy = y2 - y;
d = Math.sqrt(dx * dx + dy * dy);
bezier[cp.i] = x + cpCos * d;
bezier[cp.i + 1] = y + cpSin * d;

cp = changes[i - 1];
dx = x1 - x;
dy = y1 - y;
d = Math.sqrt(dx * dx + dy * dy);
bezier[cp.i] = x - cpCos * d;
bezier[cp.i + 1] = y - cpSin * d;
i++;
}
}
}

if (debug) {
for (i = 0; i < l; i++) {
node = changes[i];
node.dot.setAttribute("cx", bezier[node.i]);
node.dot.setAttribute("cy", bezier[node.i + 1]);
}
} else if (start) { //最核心的步骤,替换path的d属性,即路径,改变图形的形状,看起来像是飘动的感觉
e.setAttribute("d", pathStart + bezier.join(","));
} else {
e.setAttribute("d", "M" + bezier[0] + "," + bezier[1] + " C" + bezier.slice(2).join(","));
}
}});
return tl;
}


最难的就是waveSVG函数了,就是让披肩飘动起来的函数,涉及的数学太多了,目前有些还看不懂,但大致就是将传入的svg里面的元素不断变换路径,让它看起来像是飘起来一样,如果有知道waveSVG函数更加细节的还请不吝赐教哈
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: