这两天,很多群里都在疯传一个视频,视频演示了纯前端实现的“量子纠缠”效果,不少前端er表示:“前端白学了”。
原视频如下:
全网疯传的前端量子纠缠效果,源码来了!
体验地址:3d example using three.js and multiple windows
视频作者昨晚开源一个简化版的实现源码(截止发文,该项目在 Github 上已获得超过 1k Star),本文就来看看他是怎么实现的!
简化版
根据作者的描述,该项目是使用three.js和localStorage实现的在同一源上设置跨窗口的 3D 场景。
把源码克隆到本地,用 Live Server 启动一下,简化版的效果是这样的:
在线体验:https://bgstaal.github.io/multipleWindow3dScene/
虽然没有原视频那么炫酷,但基本原理应该差不多。
源码包含多个文件,最主要的文件如下:
-
index.html
-
main.js:主文件
-
WindowManager.js:窗口管理
源码
index.html
文件中引入了three.js
的压缩包,以及main.js
:
<!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>
这没啥可说的,下面就来看看 main.js 中都写了点啥。代码如下:
import WindowManager from './WindowManager.js'
const t = THREE;
let camera, scene, renderer, world;
let near, far;
let pixR = window.devicePixelRatio ? window.devicePixelRatio : 1;
let cubes = [];
let sceneOffsetTarget = {x: 0, y: 0};
let sceneOffset = {x: 0, y: 0};
let today = new Date();
today.setHours(0);
today.setMinutes(0);
today.setSeconds(0);
today.setMilliseconds(0);
today = today.getTime();
let internalTime = getTime();
let windowManager;
let initialized = false;
// // 获取从一天开始以来的秒数(以便所有窗口使用相同的时间)
function getTime () {
return (new Date().getTime() - today) / 1000.0;
}
if (new URLSearchParams(window.location.search).get("clear")) {
localStorage.clear();
}
else {
// 在某些浏览器中避免在实际点击URL之前预加载页面内容
document.addEventListener("visibilitychange", () => {
if (document.visibilityState != 'hidden' && !initialized) {
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 t.Orthographi***amera(0, 0, window.innerWidth, window.innerHeight, -10000, 10000);
camera.position.z = 2.5;
near = camera.position.z - .5;
far = camera.position.z + 0.5;
scene = new t.Scene();
scene.background = new t.Color(0.0);
scene.add( camera );
renderer = new t.WebGLRenderer({antialias: true, depthBuffer: true});
renderer.setPixelRatio(pixR);
world = new t.Object3D();
scene.add(world);
renderer.domElement.setAttribute("id", "scene");
document.body.appendChild( renderer.domElement );
}
// 设置窗口管理器的相关配置
function setupWindowManager () {
windowManager = new WindowManager();
windowManager.setWinShapeChangeCallback(updateWindowShape);
windowManager.setWinChangeCallback(windowsUpdated);
let metaData = {foo: "bar"};
// 初始化窗口管理器(windowmanager)并将当前窗口添加到窗口池中。
windowManager.init(metaData);
windowsUpdated();
}
function windowsUpdated () {
updateNumberOfCubes();
}
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 t.Color();
c.setHSL(i * .1, 1.0, .5);
let s = 100 + i * 50;
let cube = new t.Mesh(new t.BoxGeometry(s, s, s), new t.MeshBasicMaterial({color: c , wireframe: true}));
cube.position.x = win.shape.x + (win.shape.w * .5);
cube.position.y = win.shape.y + (win.shape.h * .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 = .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 < cubes.length; i++) {
let cube = cubes[i];
let win = wins[i];
let _t = t;// + i * .2;
let posTarget = {x: win.shape.x + (win.shape.w * .5), y: win.shape.y + (win.shape.h * .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 * .5;
cube.rotation.y = _t * .3;
};
renderer.render(scene, camera);
requestAnimationFrame(render);
}
// 调整渲染器大小以适合窗口大小
function resize () {
let width = window.innerWidth;
let height = window.innerHeight
camera = new t.Orthographi***amera(0, width, 0, height, -10000, 10000);
camera.updateProjectionMatrix();
renderer.setSize( width, height );
}
}
这段代码主要实现以下几点:
-
初始化场景和渲染器:在
setupScene
函数中,设置了一个正交相机、场景和渲染器,并将渲染器的 DOM 元素添加到页面中。 -
初始化窗口管理器:在
setupWindowManager
函数中,创建了一个窗口管理器实例,并初始化了窗口并添加到窗口池中。 -
更新立方体数量和位置:通过
updateNumberOfCubes
函数,根据窗口管理器中窗口的数量和位置信息,动态创建立方体并根据窗口位置更新其在场景中的位置。 -
渲染循环:在
render
函数中,使用requestAnimationFrame
不断循环渲染场景,并根据窗口管理器中窗口的位置更新立方体的位置和旋转。 -
响应窗口大小变化:通过
resize
函数,在窗口大小变化时重新设置相机的宽高比和渲染器的大小,以适应新的窗口尺寸。
接下来看看最核心的实现:WindowManager
,代码如下:
class WindowManager {
#windows;
#count;
#id;
#winData;
#winShapeChangeCallback;
#winChangeCallback;
constructor () {
let that = this;
// 监听 localStorage 是否被其他窗口更改
addEventListener("storage", (event) => {
if (event.key == "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();
});
}
// 检查窗口列表是否有变化
#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;
}
return c;
}
}
// 初始化当前窗口(添加元数据以将自定义数据存储在每个窗口实例中)
init (metaData) {
this.#windows = JSON.parse(localStorage.getItem("windows")) || [];
this.#count= localStorage.getItem("count") || 0;
this.#count++;
this.#id = this.#count;
let shape = this.getWinShape();
this.#winData = {id: this.#id, shape: shape, metaData: metaData};
this.#windows.push(this.#winData);
localStorage.setItem("count", this.#count);
this.updateWindowsLocalStorage();
}
getWinShape () {
let shape = {x: window.screenLeft, y: window.screenTop, w: window.innerWidth, h: window.innerHeight};
return shape;
}
getWindowIndexFromId (id) {
let index = -1;
for (let i = 0; i < this.#windows.length; i++) {
if (this.#windows[i].id == id) index = i;
}
return index;
}
updateWindowsLocalStorage () {
localStorage.setItem("windows", JSON.stringify(this.#windows));
}
update () {
let winShape = this.getWinShape();
if (winShape.x != this.#winData.shape.x ||
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;
if (this.#winShapeChangeCallback) this.#winShapeChangeCallback();
this.updateWindowsLocalStorage();
}
}
setWinShapeChangeCallback (callback) {
this.#winShapeChangeCallback = callback;
}
setWinChangeCallback (callback) {
this.#winChangeCallback = callback;
}
getWindows () {
return this.#windows;
}
getThisWindowData () {
return this.#winData;
}
getThisWindowID () {
return this.#id;
}
}
export default WindowManager;
这段代码定义了一个WindowManager
类,用于管理窗口的创建、更新和删除等操作,并将其作为模块导出。
该类包含以下私有属性:
-
#windows
: 存储所有窗口的数组。 -
#count
: 记录窗口的数量。 -
#id
: 当前窗口的唯一标识符。 -
#winData
: 当前窗口的元数据,包括窗口的形状、自定义数据等。 -
#winShapeChangeCallback
: 当窗口形状发生变化时调用的回调函数。 -
#winChangeCallback
: 当窗口列表发生变化时调用的回调函数。
该类包含以下公共方法:
-
init(metaData)
: 初始化当前窗口,并添加到窗口列表中。 -
getWindows()
: 获取所有窗口的数组。 -
getThisWindowData()
: 获取当前窗口的元数据。 -
getThisWindowID()
: 获取当前窗口的标识符。 -
setWinShapeChangeCallback(callback)
: 设置窗口形状变化时的回调函数。 -
setWinChangeCallback(callback)
: 设置窗口列表变化时的回调函数。 -
update()
: 更新当前窗口的形状信息,并将更新后的窗口列表存储到本地存储中。
可以看到,作者使用window.screenLeft
、window.screenTop
、window.innerWidth
和window.innerHeight
这些属性来计算立方体的位置和大小信息,通过localstorage
来在不同窗口之间共享不同的位置信息。
当新增一个窗口时,就将其保存到localstorage
中,每个窗口使用唯一的id
进行标记,并储存立方体的位置和大小信息。不同浏览器窗口都可以获得所有的窗口信息,以确保实时更新。
当窗口的位置,即screenTop
、screenLeft
发生变化时,就更新立方体。
这里就不再详细解释了,可以查看完整源码:https://github.***/bgstaal/multipleWindow3dScene