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

admin 轻心小站 关注 LV.19 运营
发表于程序源码版块 源码分享

最近,一则纯前端实现的“量子纠缠”效果视频在网络上迅速传播开来。视频中,作者在两个窗口中打开了相同的网页,然后在两个窗口中同时移动鼠标。令人惊奇的是,两个窗口中的画面竟然同步移动。不少前端开发者表示:

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

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

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

视频作者开源了一个简化版的实现源码,该项目在 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.screenLeft、window.screenTop、window.innerWidth 和 window.innerHeight 属性来计算每个窗口的形状。

  • 使用 localStorage 来保存每个窗口的形状信息。

  • 当有新的窗口打开时,将其形状信息添加到 localStorage 中。

  • 每隔一段时间,每个窗口都会从 localStorage 中获取所有窗口的形状信息,并根据这些信息更新立方体的数量和位置。

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

文章说明:

本文原创发布于探乎站长论坛,未经许可,禁止转载。

题图来自Unsplash,基于CC0协议

该文观点仅代表作者本人,探乎站长论坛平台仅提供信息存储空间服务。

评论列表 评论
发布评论

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

粉丝

0

关注

0

收藏

0

已有0次打赏