用代码构建星辰大海

avatar
数据可视化 @蚂蚁集团

作者简介 wuyue 蚂蚁金服·数据体验技术团队

我们的征途是星辰大海
—阿里人的宣言

如何用代码构建阿里的星辰大海?
首先我们想象一下当前发生的一个场景, 漫天繁星闪烁,不时有流星划破天际, 地球在自转, 无数的人在不同的地点因为阿里提供的便捷服务在交易。

效果如下:

embed: sky.mov

结构分解

如何构建上面的场景呢, 分解得到如下结构:

星空

首先要有一片天, 一个矩形,给黑色背景就好了。

我们暂定 宽 width, 高 height, 三维空间要有深度 暂定为 depth。
对应一片闪烁的繁星, 首先要在空间里生成一堆的星星,其次要让星星不停的闪烁, 暂定 按线性差值变化, 有变大(三维空间就是变亮) 有变小(对应变暗) 部分关键代码如下:

var starts = [];
for (let i = 0; i < 10000; i++) {
  starts.push({
    x: Math.random() * width,
    y: Math.random() * height,
    z: Math.random() * depth,
    //当前进度 0 - 1, 当前大小按  (1 - t) * minSize + t * maxSize
    t: Math.random(),
    //变化方向
    direction: Math.random(), 
    //变化步长 暂定为常量 后面可以根据需要每个星星都不一样
    step: 0.01,
    //最小
    minSize: 10,
    //最大
    maxSize: 20 
  });
}
animate();
function animate () {
  requestAnimationFrame(animate);
  starts.forEach((item) => {
    let { t, minSize, maxSize } = item;
    //计算当前尺寸
    item.size = (1 - t) * minSize + t * maxSize;
    //改变进度
    if (t > 1) {
      //如果已经最大了 则开始减少
      item.direction = -1; 
    } else if (t < 0) {
      //最小了 则开始变大
      item.direction = 1; 
    }
    //修改进度
    item.t += item.step * item.direction;
  });
  //根据上面的值进行渲染
  render();
}

至此 我们就完成了繁星。 但是 漫天星光闪烁,是要有流星的, 这样才够美丽。流星就是在星空有快速划过一条线的星星。所以可以如下:

//流星处理逻辑
var falls = [];
for (let i = 0; i < 10; i++) {
  falls.push({
    //流星的起点
    start: {
      x: Math.random() * width,
      y: Math.random() * height,
      z: Math.random() * depth
    },
    //流星的终点
    end: {
      x: Math.random() * width,
      y: Math.random() * height,
      z: Math.random() * depth
    },
    //当前进度 0 - 1, 当前位置按 二次贝塞尔曲线计算即可 
    t: Math.random(),
    step: 0.01
  });
}

function animate () {
  requestAnimationFrame(animate);
  starts.forEach((item) => {
    let { t, start as p0, end as p2} = item;
    //随意设置一个控制点
    let p1 = {
      x: p0.x,
      y: (p0.y + p2.y) / 2,
      z: p2.z 
    };
    //按二次贝塞尔曲线 计算当前位置
    //(1 - t) * (1 - t) * p0 + 2 * t * (1 - t ) * p1  + t * t * p2
    
    let tmp = {
      x: (1 - t) * (1 - t) * p0.x + 2 * t * (1 - t ) * p1.x + t * t * p2.x,
      y: (1 - t) * (1 - t) * p0.y + 2 * t * (1 - t ) * p1.y + t * t * p2.y,
      z: (1 - t) * (1 - t) * p0.z + 2 * t * (1 - t ) * p1.z + t * t * p2.z
    };
    item.t += item.step;
    item.tmp = tmp;
    //改变进度
    if (t > 1) {
      //如果已经最大了变为0 
      item.t = 0; 
      //重置起始点
      item.start = {
        x: Math.random() * width,
        y: Math.random() * height,
        z: Math.random() * depth
      };
      item.end = {
        x: Math.random() * width,
        y: Math.random() * height,
        z: Math.random() * depth
      };
    }
  });
  //根据上面的值进行渲染
  render();
}

当然 上面只是为了 讲解方便, 把闪烁流星的效果分开了, 其实这两部分是有重合的逻辑的, 在星星状态中加一个标志位是闪烁还是流星,然后两个逻辑就可以合在一起了,具体见github上代码实现部分这里不再啰嗦。

地球

星空有了,下面我们要构建地球了。 地球大概分为两部分, 首先 创建一个球,在三维空间的原点,然后给地球蒙上一层纹理皮肤,就是一张平面地图 从 -90 ~ 90, -180 -180的平面地图 如下:

然后就是关键的来了 ,地球上的交易点需要高亮, 所以也就是在相应的经纬度上 给一些高亮效果。具体定位代码如下:

  //经纬度转x,y平面坐标
  lnglatToXY ({lng, lat}, width, height) {
    let x = (lng - (-180)) / 360 * width;
    let y = Math.abs((lat - 90) / 180) * height;
    return {
      x,y
    };
  }

然后就是生产纹理, 大背景图片,加上热点canvas上面按位置画图就可以了。

    //渲染纹理
    async function render () {
      //全球背景图片
      let worldBg = await util.loadImg(worldSrc);
      //热点背景图片
      let hotBg = await util.loadImg(hotSrc);
      //画大背景
      ctx.drawImage(worldBg, 0, 0, width, height);
      //按经纬度绘制热点
      data.forEach((item) => {
        let lng = item.lnglat[0];
        let lat = item.lnglat[1];
        let {x, y} = util.lnglatToXY({lng, lat}, width, height);
        ctx.drawImage(hotBg, x - size / 2, y - size / 2, size, size);
      });
      //生产纹理,然后 直接映射蒙到球上就可以了
      let texture = new THREE.Texture(canvas);
      texture.needsUpdate = true;
      return texture;
    }

ok,到这里就地球就生成了。后面就让地球每帧绕y轴旋转既可

animate();
function animate () {
  requestAnimationFrame(animate);
  earth.rotation.y += 0.01;
  render();
}

交易线

ok 旋转的地球也有了, 下面就是要绘制 地球上的交易线了。还是首先要有经纬度 到空间坐标的一个转换。 具体原理见下面。

  //经纬度转空间坐标的具体代码, r 为球体半径
  lnglatToXYZ ({lng, lat}, r) {
    var phi = (90 - lat) * Math.PI / 180;
    var theta = -1 * lng * Math.PI / 180;
    return {
      x: r * Math.sin(phi) * Math.cos(theta),
      y: r * Math.cos(phi),
      z: r * Math.sin(phi) * Math.sin(theta)
    };
  }

所以 就很明确了,垂直于地球的线, 就是 同样的经纬度 ,不同的 半径。所以可以按上面公式计算出 p1, p2确定出一条直线。然后和闪烁的星空道理一样加入控制点和方向变量 大量线段就起伏变化了。关键代码如下:

//原始数据
let lines = [
  {lng, lat },
  {lng, lat },
  ....
];

//生成一些控制参数
lines.forEach((item) => {
  //控制点 0 - 1 用来计算直线的终点 t * length 这里随机数是为了 不同的线初始状态不同
  item.t = Math.random(); 
  //变化方向
  item.direction = Math.random() > 0.5 ? 1 : -1;
  //变化速度 暂定都一样
  item.step = 0.01;
  //线段的长度 这里可以根据实际数据 比如 value来映射长度 本出为了示意 用随机数了
  item.length =  Math.random() * r;
});

animate();
function animate () {
  requestAnimationFrame(animate);
  lines.forEach((item) => {
    let p1 = lnglatToXYZ(item.lng, item.lat, r);
    //当前的终点 用 t 
    let p2 = lnglatToXYZ(item.lng, item.lat, r + item.length * t);
    //根据 p1, p2 即可绘制一条直线
    if (item.t > 1) {
      item.direction = -1; 
    } else if (item.t < 0) {
      item.direction = 1; 
    }
    item.t += item.direction * item.step;
  });
  render();
}

ok 以上就是 闪烁的繁星, 天边划过的流星, 旋转的地球, 呼吸的交易线 按对应位置组合在一起, 就是星辰大海了。 当然 看到这里 你可能会问, 星辰有了 但是 说好的大海呢, 其实有的 你仔细看 地球表面都是水啊,那就是海。

最后附上源码,供大家参考。https://github.com/liuwuyue/earth 具体效果见这里 http://yiqihaiqilai.com, http://yiqihaiqilai.com?line (个人服务器,可能会抽风, 如不能看,请自己下载下源码 运行下,不用来找我)

最最后的一个重要事情,我们组这种场景还有很多, 欢迎入伙,感兴趣的同学可以关注专栏或者发送简历至'wuyue.lwy####alibaba-inc.com'.replace('####', '@'),大家一起描绘阿里的星辰大海。~

原文地址:github.com/ProtoTeam/b…