Skip to content

Latest commit

 

History

History
386 lines (318 loc) · 10.1 KB

多窗口量子纠缠技术实现.md

File metadata and controls

386 lines (318 loc) · 10.1 KB

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 事件,实现广播效果:

[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;
};

演示:

<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>

image

兼容性:

image

2.1.2 BroadcastChannel 实现

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

[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;
};

演示:

<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>

image

兼容性:

image

2.2 位置获取

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

image

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

3. 实现

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

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

image

d698f00a-75cc-4b27-a9cc-eb8873a116c2

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

const MSG_TYPE = {
  POSITION: 0,
  NEW: 1,
  REMOVE: 2,
  OTHER: 3,
};

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

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

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

完整代码:

<!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>

在线效果:https://lecepin.github.io/transfer-across-tabs-by-BroadcastChannel/

Github 项目地址: https://github.com/lecepin/transfer-across-tabs-by-BroadcastChannel