天幕:六边形特效

2,456 阅读4分钟

如题,我们将主要使用画布 Canvas 来实现一个六边形布满夜空的效果,如下 GIF 图效果。

天幕.gif

前言

最近《三体》 这部剧比较火热,本效果的创作灵感来自其片尾的画面 - 智子 2 号二维已经成功展开,将对人类进行实时监控

二维展开.png

👌,效果不错。那么,我们是否可以模拟呢?

我们在本文实现的功能有:

  1. 绘制六边形
  2. 六边形效果
  3. 鼠标交互效果 - 该操作如果是在 canvas 上直接绘制的话,会比较耗性能,所以我们使用了其他的 dom 元素来实现,文中有介绍。

其中,第 1 点是重点,我们详细讲解。

绘制六边形

绘制六边形,我们的思路如下:

1. 找到六边形的点

在这里,我们使用到三角形的知识点 - 正弦(sine)sin(θ), 余弦(cosine)con(θ) 求距离。

直角三角形计算.png

应用到六边形上,我们以六边形的中心为圆心画圆,就可以很直观得观察到,如下👇

六边形计算.png

假设我们设置圆心坐标为 (0, 0),圆的半径为 r,那么我们将得到右下角的点坐标为 (cos(360 / 6 / 2 deg) * r, sin(360 / 6 / 2 deg) * r)。同理,我们可以得到其他 5 个点的的坐标。相关代码如下:

/*
* x, y 为原点坐标
* r 为圆的半径
*/
function locate(x, y, r) {
  // 定位六边形的六个点
  for(let i = 0; i < 6; i += 1) {
    particlePosition.push({
      x: x + Math.cos(Math.PI / 6 * (1 + 2 * i))*r,
      y: y + Math.sin(Math.PI / 6 * (1 + 2 * i))*r
    })
  }
}

2. 将点连线

我们定位到六边形的六个点之后,遍历这些点,将两点距离大于等于 r - 1 且小于等于 r + 1 的点连接起来。

for(let i = 0; i < particlePosition.length; i += 1) {
  for(let j = 0; j < particlePosition.length; j += 1) {
    let dx = particlePosition[i].x - particlePosition[j].x;
    let dy = particlePosition[i].y - particlePosition[j].y;
    let distance = Math.sqrt(dx * dx + dy * dy);
    
    if(distance >= (radius - 1) && distance <= (radius + 1)) {
      // 将六边形的点连接起来
    }
  }
}

这里的判断规则为 distance >= (radius - 1) && distance <= (radius + 1),读者可以自行更改。笔者这里以 1 为偏偏移值,是因为计算出来的两点距离不绝对等于 radius 值。

连线之后,效果如下图:

六边形平铺.png

六边形效果

细心的读者,看到文章开头的 GIF 图就会发现六边形上的线条效果和六边形图片效果。

线条效果

这里使用的是 canvas 的线性渐变函数 createLinearGradient 来实现:

let randomArr = [Math.random(), Math.random(), Math.random()];
randomArr.sort(function(a,b){
  return a-b;
});
// 渐变的颜色
gradient = context.createLinearGradient(particlePosition[i].x, particlePosition[i].y, particlePosition[j].x, particlePosition[j].y);
gradient.addColorStop(randomArr[0], 'red');
gradient.addColorStop(randomArr[1], 'fuchsia');
gradient.addColorStop(randomArr[2], 'purple');
context.strokeStyle = gradient;

context.lineWidth = 1;
context.beginPath();
context.moveTo(particlePosition[i].x, particlePosition[i].y);
context.lineTo(particlePosition[j].x, particlePosition[j].y);
context.stroke();

六边形图片效果

六边形图片效果,本来是用 canvasclip 这个 api 去实现的,但是发现在本案例实现起来,翻车了,页面卡死了,故选择了操作 img 节点结合 css 来实现:

<img id="img" src="" alt="img"/>

这里的 src 值置空,是为了后面我们可以通过 javascript 来操作,切换图片引用。

#img{
  position: absolute;
  top: -100%;
  left: -100%;
  display: block;
  width: 100px;
  height: 100px;
  clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
  -webkit-clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
  z-index: 99;
}

clip-path 裁剪图片为指定的 polygon 多边形,这里裁剪为六边形。为了保持和生成的六边形尺寸一致,我们也通过 javascript 来控制图片的大小,如下 javascript 所示👇

const radius = 30;
let imgDom = document.getElementById('img');
imgDom.style.width = Math.cos(Math.PI / 6) * radius * 2 + 'px';
imgDom.style.height = radius * 2 +'px';

鼠标交互

我们实现的鼠标交互的效果是:当鼠标移动时候,计算鼠标位置和圆心位置距离最近的点进行定位并绘制当前的六边形

// 图片定位
imgDom.src = currentTarget.img.src;
imgDom.style.left = currentTarget.x - Math.cos(Math.PI / 6) * radius + "px";
imgDom.style.top = currentTarget.y - radius + "px";
// 绘制出六边形
context.beginPath();
for(let i = 0; i < 6; i += 1) {
  if(i === 0) {
    context.moveTo(currentTarget.x + Math.cos(Math.PI / 6)*radius, currentTarget.y + Math.sin(Math.PI / 6)*radius);
  } else {
    context.lineTo(currentTarget.x + Math.cos(Math.PI / 6 * (1 + 2 * i))*radius, currentTarget.y + Math.sin(Math.PI / 6 * (1 + 2 * i))*radius);
  }
}
context.closePath();

当然,我们还考虑了六边形图片随机播放圆心的计算等问题。

最终得到的效果如下 GIF 图👇

天幕.gif

源码

我们在码上掘金上完成本效果。项目源码见: 天幕:六边形特效