糟糕,我在帕鲁大陆抓到龙了! | 春节创意投稿

最近趁着幻兽帕鲁比较火,刚好准备参加掘金的活动,蹭个热度。

老规矩,先展示一下最终效果(代码在文末):

小游戏比较简单,页面有一只可爱的"龙"随机运动,点击按钮弹出一个有"帕鲁球"的弹窗,当弹窗中的"帕鲁球"完全包裹我们的"龙"时,游戏结束。

游戏涉及到几个知识点:

  • 浏览器跨 Tab 通信
  • 如何实现龙的随机运动,并在弹出中保证龙卡片位置和页面实时一致
  • 如何用 CSS 画一个好看的球

下面我详细讲解一下以上几点。

浏览器跨 Tab 通信

这个知识点是在前一阵这个动画很火的时候学到的:

在 《浏览器跨 Tab 窗口通信原理及应用实践》 中 Coco 大佬介绍了三种实现方案,本文采取了最简单的 localStorage 来实现。

实现思路非常简单,在 A 页面通过 localStorage.setItem 来设置数据, 在 B 页面通过 window.addEventListener('storage', (event) => {...}) 来监听数据变化,在 A 和 B 同源的情况下就可以实现跨 Tab 通信了。

简单举个例子:

写一个html 文件来设置数据:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <body>
    <script>
      setInterval(() => {
        const currentTime = new Date().toLocaleTimeString();
        const randomNumber = Math.random();
        console.log(`设置数据, 时间 ${currentTime} 随机数 ${randomNumber}`);
        localStorage.setItem("currentTime", currentTime);
        localStorage.setItem("randomNumber", randomNumber);
      }, 1000);
    </script>
  </body>
</html>

再写一个文件来接受数据:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <body>
    <script>
      window.addEventListener("storage", (event) => {
        const { key, oldValue, newValue } = event;
        console.log(
          `接手到storage改变事件, key:${key} 旧值${oldValue} 新值${newValue}`
        );
      });
    </script>
  </body>
</html>

通过控制台看下输出可以看到数据传输是被成功接收到了,我们可以获取到设置的 localStoragekey以及对应的值:

龙的随机运动

首先找一张龙的图片,找了好几张,最后保留了和龙年最配的图片:

把元素指定为 fixed 定位,然后指定元素的 topleft 就可以让图片动起来了。每隔 1s 随机给元素一个窗口内的任意位置,并通过 transition: 1s; 让元素的运动的时间为 1s,这样就实现了不停随机运动的龙。

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>我不吃饼干</title>

    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      html {
        height: 100%;
      }
      body {
        height: 100%;
        border: 20px ridge #bc7a4e;

        background: url(./bg.jpeg);
      }
      .dragon-img {
        width: 200px;
        height: 200px;
        border-radius: 50%;
        box-shadow: 2px 2px 6px 0px rgb(0 0 0 / 34%),
          -2px 2px 6px 0px rgb(0 0 0 / 6%);
        border: 10px solid #ffd56c;
        transition: 1s;
        position: fixed;
        top: 200px;
        left: 200px;
        z-index: -1;
      }
    </style>
  </head>

  <body>
    <img class="dragon-img" id="dragonImg" src="./dragon.jpeg" />

    <script>
      // 生成[x,y]之间的随机数
      function getRandomIntBetween(x, y) {
        return Math.floor(Math.random() * (y - x + 1)) + x;
      }

      // 获取图片元素
      const dragonImg = document.getElementById("dragonImg");

      // 随机运动函数
      function move() {
        // 根据浏览器大小设置运动的边界
        const maxX = window.innerWidth - 220;
        const maxY = window.innerHeight - 220;
        // 生成随机位置
        const x = getRandomIntBetween(20, maxX);
        const y = getRandomIntBetween(20, maxY);
        // 设置龙的新位置
        dragonImg.style.left = `${x}px`;
        dragonImg.style.top = `${y}px`;
      }

      // 每1秒移动一次
      const moveTimer = setInterval(move, 1000);
    </script>
  </body>
</html>

但是我需要给另一个弹窗发送实时位置,如何发送呢?我想的是每 20ms 发送一次当前位置:

js 复制代码
const sendTimer = setInterval(() => {
  const x = dragonImg.style.left;
  const y = dragonImg.style.top;
  console.log("发送改动位置", `${x},${y}`);
  localStorage.setItem("__dragon_move", `${x},${y}`);
}, 20);

让我们看一下效果:

可以看到数据虽然是每 20ms 发送一次,但是每 1s 内发送的是同一个位置,原来我们通过 dragonImg.style.leftdragonImg.style.top 取的是我们设置运动的最终位置,而不是当前的实时位置,这样就无法实现两个页面的同步运动了,通过 window.getComputedStyle(dragonImg) 我们可以获取元素的实时位置,现在我们改一下:

js 复制代码
// 获取图片的实时位置
function getRealTimeLocation() {
  const style = window.getComputedStyle(dragonImg);
  const x = parseInt(style.left);
  const y = parseInt(style.top);

  return [x, y];
}

const sendTimer = setInterval(() => {
  const [x, y] = getRealTimeLocation();
  console.log("发送改动位置", `${x},${y}`);
  localStorage.setItem("__dragon_move", `${x},${y}`);
}, 20);

现在可以正确的发送位置了,但是要注意一点,我们希望的是两个页面的龙在屏幕上的位置一样,而不是对应浏览器页面左上角的距离相同,但是两个浏览器Tab页和屏幕的左上角距离是不一样的,所以我们在发送位置的时候还需要考虑浏览器和屏幕的左上角距离。

js 复制代码
const sendTimer = setInterval(() => {
  // 浏览器窗口左上角相对于屏幕左上角的距离
  const { screenX, screenY } = window;
  const [x, y] = getRealTimeLocation();
  // 估算浏览器UI元素的高度 包括了视口的顶部,比如标签栏和工具栏。
  const z = outerHeight - innerHeight;
  console.log("发送改动位置", `${screenX + x},${screenY + y},${z}`);
  localStorage.setItem("__dragon_move", `${screenX + x},${screenY + y},${z}`);
}, 20);

用 CSS 画一个好看的球

我相信这个这个纯 CSS 实现的球对于大部分人还是有难度的,因为我也是抄的。

不过我可以简单讲一下实现原理 XD。

首先画一个球,我们给它加一个径向渐变

css 复制代码
.ball {
  position: relative;
  width: 300px;
  height: 300px;
  border-radius: 100%;
  background: radial-gradient(circle at 50% 55%,
          rgba(240, 245, 255, 0.9),
          rgba(240, 245, 255, 0.9) 40%,
          rgba(225, 238, 255, 0.8) 60%,
          rgba(43, 130, 255, 0.4));
}

然后通过 beforeafter 两个伪元素加一下高亮样式,原理就是通过径向渐变在合适的位置加上白色的高亮。

现在一个球展示显示完成了。

代码

主页面

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>我不吃饼干</title>

  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    html {
      height: 100%;
    }

    body {
      height: 100%;
      border: 20px ridge #bc7a4e;

      background: url(./bg.jpeg);
    }

    .dragon-img {
      width: 200px;
      height: 200px;
      border-radius: 50%;
      box-shadow: 2px 2px 6px 0px rgb(0 0 0 / 34%),
        -2px 2px 6px 0px rgb(0 0 0 / 6%);
      border: 10px solid #ffd56c;
      transition: 1s;
      position: fixed;
      top: 200px;
      left: 200px;
      z-index: -1;
    }

    .open-button {
      width: 300px;
      height: 100px;
      border: none;
      border-radius: 50px;
      font-size: 36px;
      margin: 20px auto;
      display: block;
      animation: blink 1s infinite;
    }

    .open-button:active {
      transform: scale(0.95);
      /* 点击时缩小按钮 */
    }

    @keyframes blink {

      0%,
      100% {
        background-color: #f00;
        /* 按钮初始和结束时的背景颜色 */
        color: #fff;
        /* 按钮文字颜色 */
        box-shadow: 0 0 10px #f00;
        /* 外发光效果增加紧迫感 */
      }

      50% {
        background-color: #fff;
        /* 按钮中间状态的背景颜色 */
        color: #f00;
        /* 按钮文字颜色 */
        box-shadow: 0 0 20px #f00;
        /* 外发光效果更强 */
      }
    }
  </style>
</head>

<body>
  <img class="dragon-img" id="dragonImg" src="./dragon.jpeg" />

  <button class="open-button" id="openButton" onclick="openBall()">
    弹出帕鲁球
  </button>

  <script>
    let openBall = () => {
      window.open(
        "./ball.html",
        "_blank",
        "width=315,height=315,menubar=no,toolbar=no,location=no,status=no,resizable=no,scrollbars=no"
      );
      openButton.style.display = "none";
    };

    // 生成[x,y]之间的随机数
    function getRandomIntBetween(x, y) {
      return Math.floor(Math.random() * (y - x + 1)) + x;
    }

    // 获取图片元素
    const dragonImg = document.getElementById("dragonImg");

    // 随机运动函数
    function move() {
      // 根据浏览器大小设置运动的边界
      const maxX = window.innerWidth - 220;
      const maxY = window.innerHeight - 220;
      // 生成随机位置
      const x = getRandomIntBetween(20, maxX);
      const y = getRandomIntBetween(20, maxY);
      // 设置龙的新位置
      dragonImg.style.left = `${x}px`;
      dragonImg.style.top = `${y}px`;
    }

    // 获取图片的实时位置
    function getRealTimeLocation() {
      const style = window.getComputedStyle(dragonImg);
      const x = parseInt(style.left);
      const y = parseInt(style.top);

      return [x, y];
    }

    // 每1秒移动一次
    const moveTimer = setInterval(move, 1000);

    const sendTimer = setInterval(() => {
      // 浏览器窗口左上角相对于屏幕左上角的距离
      const { screenX, screenY } = window;
      const [x, y] = getRealTimeLocation();
      // 估算浏览器UI元素的高度 包括了视口的顶部,比如标签栏和工具栏。
      const z = outerHeight - innerHeight;
      console.log("发送改动位置", `${screenX + x},${screenY + y},${z}`);
      localStorage.setItem(
        "__dragon_move",
        `${screenX + x},${screenY + y},${z}`
      );
    }, 20);

    window.addEventListener("storage", (event) => {
      const { key, newValue } = event;
      if (key === "__dragon_stop") {
        console.log("游戏结束");
        console.log(dragonImg.style)
        const style = window.getComputedStyle(dragonImg);
        dragonImg.style.left = style.left;
        dragonImg.style.top = style.top;
        clearInterval(moveTimer);
        clearInterval(sendTimer);
      }
    });

  </script>
</body>

</html>

弹出窗口页 ball.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>点击此处拖动帕鲁球</title>

    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }

      html {
        height: 100%;
      }

      body {
        height: 100%;
      }

      .dragon-img {
        width: 200px;
        height: 200px;
        border-radius: 50%;
        box-shadow: 2px 2px 6px 0px rgb(0 0 0 / 34%),
          -2px 2px 6px 0px rgb(0 0 0 / 6%);
        border: 10px solid #ffd56c;
        /* transition: 1s; */
        position: fixed;
        top: 0px;
        left: 0px;
        z-index: -1;
      }

      .ball {
        position: relative;
        width: 300px;
        height: 300px;
        border-radius: 100%;
        background: radial-gradient(
          circle at 50% 55%,
          rgba(240, 245, 255, 0.9),
          rgba(240, 245, 255, 0.9) 40%,
          rgba(225, 238, 255, 0.8) 60%,
          rgba(43, 130, 255, 0.4)
        );
        opacity: 0.8;
      }

      .ball:before {
        content: "";
        position: absolute;
        top: 1%;
        left: 5%;
        border-radius: 100%;
        height: 80%;
        width: 40%;
        background: radial-gradient(
          circle at 130% 130%,
          rgba(255, 255, 255, 0) 0,
          rgba(255, 255, 255, 0) 46%,
          rgba(255, 255, 255, 0.8) 50%,
          rgba(255, 255, 255, 0.8) 58%,
          rgba(255, 255, 255, 0) 60%,
          rgba(255, 255, 255, 0) 100%
        );
        transform: translateX(131%) translateY(58%) rotateZ(168deg)
          rotateX(10deg);
      }

      .ball:after {
        content: "";
        position: absolute;
        display: block;
        top: 5%;
        left: 10%;
        width: 80%;
        height: 80%;
        border-radius: 100%;
        z-index: 2;
        transform: rotateZ(-30deg);
        background: radial-gradient(
          circle at 50% 80%,
          rgba(255, 255, 255, 0),
          rgba(255, 255, 255, 0) 74%,
          white 80%,
          white 84%,
          rgba(255, 255, 255, 0) 100%
        );
      }
    </style>
  </head>

  <body>
    <div class="ball"></div>
    <img class="dragon-img" id="dragonImg" src="./dragon.jpeg" />

    <script>
      let over = false;
      // 计算两个圆心之间的距离
      function isCircleACoveringCircleB(xA, yA, rA, xB, yB, rB) {
        var distance = Math.sqrt((xA - xB) ** 2 + (yA - yB) ** 2);
        console.log(xA, yA, rA, xB, yB, rB, distance);
        // 判断圆A的半径是否足够大以覆盖圆B
        return rA >= distance + rB;
      }

      window.addEventListener("storage", (event) => {
        if (over) {
          return;
        }
        const { key, newValue } = event;
        console.log("接受改动位置", newValue);
        if (key === "__dragon_move") {
          const [dx, dy, dz] = newValue.split(",");

          // 浏览器窗口左上角相对于屏幕左上角的距离
          const { screenX, screenY } = window;
          const x = dx - screenX;
          const y = dy - screenY;

          const z = outerHeight - innerHeight;

          const left = x;
          const top = y + (dz - z);

          dragonImg.style.left = `${left}px`;
          dragonImg.style.top = `${top}px`;

          const cover = isCircleACoveringCircleB(
            150,
            150,
            150,
            left + 100,
            top + 100,
            100
          );

          if (cover) {
            console.log("游戏结束");
            over = true;

            localStorage.setItem(
              "__dragon_stop",
              `${screenX + x},${screenY + y}`
            );
          }
        }
      });
    </script>
  </body>
</html>

参考资料

相关推荐
Мартин.3 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。4 小时前
案例-表白墙简单实现
前端·javascript·css
数云界4 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd4 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常4 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer4 小时前
Vite:为什么选 Vite
前端
小御姐@stella4 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing4 小时前
【React】增量传输与渲染
前端·javascript·面试
eHackyd4 小时前
前端知识汇总(持续更新)
前端
万叶学编程7 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js