【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 application
        const 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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ustinian_310

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值