木匣子

Web/Game/Programming/Life etc.

The Red Heart

今天在网上闲逛的时候看到了这个漂亮的心形曲线特效,动感十足。于是动手分析了一下它的实现方式,记于此文。

See the Pen The Red Heart by Ninja Lau (@mutoo) on CodePen.

记得 Make things with math 的作者 Steven Wittens 曾经说过:任何看起来复杂的函数图形,都是由各种简单函数组成的。这个心形曲线特效也不例外。

0x01 Heart Curve

Wolfram MathWorld 的 Heart Curve 页面记载了几种心形曲线的公式:

Heart Curve

其中第六个参数方程非常符合我们的案例。代码中的 heartPosition 函数正是出于此:

var heartPosition = function (rad) {
    return [16 * Math.pow(Math.sin(rad), 3), -(15 * Math.cos(rad) - 5 * Math.cos(2 * rad) - 2 * Math.cos(3 * rad) - Math.cos(4 * rad))];
};

该函数传入一个由原点发出的射线与x轴的夹角(弧度),返回一组坐标 [x, y] 。由于 canvas 中坐标系的 Y 轴是由上而下的,所以公式中的 Y 坐标需要取反。

0x02 Dotted Heart Curve

通过该函数绕原点一周,我们能得到一个单位长度的心形。从 0 rad 开始,并以设定好的角度 dr 步进,将心形曲线上的一些采样点保存下来:

var pointsOrigin = [];
var dr = 0.1, i;
for (i = 0; i < Math.PI * 2; i += dr)
    pointsOrigin.push(heartPosition(i));

为了让心形更有层次感,我们可以参加一个简单的缩放函数,并创建多个心形:

var scaleAndTranslate = function (pos, sx, sy) {
    return [pos[0] * sx, pos[1] * sy];
};

for (i = 0; i < Math.PI * 2; i += dr)
    pointsOrigin.push(scaleAndTranslate(heartPosition(i), 2, 2));

for (i = 0; i < Math.PI * 2; i += dr)
    pointsOrigin.push(scaleAndTranslate(heartPosition(i), 3, 3));

0x03 Particles

现在我们有了三个不同大小的心形曲线上的采样点了。为了让这些点变得生动一点,接下来需要制造一些粒子在其上面运动。这些粒子遵循下面的运动规则:

  • 从采样点中随机选取一些点为作目标点;
  • 使用简单的物理规则向目标点移动,例如:速度、摩擦力等;
  • 当粒子接近目标点后,会根据概率重新选择目标点。其中有比较大的概念选择临近的采样点,这样可以让心形曲线保持完整的流动性。

0x04 Particle Tracking

粒子运动形单影支,如果能加上轨迹效果就更好了。可以在粒子初始化时在相同位置创建若干个轨迹粒子。轨迹粒子的运动可以采用缓动公式,逐渐向前一个轨迹粒子靠拢即可。

关于粒子运动的算法,这里不细说。想深入研究的话,强烈推荐 Foundation ActionScript 3.0 Animation: Making Things Move 一书,里面的例子简单易懂。

0x05 Heart Beating

既然是个心,那么如何让它跳动呢。首先我们需要一个函数,能对采样点进行缩放:

var targetPoints = [];
var pulse = function (kx, ky) {
    for (i = 0; i < pointsOrigin.length; i++) {
        targetPoints[i] = [];
        targetPoints[i][0] = kx * pointsOrigin[i][0];
        targetPoints[i][1] = ky * pointsOrigin[i][1];
    }
};

然后周期性地改变缩放系数:

var time = 0;
var loop = function () {
    var k = 0.5 + 0.5 * Math.cos(time);
    pulse(k, k);
    time += ((Math.sin(time)) < 0 ? 9 : (k > 0.8) ? .2 : 1) * 0.04;

    /* render particles */

    window.requestAnimationFrame(loop, canvas);
};

有趣的是,这里 time 的步进并不是均速的,而是一组分段函数,为了是模拟心跳的那种节奏感。这里用了很多魔数(magic numbers),显然都是慢慢微调出来的。

0x06 Color

红心的色彩是通过 HSLA 色彩空间随机选取不同饱和度明度的 30% 透明红色,以此来表现出丰富的色泽,虽然只有红色,但是能够给人很炫丽的感觉。