# 使用 SVG 和 JS 创建一个由星形变心形的动画

## 想法

See the Pen star vs. heart: highlight corresponding cubic Bézier curves on click by Ana Tudor (@thebabydino) on CodePen.

## 开始编写代码

```<svg>
<path id='shape'/>
</svg>```

```const _SVG = document.querySelector('svg'),
_SHAPE = document.getElementById('shape'),
D = 1000,
O = { ini: {}, fin: {}, afn: {} };

(function init() {
_SVG.setAttribute('viewBox', [-.5*D, -.5*D, D, D].join(' '));
})();```

## 几何图形

### 星形

See the Pen construct regular pentagon/ pentagram by Ana Tudor (@thebabydino) on CodePen.

```const P = 5; /* number of cubic curves/ polygon vertices */

function getStarPoints(f = .5) {
const RCO = f*D /* outer (pentagram) circumradius  */,
BAS = 2*(2*Math.PI/P) /* base angle for star poly */,
BAC = 2*Math.PI/P /* base angle for convex poly */,
RI = RCO*Math.cos(.5*BAS) /*pentagram/ inner pentagon inradius */,
RCI = RI/Math.cos(.5*BAC) /* inner pentagon circumradius */,
ND = 2*P /* total number of distinct points we need to get */,
BAD = 2*Math.PI/ND /* base angle for point distribution */,
PTS = [] /* array we fill with point coordinates */;

for(let i = 0; i < ND; i++) {}

return PTS;
}```

See the Pen position of point in a plane (drag point) by Ana Tudor (@thebabydino) on CodePen.

```for(let i = 0; i < ND; i++) {
let cr = i%2 ? RCI : RCO,
x = Math.round(cr*Math.cos(ca)),
y = Math.round(cr*Math.sin(ca));
}```

```for(let i = 0; i < ND; i++) {
/* same as before */

PTS.push([x, y]);
if(!(i%2)) PTS.push([x, y]);
}```

```(function init() {
/* same as before */

O.d = {
ini: getStarPoints(),
afn: function(pts) {
return pts.reduce((a, c, i) => {
return a + (i%3 ? ' ' : 'C') + c
}, `M\${pts[pts.length - 1]}`)
}
};

for(let p in O) _SHAPE.setAttribute(p, O[p].afn(O[p].ini))
})();```

See the Pen make SVG star shape by Ana Tudor (@thebabydino) on CodePen.

`ca = i*BAD + .5*Math.PI`

```function fnStr(fname, farg) { return `\${fname}(\${farg})` };

(function init() {
/* same as before */

O.transform = { ini: -180,  afn: (ang) => fnStr('rotate', ang) };

/* same as before */
})();```

```(function init() {
/* same as before */

O.fill = { ini: [255, 215, 0],  afn: (rgb) => fnStr('rgb', rgb) };

/* same as before */
})();```

See the Pen make SVG star shape #2 by Ana Tudor (@thebabydino) on CodePen.

### 心形

TO0SO1 是一个正方形，所以它的所有角度都是 `90°`（弧度为 `π/2` ） 。

```function getHeartPoints(f = .25) {
const R = f*D /* helper circle radius  */,
RC = Math.round(R/Math.SQRT2) /* circumradius of square of edge R */,
XT = 0, YT = -RC /* coords of point T */,
XA = 2*RC, YA = -RC /* coords of A points (x in abs value) */,
XB = 2*RC, YB = RC /* coords of B points (x in abs value) */,
XC = 0, YC = 3*RC /* coords of point C */,
XD = RC, YD = -2*RC /* coords of D points (x in abs value) */,
XE = 3*RC, YE = 0 /* coords of E points (x in abs value) */;
}```

See the Pen heart structure - end and intersection points by Ana Tudor (@thebabydino) on CodePen.

```function getHeartPoints(f = .25) {
/* same as before */
const /* const for cubic curve approx of quarter circle */
C = .551915,
CC = 1 - C,
/* coords of ctrl points on TD segs */
XTD = Math.round(CC*XT + C*XD), YTD = Math.round(CC*YT + C*YD),
/* coords of ctrl points on AD segs */
/* coords of ctrl points on AE segs */
XAE = Math.round(CC*XA + C*XE), YAE = Math.round(CC*YA + C*YE),
/* coords of ctrl points on BE segs */
XBE = Math.round(CC*XB + C*XE), YBE = Math.round(CC*YB + C*YE);

/* same as before */
}```

See the Pen star vs. heart: corresponding cubic Bézier curves (annotated, highlight on click) by Ana Tudor (@thebabydino) on CodePen.

```return [
[XC, YC], [XC, YC], [-XB, YB],
[-XBE, YBE], [-XAE, YAE], [-XA, YA],
[XAE, YAE], [XBE, YBE], [XB, YB]
];```

```function fnStr(fname, farg) { return `\${fname}(\${farg})` };

(function init() {
_SVG.setAttribute('viewBox', [-.5*D, -.5*D, D, D].join(' '));

O.d = {
ini: getStarPoints(),
fin: getHeartPoints(),
afn: function(pts) {
return pts.reduce((a, c, i) => {
return a + (i%3 ? ' ' : 'C') + c
}, `M\${pts[pts.length - 1]}`)
}
};

O.transform = {
ini: -180,
fin: 0,
afn: (ang) => fnStr('rotate', ang)
};

O.fill = {
ini: [255, 215, 0],
fin: [220, 20, 60],
afn: (rgb) => fnStr('rgb', rgb)
};

for(let p in O) _SHAPE.setAttribute(p, O[p].afn(O[p].fin))
})();```

See the Pen make SVG heart shape by Ana Tudor (@thebabydino) on CodePen.

### 确保两个形状对齐

See the Pen SVG star vs. heart alignment by Ana Tudor (@thebabydino) on CodePen.

`return [ /* same coords */ ].map(([x, y]) => [x, y - .09*R])`

See the Pen star-heart alignment for various f factors by Ana Tudor (@thebabydino) on CodePen.

### 在两个形状之间切换

`_SHAPE` 元素上添加一个 `'click'` 事件监听器并编写这个状态下的代码，我们改变了方向变量 (`dir`) 以及形状的属性，这样就可以实现从金星变红心或者红心变金星：

```let dir = -1;

(function init() {
/* same as before */

dir *= -1;

for(let p in O)
_SHAPE.setAttribute(p, O[p].afn(O[p][dir > 0 ? 'fin' : 'ini']));
}, false);
})();```

See the Pen toggle between star and heart on click by Ana Tudor (@thebabydino) on CodePen.

### 从一个形状到另一个形状的过渡

```/* same as before */
const NF = 50,
TFN = {
'ease-out': function(k) {
return 1 - Math.pow(1 - k, 1.675)
},
'ease-in-out': function(k) {
return .5*(Math.sin((k - .5)*Math.PI) + 1)
},
'bounce-ini-fin': function(k, s = -.65*Math.PI, e = -s) {
return (Math.sin(k*(e - s) + s) - Math.sin(s))/(Math.sin(e) - Math.sin(s))
}
};```

```(function init() {
/* same as before */

O.d = {
/* same as before */
tfn: 'ease-in-out'
};

O.transform = {
/* same as before */
tfn: 'bounce-ini-fin'
};

O.fill = {
/* same as before */
tfn: 'ease-out'
};

/* same as before */
})();```

```let rID = null, cf = 0, m;

function stopAni() {
cancelAnimationFrame(rID);
rID = null;
};

function update() {
cf += dir;

let k = cf/NF;

if(!(cf%NF)) {
stopAni();
return
}

rID = requestAnimationFrame(update)
};```

```addEventListener('click', e => {
if(rID) stopAni();
dir *= -1;
m = .5*(1 - dir);
update();
}, false);```

`update()` 函数中，我们想将过渡属性设置成一些中间值 (取决于进度 `k`) 。正如在之前文章中看到的， 在刚开始甚至设置监听器之前就计算结束值与初始值之间的范围会比较好，所以接下来： 创建一个计算数字（或者数组中的，无论层级多深）范围的函数，然后使用这个函数设置过渡属性值的范围。

```function range(ini, fin) {
return typeof ini == 'number' ?
fin - ini :
ini.map((c, i) => range(ini[i], fin[i]))
};

(function init() {
/* same as before */

for(let p in O) {
O[p].rng = range(O[p].ini, O[p].fin);
_SHAPE.setAttribute(p, O[p].afn(O[p].ini));
}

/* same as before */
})();```

```function update() {
/* same as before */

for(let p in O) {
let c = O[p];

_SHAPE.setAttribute(p, c.afn(int(c.ini, c.rng, TFN[c.tfn], k)));
}

/* same as before */
};```

```function int(ini, rng, tfn, k) {
return typeof ini == 'number' ?
Math.round(ini + (m + dir*tfn(m + dir*k))*rng) :
ini.map((c, i) => int(ini[i], rng[i], tfn, k))
};```

See the Pen SVG + plain JS: star to heart & back (click) by Ana Tudor (@thebabydino) on CodePen.

```function int(ini, rng, tfn, k, cnt) {
return typeof ini == 'number' ?
Math.round(ini + cnt*(m + dir*tfn(m + dir*k))*rng) :
ini.map((c, i) => int(ini[i], rng[i], tfn, k, cnt))
};

function update() {
/* same as before */

for(let p in O) {
let c = O[p];

_SHAPE.setAttribute(p, c.afn(int(c.ini, c.rng, TFN[c.tfn], k, c.cnt ? dir : 1)));
}

/* same as before */
};

(function init() {
/* same as before */

O.transform = {
ini: -180,
fin: 0,
afn: (ang) => fnStr('rotate', ang),
tfn: 'bounce-ini-fin',
cnt: 1
};

/* same as before */
})();```

See the Pen #CodeVember #15 - no library star or heart this? by Ana Tudor (@thebabydino) on CodePen.

0 条评论

• ### Angular Schematics 三部曲之 Add

因工作繁忙，差不多有三个月没有写过技术文章了，自八月份第一次编写 schematics 以来，我一直打算分享关于 schematics 的编写技巧，无奈还是拖到...

• ### Carousel 旋转画廊特效的疑难杂症

疑难杂症 该画廊特效的特点就是前后元素有层级关系。 我想很多人应该看过或者用过这个插件carousel.js，网上也有相关的教程。不知道这个插件的原型是哪个，有...

• ### Ng-Matero 0.1 发布了！

Ng-Matero 0.1 终于发布了！周末折腾了两天，结果最后发版还是出了点意外，好在今天补了一版。

• ### Parsix GNU/Linux 项目宣布即将终止

基于 Debian 的 Parsix 发行版已经宣布将会在 Debian Stretch 发布六个月后终止。官方表示 Parsix GNU/Linux 8.15...

• ### 目前最全，可视化数据工具大集合

数据可视化技术的基本思想是将数据库中每一个数据项作为单个图元元素表示，大量的数据集构成数据图像， 同时将数据的各个属性值以多维数据的形式表示，可以从不同的维度观...

• ### Python-基础-day4

对于浅copy来说，只是在内存中重新创建了开辟了一个空间存放一个新列表，但是新列表中的元素与原列表中的元素是公用的。

• ### Linux如何管理文档多租户

同一群组microsoft下的两个账号justmine001和justmine002需要共同拥有目录/microsoft/eshop的开发权，以便进行协同工作，...

• ### 你真的了解Lateral View explode吗？--源码复盘

Lateral view与UDTF函数一起使用，UDTF对每个输入行产生0或者多个输出行。Lateral view首先在基表的每个输入行应用UDTF，然后连接结...

• ### TP入门第四天

1、URL大小写 默认配置：’URL_CASE_INSENSITIVE’  => false,   // URL地址是否不区分大小写 这样默认情况下是区分大小写...