当前位置: 首页 > news >正文

【JavaScript】利用`localStorage`实现多窗口数据交互同步【附完整源码】

3D多窗口交互系统:点线虚线与引力场效果详解

一、项目概述

这个项目创建了一个3D多窗口交互系统,当用户打开多个窗口时,会在每个窗口中显示旋转的3D立方体,并在两个窗口之间生成动态的引力场效果线。这些引力场线以点状虚线呈现,并带有垂直于主线的短线装饰。

1. 打开一个网页的效果如下:

在这里插入图片描述

1. 打开两个网页的效果如下(网页重合时):

在这里插入图片描述

1. 打开两个网页的效果如下(网页不重合时):

在这里插入图片描述

二、核心功能解析

1. 多窗口管理机制

1.1 WindowManager类

这个类是项目的核心控制器,负责管理所有关联窗口的状态:

class WindowManager {#windows;       // 存储所有窗口数据#count;         // 窗口计数器#id;           // 当前窗口ID#winData;       // 当前窗口数据#winShapeChangeCallback; // 窗口形状变化回调#winChangeCallback;      // 窗口数量变化回调
}
1.2 窗口同步原理
  • localStorage:所有窗口共享的存储空间
  • storage事件监听:当任何窗口更新数据时,其他窗口会收到通知
  • beforeunload事件:窗口关闭时自动清理自身数据

2. 3D场景搭建

2.1 Three.js基础设置
function setupScene() {// 使用正交相机(适合2D/3D混合场景)camera = new THREE.OrthographicCamera(0, 0, window.innerWidth, window.innerHeight, -10000, 10000);// 创建场景并设置黑色背景scene = new THREE.Scene();scene.background = new THREE.Color(0.0);// 创建WebGL渲染器renderer = new THREE.WebGLRenderer({antialias: true, depthBuffer: true});// 创建世界容器(方便整体移动)world = new THREE.Object3D();scene.add(world);
}
2.2 立方体创建逻辑

每个窗口对应一个彩色线框立方体:

let color = new THREE.Color();
color.setHSL(i * 0.1, 1.0, 0.5); // 使用HSL色彩空间,按窗口顺序分配颜色let cube = new THREE.Mesh(new THREE.BoxGeometry(size, size, size), new THREE.MeshBasicMaterial({color: color, wireframe: true})
);

3. 引力场效果实现

3.1 引力场主曲线
// 创建点状虚线效果
const mainMaterial = new THREE.LineDashedMaterial({color: color,dashSize: 3,    // 虚线段的长度gapSize: 5,     // 间隔的长度linewidth: 2,transparent: true,opacity: 0.8
});
3.2 动态曲线路径

根据窗口距离计算曲线强度:

const strength = Math.min(1, 2000 / distance); // 距离越近强度越大// 创建波浪形曲线路径
const curveAmount = 50 * (1 - strength) * Math.sin(t * Math.PI);
mainPoints.push(new THREE.Vector3(x + curveAmount * Math.cos(t * Math.PI * 4),y + curveAmount * Math.sin(t * Math.PI * 4),0
));
3.3 顶点装饰短线
// 计算垂直于曲线的方向
const tangentX = nextPoint.x - point.x;
const tangentY = nextPoint.y - point.y;
const normalX = -tangentY / tangentLength; // 法线向量X
const normalY = tangentX / tangentLength;  // 法线向量Y// 创建垂直于主线的短线
vertexPoints.push(new THREE.Vector3(point.x - normalX * vertexLineLength,point.y - normalY * vertexLineLength,0
));

三、关键技术点详解

1. 窗口位置同步

1.1 实时更新机制
function updateWindowShape(easing = true) {// 计算场景偏移目标值(使所有窗口内容在全局坐标系中正确对齐)sceneOffsetTarget = {x: -window.screenX, y: -window.screenY};if (!easing) sceneOffset = sceneOffsetTarget;
}
1.2 平滑过渡效果
// 使用缓动算法实现平滑移动
const falloff = 0.05;
sceneOffset.x = sceneOffset.x + ((sceneOffsetTarget.x - sceneOffset.x) * falloff);
sceneOffset.y = sceneOffset.y + ((sceneOffsetTarget.y - sceneOffset.y) * falloff);

2. 立方体动画

cube.rotation.x = time * 0.5; // 绕X轴旋转
cube.rotation.y = time * 0.3; // 绕Y轴旋转// 平滑移动到目标位置
cube.position.x = cube.position.x + (targetPos.x - cube.position.x) * falloff;
cube.position.y = cube.position.y + (targetPos.y - cube.position.y) * falloff;

3. 响应式设计

function resize() {// 根据窗口大小调整相机和渲染器const width = window.innerWidth;const height = window.innerHeight;camera = new THREE.OrthographicCamera(0, width, 0, height, -10000, 10000);renderer.setSize(width, height);
}

四、小白使用指南

1. 如何运行项目

  1. 将代码保存为HTML文件
  2. 右击选择在浏览器中打开该文件
  3. 复制地址栏URL并在新窗口中打开(至少打开2个窗口查看完整效果)

2. 调试技巧

  • 添加?clear参数重置所有存储数据
  • F12打开开发者工具查看控制台日志

3. 自定义修改建议

  1. 修改立方体样式:调整BoxGeometry参数和材质属性
  2. 改变引力场效果:修改LineDashedMaterial的dashSize/gapSize
  3. 添加更多交互:在render函数中加入鼠标/键盘交互逻辑

五、总结与扩展

展示了如何利用现代Web技术创建复杂的多窗口交互体验。关键创新点包括:

  1. 使用localStorage实现跨窗口通信
  2. 引用Three.js实现高性能3D渲染
  3. 利用点状虚线模拟引力场实现可视化

可扩展方向:

  • 添加窗口碰撞检测和物理效果

六、【源代码】

<!DOCTYPE html>
<html lang="en">
<head><title>3D Multi-Window with Dot-Dashed Gravity Fields</title><script src="https://cdn.jsdelivr.net/npm/three@0.124.0/build/three.min.js"></script><style>* {margin: 0;padding: 0;}#scene {position: fixed;top: 0;left: 0;width: 100%;height: 100%;}</style>
</head>
<body><script>class WindowManager {#windows;#count;#id;#winData;#winShapeChangeCallback;#winChangeCallback;constructor() {let that = this;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);that.#windows.splice(index, 1);that.updateWindowsLocalStorage();});}#didWindowsChange(pWins, nWins) {if (pWins.length != nWins.length) return true;for (let i = 0; i < pWins.length; i++) {if (pWins[i].id != nWins[i].id) return true;}return false;}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() {return {x: window.screenLeft, y: window.screenTop, w: window.innerWidth, h: window.innerHeight};}getWindowIndexFromId(id) {for (let i = 0; i < this.#windows.length; i++) {if (this.#windows[i].id == id) return i;}return -1;}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;}}// Main applicationconst THREE = window.THREE;let camera, scene, renderer, world;let pixR = window.devicePixelRatio ? window.devicePixelRatio : 1;let cubes = [];let gravityFields = [];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 windowManager;let initialized = false;function getTime() {return (new Date().getTime() - today) / 1000.0;}if (new URLSearchParams(window.location.search).get("clear")) {localStorage.clear();} else {    document.addEventListener("visibilitychange", () => {if (document.visibilityState != 'hidden' && !initialized) {init();}});window.onload = () => {if (document.visibilityState != 'hidden') {init();}};function init() {initialized = true;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;scene = new THREE.Scene();scene.background = new THREE.Color(0.0);scene.add(camera);renderer = new THREE.WebGLRenderer({antialias: true, depthBuffer: true});renderer.setPixelRatio(pixR);world = new THREE.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.init(metaData);windowsUpdated();}function windowsUpdated() {updateNumberOfCubes();updateGravityFields();}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 color = new THREE.Color();color.setHSL(i * 0.1, 1.0, 0.5);let size = 100 + i * 50;let cube = new THREE.Mesh(new THREE.BoxGeometry(size, size, size), new THREE.MeshBasicMaterial({color: color, 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 updateGravityFields() {gravityFields.forEach((field) => {world.remove(field);});gravityFields = [];let wins = windowManager.getWindows();if (wins.length === 2) {const win1 = wins[0];const win2 = wins[1];const pos1 = {x: win1.shape.x + (win1.shape.w * 0.5),y: win1.shape.y + (win1.shape.h * 0.5)};const pos2 = {x: win2.shape.x + (win2.shape.w * 0.5),y: win2.shape.y + (win2.shape.h * 0.5)};const dx = pos2.x - pos1.x;const dy = pos2.y - pos1.y;const distance = Math.sqrt(dx * dx + dy * dy);// 创建主引力场线(点状虚线)const mainPoints = [];const segments = 30;const strength = Math.min(1, 2000 / distance);const color = new THREE.Color();color.setHSL(0.7 * (1 - strength), 1.0, 0.5);// 创建主曲线路径for (let i = 0; i <= segments; i++) {const t = i / segments;const x = pos1.x + t * dx;const y = pos1.y + t * dy;const curveAmount = 50 * (1 - strength) * Math.sin(t * Math.PI);mainPoints.push(new THREE.Vector3(x + curveAmount * Math.cos(t * Math.PI * 4),y + curveAmount * Math.sin(t * Math.PI * 4),0));}// 主引力场线(点状虚线)const mainGeometry = new THREE.BufferGeometry().setFromPoints(mainPoints);const mainMaterial = new THREE.LineDashedMaterial({color: color,dashSize: 3,gapSize: 5,linewidth: 2,transparent: true,opacity: 0.8});const mainLine = new THREE.Line(mainGeometry, mainMaterial);mainLine.computeLineDistances();world.add(mainLine);gravityFields.push(mainLine);// 添加顶点虚线(从每个顶点向外辐射的短线)const vertexSegmentCount = 8;const vertexLineLength = 20 * strength;for (let i = 0; i <= segments; i += 2) {if (i === 0 || i === segments) continue; // 跳过端点const point = mainPoints[i];const nextPoint = mainPoints[i+1];// 计算切线方向const tangentX = nextPoint.x - point.x;const tangentY = nextPoint.y - point.y;const tangentLength = Math.sqrt(tangentX * tangentX + tangentY * tangentY);const normalX = -tangentY / tangentLength;const normalY = tangentX / tangentLength;// 创建垂直于曲线的短线const vertexPoints = [];vertexPoints.push(new THREE.Vector3(point.x - normalX * vertexLineLength,point.y - normalY * vertexLineLength,0));vertexPoints.push(new THREE.Vector3(point.x + normalX * vertexLineLength,point.y + normalY * vertexLineLength,0));const vertexGeometry = new THREE.BufferGeometry().setFromPoints(vertexPoints);const vertexMaterial = new THREE.LineDashedMaterial({color: color,dashSize: 1,gapSize: 2,linewidth: 1,transparent: true,opacity: 0.6});const vertexLine = new THREE.Line(vertexGeometry, vertexMaterial);vertexLine.computeLineDistances();world.add(vertexLine);gravityFields.push(vertexLine);}}}function updateWindowShape(easing = true) {sceneOffsetTarget = {x: -window.screenX, y: -window.screenY};if (!easing) sceneOffset = sceneOffsetTarget;}function render() {let time = getTime();windowManager.update();const 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 < cubes.length; i++) {let cube = cubes[i];let win = wins[i];let targetPos = {x: win.shape.x + (win.shape.w * 0.5),y: win.shape.y + (win.shape.h * 0.5)};cube.position.x = cube.position.x + (targetPos.x - cube.position.x) * falloff;cube.position.y = cube.position.y + (targetPos.y - cube.position.y) * falloff;cube.rotation.x = time * 0.5;cube.rotation.y = time * 0.3;};if (wins.length === 2) {updateGravityFields();}renderer.render(scene, camera);requestAnimationFrame(render);}function resize() {const width = window.innerWidth;const height = window.innerHeight;camera = new THREE.OrthographicCamera(0, width, 0, height, -10000, 10000);camera.updateProjectionMatrix();renderer.setSize(width, height);}}</script>
</body>
</html>
http://www.xdnf.cn/news/942607.html

相关文章:

  • OD 算法题 B卷【删除字符串中出现次数最少的字符】
  • 如何禁用windows server系统自动更新并防止自动重启
  • 推理式奖励模型:使用自然语言反馈改进强化学习效果
  • 卫星接收天线G/T值怎么计算?附G/T计算excel表格链接
  • 打卡day48
  • 12.7Swing控件5 JProgressBar
  • Spring AI中使用ChatMemory实现会话记忆功能
  • 算法打卡第18天
  • 【CUDA 】第5章 共享内存和常量内存——5.3减少全局内存访问(2)
  • Linux 环境配置
  • 【立体匹配】:双目立体匹配SGBM:(1)运行
  • 深入解析JavaScript构造函数与原型链
  • JavaScript 自定义对象详解
  • AI医生时代来临!o1模型在医疗诊断中超越人类医生
  • 查看进程线程的方法
  • 进制符号表示
  • 【阿里巴巴 x 浙江大学】信息与交互设计 - 信息设计漫谈
  • AIGC 基础篇 Python基础 02
  • MS8312A 车规 精密、低噪、CMOS、轨到轨输入输出运算放大器,用于传感器、条形扫描器
  • arxir网址自动转向国内镜像
  • 【DTOF传感器】光子飞行时间传感技术
  • 通信之光端机
  • 苏超 - 江苏省城市足球联赛
  • Angular中Webpack与ngx-build-plus 浅学
  • 【刷题模板】链表、堆栈
  • AI+预测3D新模型百十个定位预测+胆码预测+去和尾2025年6月8日第102弹
  • 06. C#入门系列【自定义类型】:从青铜到王者的进阶之路
  • 星耀8上市品鉴暨北京中和吉晟吉利银河用户中心开业媒体见面会
  • 免费批量去水印工具 - 针对文心一言生成图片
  • DDR供电设计中的VTT与VREF作用和区别