最近,一则纯前端实现的“量子纠缠”效果视频在网络上迅速传播开来。视频中,作者在两个窗口中打开了相同的网页,然后在两个窗口中同时移动鼠标。令人惊奇的是,两个窗口中的画面竟然同步移动。不少前端开发者表示:
最近,一则纯前端实现的“量子纠缠”效果视频在网络上迅速传播开来。视频中,作者在两个窗口中打开了相同的网页,然后在两个窗口中同时移动鼠标。
令人惊奇的是,两个窗口中的画面竟然同步移动。不少前端开发者表示:“前端白学了,这效果也太神奇了!”
视频作者开源了一个简化版的实现源码,该项目在 GitHub 上已获得超过 7.4k Star。
完整源码地址:https://github.com/bgstaal/multipleWindow3dScene
下面我们来对最主要的三个文件进行分析:
<!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 场景的实际实现和跨多个窗口的同步。
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() 函数调整渲染器大小,使其适应窗口大小。
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发生变化时,就更新立方体。
粉丝
0
关注
0
收藏
0