前端也能玩“量子纠缠”,附源码

最近,一则纯前端实现的“量子纠缠”效果视频在网络上迅速传播开来。视频中,作者在两个窗口中打开了相同的网页,然后在两个窗口中同时移动鼠标。

令人惊奇的是,两个窗口中的画面竟然同步移动。不少前端开发者表示:“前端白学了,这效果也太神奇了!”

前端也能玩“量子纠缠”,附源码

视频作者开源了一个简化版的实现源码,该项目在 GitHub 上已获得超过 7.4k Star。

完整源码地址:https://github.com/bgstaal/multipleWindow3dScene

下面我们来对最主要的三个文件进行分析:

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
   <title>3d example using three.js and multiple windows</title>
   <script type="text/javascript" src="./three.r124.min.js"></script>
   <style type="text/css">
    
    *
    {
     margin: 0;
     padding: 0;
    }
  
   </style>
  </head>
  <body>
   
   <script type="module" src="./main.js"></script>
  </body>
</html>

这段代码为具有多个窗口的 three.js 应用程序设置了基本结构。 main.js 文件将处理 3D 场景的实际实现和跨多个窗口的同步。

main.js

import WindowManager from './WindowManager.js'; // 导入 WindowManager 类

const THREE = t; 
let camera, scene, renderer, world; // 声明相机、场景、渲染器和世界对象
let near, far; // 声明 near 和 far 平面距离
let pixR = window.devicePixelRatio ? window.devicePixelRatio : 1; // 获取像素密度
let cubes = []; // 创建空数组存放立方体对象
let sceneOffsetTarget = { x: 0, y: 0 }; // 定义场景偏移目标
let sceneOffset = { x: 0, y: 0 }; // 定义场景偏移量

// 获取自今日凌晨以来的时间(以确保所有窗口使用相同的时间)
function getTime() {
  return (new Date().getTime() - today) / 1000.0;
}

// 检查 URL 中是否有 "clear" 参数,如果存在则清除 localStorage
if (new URLSearchParams(window.location.search).get('clear')) {
  localStorage.clear(); 
} else { 
  // 添加事件监听器,当页面可见性发生变化时触发 init 函数
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState !== 'hidden' && !initialized) {
      init(); 
    }
  });

  // 添加 window.onload 事件监听器,当页面加载完成后触发 init 函数
  window.onload = () => {
    if (document.visibilityState !== 'hidden') {
      init(); 
    }
  };

  function init() { // 初始化函数
    initialized = true; // 设置初始化标志位

    // 添加短暂停,因为 window.offsetX 在短时间内会返回错误的值
    setTimeout(() => {
      setupScene(); // 设置场景
      setupWindowManager(); // 初始化窗口管理器
      resize(); // 调整渲染器大小
      updateWindowShape(false); // 更新窗口形状
      render(); // 开始渲染循环
      window.addEventListener('resize', resize); // 添加窗口大小变化事件监听器
    }, 500);
  }

  function setupScene() { // 设置场景函数
    camera = new THREE.OrthographicCamera(0, 0, window.innerWidth, window.innerHeight, -10000, 10000); // 创建正交相机
    camera.position.z = 2.5; // 设置相机位置
    near = camera.position.z - 0.5; // 计算近平面距离
    far = camera.position.z + 0.5; // 计算远平面距离

    scene = new THREE.Scene(); // 创建场景
    scene.background = new THREE.Color(0.0); // 设置场景背景颜色
    scene.add(camera); // 将相机添加到场景中

    renderer = new THREE.WebGLRenderer({ antialias: true, depthBuffer: true }); // 创建 WebGL 渲染器
    renderer.setPixelRatio(pixR); // 设置像素密度

    world = new THREE.Object3D(); // 创建世界对象
    scene.add(world); // 将世界对象添加到场景中

    renderer.domElement.setAttribute('id', 'scene'); // 设置渲染器 DOM 元素的 ID
    document.body.appendChild(renderer.domElement); // 将渲染器 DOM 元素添加到 body 中
  }

  function setupWindowManager() { // 初始化窗口管理器函数
    windowManager = new WindowManager(); // 创建窗口管理器实例
    windowManager.setWinShapeChangeCallback(updateWindowShape); // 设置窗口形状变化回调函数
    windowManager.setWinChangeCallback(windowsUpdated); // 设置窗口更改回调函数

    // 添加自定义元数据到每个窗口实例
    let metaData = { foo: 'bar' }; 

    // 初始化窗口管理器并添加当前窗口
    windowManager.init(metaData); 

    // 调用 windowsUpdated 函数来更新立方体数量
    windowsUpdated(); 
  }

  function windowsUpdated() { 
    updateNumberOfCubes(); 
  }

  function updateNumberOfCubes() { 
    let wins = windowManager.getWindows(); // 获取当前窗口配置

    // 删除所有现有立方体
    cubes.forEach((c) => {
      world.remove(c); // 从世界对象中移除立方体
    });

    cubes = []; // 重置立方
function updateNumberOfCubes() {
  let wins = windowManager.getWindows(); // 获取当前窗口配置

  // 删除所有现有立方体
  cubes.forEach((c) => {
    world.remove(c); // 从世界对象中移除立方体
  });

  cubes = []; // 重置立方体数组

  // 创建新的立方体
  for (let i = 0; i < wins.length; i++) {
    let win = wins[i];

    // 生成随机颜色
    let c = new THREE.Color();
    c.setHSL(i * 0.1, 1.0, 0.5);

    // 生成立方体
    let cube = new THREE.Mesh(new THREE.BoxGeometry(100 + i * 50, 100 + i * 50, 100 + i * 50), new THREE.MeshBasicMaterial({ color: c, wireframe: true }));
    cube.position.x = win.shape.x + (win.shape.w * 0.5);
    cube.position.y = win.shape.y + (win.shape.h * 0.5);

    world.add(cube); // 将立方体添加到世界对象中
    cubes.push(cube); // 将立方体添加到立方体数组中
  }
}

function updateWindowShape(easing = true) {
  // 将场景偏移量设置为当前窗口的偏移量
  sceneOffsetTarget = { x: -window.screenX, y: -window.screenY };
  if (!easing) {
    sceneOffset = sceneOffsetTarget; // 立即更新场景偏移量
  }
}

function render() {
  // 获取当前时间
  let t = getTime();

  // 更新窗口管理器
  windowManager.update();

  // 计算场景偏移量
  let falloff = 0.05;
  sceneOffset.x = sceneOffset.x + ((sceneOffsetTarget.x - sceneOffset.x) * falloff);
  sceneOffset.y = sceneOffset.y + ((sceneOffsetTarget.y - sceneOffset.y) * falloff);

  // 设置世界对象的偏移量
  world.position.x = sceneOffset.x;
  world.position.y = sceneOffset.y;

  // 遍历所有窗口
  let wins = windowManager.getWindows();
  for (let i = 0; i < wins.length; i++) {
    let cube = cubes[i];
    let win = wins[i];
    let _t = t; // + i * 0.2;

    let posTarget = { x: win.shape.x + (win.shape.w * 0.5), y: win.shape.y + (win.shape.h * 0.5) };

    cube.position.x = cube.position.x + ((posTarget.x - cube.position.x) * falloff);
    cube.position.y = cube.position.y + ((posTarget.y - cube.position.y) * falloff);
    cube.rotation.x = _t * 0.5;
    cube.rotation.y = _t * 0.3;
  }

  // 渲染场景
  renderer.render(scene, camera);

  // 请求下一次渲染
  requestAnimationFrame(render);
}

// 调整渲染器大小
function resize() {
  let width = window.innerWidth;
  let height = window.innerHeight;

  camera = new THREE.OrthographicCamera(0, width, 0, height, -10000, 10000);
  camera.updateProjectionMatrix();
  renderer.setSize(width, height);
}

该代码使用 THREE.js 库来创建一个简单的场景,其中包含多个立方体。立方体的数量和位置由窗口管理器控制。窗口管理器负责跟踪所有打开的窗口,并根据每个窗口的大小和位置更新立方体的数量和位置。

  • setupScene() 函数设置场景,包括相机、场景、渲染器和世界对象。
  • setupWindowManager() 函数初始化窗口管理器,并添加当前窗口到窗口管理器中。
  • windowsUpdated() 函数更新立方体数量,根据当前窗口的数量。
  • updateNumberOfCubes() 函数删除所有现有立方体,并添加新的立方体,数量与当前窗口数量相同。
  • render() 函数渲染场景,包括计算场景偏移量、遍历所有窗口并更新立方体位置、渲染场景。
  • resize() 函数调整渲染器大小,使其适应窗口大小。

WindowManager.js

class WindowManager 
{
 #windows; // 存储所有窗口信息的数组
 #count; // 窗口计数器
 #id; // 当前窗口的ID
 #winData; // 当前窗口的数据
 #winShapeChangeCallback; // 窗口形状更改回调函数
 #winChangeCallback; // 窗口更改回调函数
 
 constructor ()
 {
  let that = this; 

  // 监听 localStorage 变化事件,当其他窗口修改 localStorage 时触发
  addEventListener("storage", (event) => 
  {
   if (event.key == "windows") // 判断是否为 "windows" 键变化
   {
    let newWindows = JSON.parse(event.newValue); // 解析新窗口数据
    let winChange = that.#didWindowsChange(that.#windows, newWindows); // 检查窗口数据是否变化

    that.#windows = newWindows; // 更新窗口数据

    if (winChange) // 如果窗口数据有变化
    {
     if (that.#winChangeCallback) that.#winChangeCallback(); // 调用窗口更改回调函数
    }
   }
  });

  // 监听当前窗口关闭事件
  window.addEventListener('beforeunload', function (e) 
  {
   let index = that.getWindowIndexFromId(that.#id); // 获取当前窗口在窗口列表中的索引

   // 从窗口列表中删除当前窗口并更新 localStorage
   that.#windows.splice(index, 1); // 删除窗口数据
   that.updateWindowsLocalStorage(); // 更新窗口数据到 localStorage
  });
 }

 // 检查窗口数据是否发生变化
 #didWindowsChange (pWins, nWins)
 {
  if (pWins.length != nWins.length) // 窗口数量不相等
  {
   return true; // 窗口数据发生变化
  }
  else
  {
   let c = false; // 默认没有变化

   for (let i = 0; i < pWins.length; i++) // 遍历所有窗口
   {
    if (pWins[i].id != nWins[i].id) c = true; // 窗口ID不相等
   }

   return c; // 如果存在窗口ID不一致,则窗口数据发生变化
  }
 }

 // 初始化当前窗口,并为每个窗口存储自定义元数据
 init (metaData)
 {
  this.#windows = JSON.parse(localStorage.getItem("windows")) || []; // 获取窗口数据,如果不存在则初始化为空数组
  this.#count= localStorage.getItem("count") || 0; // 获取窗口计数器,如果不存在则初始化为0
  this.#count++; // 窗口计数器加1

  this.#id = this.#count; // 设置当前窗口ID
  let shape = this.getWinShape(); // 获取当前窗口形状
  this.#winData = {id: this.#id, shape: shape, metaData: metaData}; // 创建当前窗口数据对象
  this.#windows.push(this.#winData); // 将当前窗口数据添加到窗口列表

  localStorage.setItem("count", this.#count); // 更新窗口计数器到 localStorage
  this.updateWindowsLocalStorage(); // 更新窗口数据到 localStorage
 }

 getWinShape ()
 {
  let shape = {x: window.screenLeft, y: window.screenTop, w: window.innerWidth, h: window.innerHeight}; // 获取当前窗口形状
  return shape;
 }

 getWindowIndexFromId (id) // 查找指定ID的窗口索引
 {
  let index = -1; // 初始化索引为-1

  for (let i = 0; i < this.#windows.length; i++) // 遍历所有窗口
  {
   if (this.#windows[i].id == id) index = i; // 如果窗口ID匹配,更新索引
  }

  return index; // 返回索引
 }

 updateWindowsLocalStorage () // 更新窗口数据到 localStorage
 {
  localStorage.setItem("windows", JSON.stringify(this.#windows)); // 将窗口数据转换为字符串并存储到 localStorage
 }

 update () // 更新当前窗口数据
 {
  let winShape = this.getWinShape(); // 获取当前窗口形状

  // 检查窗口形状是否发生变化
  if (winShape.x != this.#winData.shape.x ||
   winShape
   winShape.y != this.#winData.shape.y ||
   winShape.w != this.#winData.shape.w ||
   winShape.h != this.#winData.shape.h)
  {

   this.#winData.shape = winShape; // 更新当前窗口数据的形状

   let index = this.getWindowIndexFromId(this.#id); // 获取当前窗口在窗口列表中的索引
   this.#windows[index].shape = winShape; // 更新窗口列表中当前窗口的形状

   //console.log(windows);
   if (this.#winShapeChangeCallback) this.#winShapeChangeCallback(); // 调用窗口形状更改回调函数
   this.updateWindowsLocalStorage(); // 更新窗口数据到 localStorage
  }
 }

 setWinShapeChangeCallback (callback) // 设置窗口形状更改回调函数
 {
  this.#winShapeChangeCallback = callback;
 }

 setWinChangeCallback (callback) // 设置窗口更改回调函数
 {
  this.#winChangeCallback = callback;
 }

 getWindows () // 获取所有窗口
 {
  return this.#windows;
 }

 getThisWindowData () // 获取当前窗口数据
 {
  return this.#winData;
 }

 getThisWindowID () // 获取当前窗口ID
 {
  return this.#id;
 }
}

export default WindowManager;

WindowManager 类提供了以下方法:

  • init():初始化当前窗口,并添加自定义元数据
  • getWinShape():获取当前窗口的形状
  • getWindowIndexFromId():获取指定 ID 的窗口在数组中的索引
  • updateWindowsLocalStorage():更新 localStorage 中的窗口数据
  • update():更新窗口形状
  • setWinShapeChangeCallback():设置窗口形状变化回调函数
  • setWinChangeCallback():设置窗口变化回调函数
  • getWindows():获取所有窗口数据
  • getThisWindowData():获取当前窗口数据
  • getThisWindowID():获取当前窗口的 ID

在一个立方体展示应用程序中,作者使用了以下几种方式来跟踪所有打开的窗口,并根据每个窗口的大小和位置更新立方体的数量和位置:

  • 使用 window.screenLeftwindow.screenTopwindow.innerWidth 和 window.innerHeight 属性来计算每个窗口的形状。
  • 使用 localStorage 来保存每个窗口的形状信息。
  • 当有新的窗口打开时,将其形状信息添加到 localStorage 中。
  • 每隔一段时间,每个窗口都会从 localStorage 中获取所有窗口的形状信息,并根据这些信息更新立方体的数量和位置。

这种方法可以有效地跟踪所有打开的窗口,并确保每个窗口都看到相同的立方体数量和位置。当窗口的位置,即screenTopscreenLeft发生变化时,就更新立方体。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

给TA打赏
共{{data.count}}人
人已打赏
技术教程

Windows 11跳过联网验证和绕过 Mircosoft 账户登录要求

2023-11-24 15:58:33

技术教程

PHP 8.3 大升级:20 个新特性全面解析

2023-11-28 14:23:17

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索