多窗口量子纠缠技术实现

1. 前言

最近看到下面这个,多窗口下实现量子纠缠的特效感觉很惊艳和有创意。

除 Three.js 的特效果部分,技术实现上还是很简单的。

2. 技术点分析

这里面核心两个技术点:

  1. 不同 Tab 窗口下的消息通讯。
  2. 窗口的位置获取。

2.1 Tab 窗口通讯

比较常用的有两种方式:

  1. Storage API 的 StorageEvent。
  2. BroadcastChannel API。

2.1.1 StorageEvent 实现

通过设置 localStorage/sessionStorage 的 setItem 可以在其他同源窗口下触发 StorageEvent 事件,实现广播效果:

ini 复制代码
[Exposed=Window]
interface StorageEvent : Event {
  constructor(DOMString type, optional StorageEventInit eventInitDict = {});

  readonly attribute DOMString? key;
  readonly attribute DOMString? oldValue;
  readonly attribute DOMString? newValue;
  readonly attribute USVString url;
  readonly attribute Storage? storageArea;

  undefined initStorageEvent(DOMString type, optional boolean bubbles = false, optional boolean cancelable = false, optional DOMString? key = null, optional DOMString? oldValue = null, optional DOMString? newValue = null, optional USVString url = "", optional Storage? storageArea = null);
};

dictionary StorageEventInit : EventInit {
  DOMString? key = null;
  DOMString? oldValue = null;
  DOMString? newValue = null;
  USVString url = "";
  Storage? storageArea = null;
};

演示:

html 复制代码
<button id="a">我的位置</button>
<script>
  window.onstorage = (e) => {
    const { key, newValue, oldValue, url } = e;
    console.log(
      `key: ${key}, newValue: ${newValue}, oldValue: ${oldValue}, url: ${url}`,
      e
    );
  };
  a.addEventListener("click", () => {
    localStorage.setItem(
      "position",
      JSON.stringify({
        x: Math.random(),
        y: Math.random(),
      })
    );
  });
</script>

兼容性:

2.1.2 BroadcastChannel 实现

通过创建同名频道进行通讯。

ini 复制代码
[Exposed=(Window,Worker)]
interface BroadcastChannel : EventTarget {
  constructor(DOMString name);

  readonly attribute DOMString name;
  undefined postMessage(any message);
  undefined close();
  attribute EventHandler onmessage;
  attribute EventHandler onmessageerror;
};

演示:

html 复制代码
<button id="a">我的位置</button>
<script>
  const channel = new BroadcastChannel("my_channel");
  channel.onmessage = (e) => {
    console.log("收到广播", e.data);
  };
  a.addEventListener("click", () => {
    console.log("发送广播");
    channel.postMessage({ x: 100, y: 200 });
  });
</script>

兼容性:

2.2 位置获取

通过获取浏览器窗口在屏幕的位置+内外宽高,就可以获取到绝对的位置。

js 复制代码
const absoluteCenter = {
  x:
    window.screenX +
    (window.outerWidth - window.innerWidth) +
    window.innerWidth / 2,
  y:
    window.screenY +
    (window.outerHeight - window.innerHeight) +
    window.innerHeight / 2,
};

3. 实现

下面我基于 BroadcastChannel 实现一个示例。

实现不同窗口间,根据其他窗口自行创建箭头,并实时指向彼此:

通讯间需要定义清楚消息类型:

js 复制代码
const MSG_TYPE = {
  POSITION: 0,
  NEW: 1,
  REMOVE: 2,
  OTHER: 3,
};

如果要兼容浏览器崩溃下无法发送移除消息,可以加入喂狗程序:

js 复制代码
function sendHeartbeat() {
  channel.postMessage({ type: "heartbeat", pageId: wId });
  lastHeartbeat = Date.now();
}

setInterval(function () {
  if (Date.now() - lastHeartbeat > heartbeatInterval * 2) {
    console.log(wId + "离线");
  }
}, heartbeatInterval);

完整代码:

html 复制代码
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      body,
      html {
        height: 100%;
      }

      .arrow {
        width: 80vmin;
        height: 80vmin;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%) rotate(0deg);
      }

      .arrow::after {
        content: "";
        display: block;
        width: 100vmax;
        height: 3px;
        background-color: red;
        position: absolute;
        top: 50%;
        right: 50%;
        transform: translateY(-50%);
      }

      .arrow img {
        width: 100%;
        height: 100%;
      }

      #center {
        width: 30px;
        height: 30px;
        border-radius: 50%;
        z-index: 999;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
      }

      #msg {
        position: absolute;
        left: 1em;
        top: 1em;
      }
    </style>
  </head>

  <body>
    <div id="center"></div>
    <div id="msg"></div>

    <script>
      const otherWindows = new Map();
      const channel = new BroadcastChannel("tab_position_channel");
      const wId = Date.now() + "-" + Math.random();
      const color =
        "#" + Math.floor(Math.random() * parseInt("ffff", 16)).toString(16);
      let _new = true;
      const MSG_TYPE = {
        POSITION: 0,
        NEW: 1,
        REMOVE: 2,
        OTHER: 3,
      };
      const lastPosition = {
        x: 0,
        y: 0,
      };

      window.onunload = () => {
        channel.postMessage({
          type: MSG_TYPE.REMOVE,
          wId,
        });
      };

      channel.onmessage = ({ data: { type, x, y, wId: otherId } }) => {
        console.log(`type: ${type}, x: ${x}, y: ${y}, wId: ${wId}`);
        switch (type) {
          case MSG_TYPE.NEW:
            _AddWin({ otherId, x, y });

            channel.postMessage({
              type: MSG_TYPE.OTHER,
              x: lastPosition.x,
              y: lastPosition.y,
              wId,
            });

            break;
          case MSG_TYPE.POSITION:
            otherWindows.set(otherId, {
              x,
              y,
            });

            [...otherWindows.keys()].forEach((otherId) => {
              const arrow = document.getElementById(otherId);
              const { x, y } = otherWindows.get(otherId);
              arrow.style.transform = `translate(-50%, -50%) rotate(${
                (Math.atan2(lastPosition.y - y, lastPosition.x - x) * 180) /
                Math.PI
              }deg)`;
            });

            break;
          case MSG_TYPE.OTHER:
            if (!otherWindows.has(otherId)) {
              _AddWin({ otherId, x, y });
            }
            break;
          case MSG_TYPE.REMOVE:
            otherWindows.delete(otherId);
            document.getElementById(otherId)?.remove();
            break;
        }
      };

      function _AddWin({ otherId, x, y }) {
        otherWindows.set(otherId, {
          x,
          y,
        });

        const arrow = document.createElement("div");
        arrow.className = "arrow";
        arrow.id = otherId;
        arrow.style.transform = `translate(-50%, -50%) rotate(${
          (Math.atan2(lastPosition.y - y, lastPosition.x - x) * 180) / Math.PI
        }deg)`;
        arrow.innerHTML = `<img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%221000%22%20height%3D%221000%22%3E%3Cpath%20fill%3D%22currentColor%22%20fill-opacity%3D%22.8%22%20d%3D%22M1005%20530.682H217.267v-61.364H1005v61.363z%22%2F%3E%3Cpath%20fill%3D%22currentColor%22%20fill-opacity%3D%22.8%22%20d%3D%22M217.989%20581.415%204%20503.552l213.989-79.967z%22%2F%3E%3C%2Fsvg%3E" />`;
        document.body.appendChild(arrow);
      }

      function render() {
        requestAnimationFrame(() => {
          const absoluteCenter = {
            x:
              window.screenX /* + (window.outerWidth - window.innerWidth)*/ +
              window.innerWidth / 2,
            y:
              window.screenY +
              (window.outerHeight - window.innerHeight) +
              window.innerHeight / 2,
          };

          if (
            absoluteCenter.x !== lastPosition.x ||
            absoluteCenter.y !== lastPosition.y
          ) {
            lastPosition.x = absoluteCenter.x;
            lastPosition.y = absoluteCenter.y;

            if (_new) {
              _new = false;
              channel.postMessage({
                type: MSG_TYPE.NEW,
                x: lastPosition.x,
                y: lastPosition.y,
                wId,
              });
            } else {
              channel.postMessage({
                type: MSG_TYPE.POSITION,
                x: absoluteCenter.x,
                y: absoluteCenter.y,
                wId,
              });
            }

            otherWindows.forEach(({ x, y }, otherId) => {
              const angle =
                (Math.atan2(lastPosition.y - y, lastPosition.x - x) * 180) /
                Math.PI;
              document.getElementById(
                otherId
              ).style.transform = `translate(-50%, -50%) rotate(${angle}deg)`;
            });
          }

          msg.innerHTML =
            otherWindows.size === 0
              ? "未发现其他窗口"
              : `发现窗口: <ol>${[...otherWindows.keys()]
                  .map((it) => `<li>${it}</li>`)
                  .join("")}</ol>`;
          render();
        });
      }

      render();
      center.style.backgroundColor = color;
    </script>
  </body>
</html>

在线效果:lecepin.github.io/transfer-ac...

Github 项目地址: github.com/lecepin/tra...

原文地址: github.com/lecepin/blo...

相关推荐
江华森22 分钟前
Web漏洞与中间件框架漏洞实战详解
面试
lovingsoft25 分钟前
Chrome Lighthouse 详解
前端·chrome
Qlly35 分钟前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝39 分钟前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦40 分钟前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡1 小时前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI1 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95271 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人1 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring
内存过载1 小时前
用表格模拟甘特图:项目排期的土办法
前端·低代码