【MoYu】能够弹射星星的黑洞

813 阅读4分钟

前言

MoYu有点无聊,突发奇想做个能够弹射星星的黑洞会不会很酷?

结果做下来发现,实际上就是实现了最基本的碰撞检测,好像也没啥好玩的,想拓展一下貌似也想不到有啥好拓展的了。

虽然也没啥好玩的,不过发出来给有兴趣的人康康,兴许还能得到一些大佬的建议可以学习学习,还也能开拓一下创意眼界。

效果展示

星星的生成

四个字,随机坐标。额外再随机加点CSS样式即可。

这里利用了transform: translate(X,Y)进行定位,而不是left、top,原因很简单,性能更高

首先为了方便用JS创建更多的星星,我们可以在html里写一个星星的原型,display设为none(当然要用纯JS写也行,但JS调样式比较麻烦,就不推荐了)

<div class="holePhantom" style="display: none"></div>

接着是创建星星:

      const newStar = star.cloneNode(true); // 从星星原型拷贝新节点作为星星
      // 随机生成星星的坐标
      let left = Math.random() * document.body.offsetWidth;
      let top = Math.random() * document.body.offsetHeight;
      // 利用translate移动,因为transform比单纯的left和right性能更佳
      newStar.style.transform = `translate(${left}px, ${top}px)`;
      newStar.style.display = "block";
      const starCore = newStar.getElementsByClassName("star")[0];
      document.body.appendChild(newStar);

黑洞

黑洞的生成就很简单了,只是一个单纯的html元素,只是多了点样式,样式详情可以看效果展示里的代码,就不赘述了。

JS用到的定量

后面的代码中会用到的一些定量,先罗列出来,以免看不懂了。

    const holePhantomNum = 100; // Hole的幻影拖尾数量
    const holePhantomD = 10; // Hole幻影直径
    const holePhantomR = 5; // Hole幻影半径
    const StarNum = 100; // 星星数量
    const star = document.getElementsByClassName("starBox")[0];
    const Hole = document.getElementsByClassName("holeBox")[0];
    const HoleD = 100; // Hole直径
    const HoleR = HoleD / 2; // Hole半径
    const InitHoleX = Hole.offsetLeft + HoleR; // 初始Hole位置 X坐标
    const InitHoleY = Hole.offsetTop + HoleR; // 初始Hole位置 Y坐标
    const HoleShadow = 20; // Hole的阴影宽度
    const StarScale = 0.6; // 星星的缩放大小,同 .star 保持一致
    const StarWidth = 10; // 星星的宽度
    const StarHeight = 10; // 星星的高度
    const otherXDist = StarWidth + HoleShadow; // 除星星与Hole的坐标距离外,额外的X轴距离
    const otherYDist = StarHeight + HoleShadow; // 除星星与Hole的坐标距离外,额外的Y轴距离
    const starArr = []; // 存储星星Dom的数组

批量生成星星

从简单的单个星星生成,到批量星星生成,只是单纯多了个for循环罢了。

但其实出了单纯的生成外,还要注意我们现在要做的是能够弹射星星的黑洞,那么默认初始化的时候,黑洞内部也不应该有星星才对,因此需要对星星的初始坐标进行规避黑洞范围的计算,完整的代码如下:

    // 生成星星
    for (let i = 0; i < StarNum; i++) {
      const newStar = star.cloneNode(true); // 从星星原型拷贝新节点作为星星
      const randomShining = `shining ${
        0.5 + Math.random() * 0.7
      }s linear infinite`;
      // 随机生成星星的坐标
      let left = Math.random() * document.body.offsetWidth;
      let top = Math.random() * document.body.offsetHeight;
      // 计算当前星星与Hole的距离:√((HoleX - StarX)^2 + (HoleY - StarY)^2)
      const holeDist = Math.abs(
        Math.sqrt(Math.pow(InitHoleX - left, 2) + Math.pow(InitHoleY - top, 2))
      );
      // 星星与Hole重叠,则根据将生成的坐标进行计算定位于Hole外
      if (holeDist < HoleR) {
        // 计算公式:
        // NewX = (HoleR / Dist) * HoleX + (StarX - HoleX)
        // NewY = (HoleR / Dist) * HoleY + (StarY - HoleY)
        const ratio = HoleR / holeDist;
        const leftDist = left - InitHoleX;
        const topDist = top - InitHoleY;
        left = InitHoleX + leftDist * ratio;
        top = InitHoleY + topDist * ratio;
      }
      // 利用translate移动,因为transform比单纯的left和right性能更佳
      newStar.style.transform = `translate(${
        left + (left < InitHoleX ? -1 : 1) * otherXDist
      }px, ${top + (top < InitHoleY ? -1 : 1) * otherYDist}px)`;
      newStar.style.display = "block";
      const starCore = newStar.getElementsByClassName("star")[0];
      starCore.style.animation = randomShining;
      starArr.push(newStar);
      document.body.appendChild(newStar);
    }
    // 移除星星原型
    star.remove();

黑洞的移动

在这里,我们用移动鼠标的方式来控制黑洞,也就是黑洞位置跟随鼠标了,当然如果各位喜欢,也可以换成上下左右,至于坐标的变更也是要稍微改动一下的。

以下是跟随鼠标的代码:

    // 移动Hole(*跟随鼠标)
    const movingHole = (e) => {
      const HoleX = e.x;
      const HoleY = e.y;
      const hole = document.getElementsByClassName("holeBox")[0];
      const HoleDistX = HoleX - HoleR;
      const HoleDistY = HoleY - HoleR;
      hole.style.transform = `translate(${HoleDistX}px, ${HoleDistY}px)`;
      // 弹射星星
      reboundStar(HoleX, HoleY);
    };
    // 开始监听鼠标移动坐标
    document.body.addEventListener("mousemove", movingHole);

弹射星星

弹射星星,需要结合黑洞的坐标和半径等距离计算,实际上,和批量生成星星的计算方法大同小异,基本就是额外添加了边界值判断、一些样式变更等,也不难理解。

首先我们先定义星星移动的函数:

    // 星星移动函数
    const moveStar = (star, MX, MY) => {
      let HoleX = MX;
      let HoleY = MY;
      // 没有传入MX和MY时,直接获取Hole的Dom来获取坐标
      if (!MX || !MY) {
        const hole = document.getElementsByClassName("holeBox")[0];
        const holeTransform = window
          .getComputedStyle(hole)
          .transform.replace("matrix(", "")
          .replace(")", "")
          .split(",");
        HoleX = parseInt(holeTransform.at(-2).trim());
        HoleY = parseInt(holeTransform.at(-1).trim());
      }
      const randomRun = Math.random() * 30; // 额外的随机移动距离,提高随机性(可有可无
      const totalOtherXDist = otherXDist + randomRun;
      const totalOtherYDist = otherYDist + randomRun;
      // 获取Star的坐标
      const starTransform = window
        .getComputedStyle(star)
        .transform.replace("matrix(", "")
        .replace(")", "")
        .split(",");
      let left = parseInt(starTransform.at(-2).trim());
      let top = parseInt(starTransform.at(-1).trim());
      // 计算当前星星与Hole的距离:√((HoleX - StarX)^2 + (HoleY - StarY)^2)
      const holeDist = Math.abs(
        Math.sqrt(Math.pow(HoleX - left, 2) + Math.pow(HoleY - top, 2))
      );
      // 星星与Hole重叠,则根据将生成的坐标进行计算定位于Hole外
      if (holeDist < HoleR) {
        // 计算公式:
        // NewX = (HoleR / Dist) * HoleX + (StarX - HoleX)
        // NewY = (HoleR / Dist) * HoleY + (StarY - HoleY)
        const ratio = HoleR / holeDist;
        const leftDist = left - HoleX;
        const topDist = top - HoleY;
        left = HoleX + leftDist * ratio;
        top = HoleY + topDist * ratio;
        let moveX = left;
        let moveY = top;
        // 边界判断,超出时,往反方向移动
        if (moveX < 0) moveX = moveX + HoleD;
        if (moveX > document.body.offsetWidth) moveX = moveX - HoleD;
        if (moveY < 0) moveY = HoleY + HoleD;
        if (moveY > document.body.offsetHeight) moveY = HoleY - HoleD;
        const starCore = star.getElementsByClassName("star")[0];
        starCore.style.backgroundColor = "#f00"; // 星星被Hole移动时的变色效果
        starCore.style.transform = "scale(1.2)"; // 星星被Hole移动时的放大效果
        // 利用translate移动,因为transform比单纯的left和right性能更佳
        star.style.transform = `translate(${
          moveX + (left < HoleX ? -1 : 1) * totalOtherXDist
        }px, ${moveY + (top < HoleY ? -1 : 1) * totalOtherYDist}px)`;
        setTimeout(() => {
          // 动画结束后,恢复原始颜色和尺寸
          starCore.style.backgroundColor = "#fff";
          starCore.style.transform = "scale(0.6)";
        }, 500);
      }
    };

需要注意的是,我们对黑洞和星星的坐标获取,并不能用常用的offsetLeft和offsetTop等进行获取,要记得我们用的是transform:translate(X,Y)进行定位的,因此这里比较特殊,用的是window.getComputedStyle函数获取transform的matrix值。

弹射星星的函数代码:

    // 弹射星星
    const reboundStar = (MX, MY) => {
      let HoleX = MX;
      let HoleY = MY;
      // 没有传入MX和MY时,直接获取Hole的Dom来获取坐标
      if (!MX || !MY) {
        const hole = document.getElementsByClassName("holeBox")[0];
        const holeTransform = window
          .getComputedStyle(hole)
          .transform.replace("matrix(", "")
          .replace(")", "")
          .split(",");
        HoleX = parseInt(holeTransform.at(-2).trim());
        HoleY = parseInt(holeTransform.at(-1).trim());
      }
      // 对每个星星进行判断并进行移动
      starArr.forEach((star) => {
        moveStar(star, HoleX, HoleY);
      });
    };

性能优化

很好,大家能看到这里,相信已经完成了所有代码,接下来再调一下CSS的动画性能。

其实也很简单,归根结底就是两条代码:

// 强制开启硬件渲染
transform-style: preserve-3d;
// 放在有动画的css样式内,输入动画涉及的属性即可,多个属性以','间隔
will-change: xxx;

不过最终结果虽然稍微好了点,但貌似还是有点卡,大概或许是我的技术还不到位,各位大佬要是有时间留言帮忙多多提点建议呗~

总结

最后,不知道大家理解得怎么样了呢?不过只是个小玩意儿,在各位大佬面前班门弄斧实在是献丑了。不过我无论是看文章还是发文章,不求装逼,不求夸奖,都是抱着纯纯的学习心态,有错误大佬们多多包涵!

感谢各位看官赏脸点了进来,好了,下次再见!

(转载请注明来源)