A Simplified Front‑End Multi‑Window 3D Scene Using three.js and localStorage

This article explains how a pure‑frontend implementation using three.js and localStorage creates a synchronized 3D scene across multiple browser windows, detailing the project structure, core code files, and the window‑management logic that enables cross‑window coordination without a back‑end.

IT Services Circle
IT Services Circle
IT Services Circle
A Simplified Front‑End Multi‑Window 3D Scene Using three.js and localStorage

Recently a video showing a pure‑frontend "quantum entanglement" effect went viral; the author open‑sourced a simplified version built with three.js and localStorage that creates a 3D scene shared across multiple browser windows.

The project consists of three main files: index.html, main.js and WindowManager.js. The HTML loads the three.js library and the main module, while the JavaScript sets up an orthographic camera, scene, renderer and a window manager that stores each window’s shape in localStorage.

<!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>

The main.js script imports the window manager, defines global variables, initializes the scene, creates a cube for each window, updates positions based on window coordinates, and runs a render loop with requestAnimationFrame. It also handles visibility changes, resizing, and synchronises window data through the manager.

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,0,0,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 {
  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 t.OrthographicCamera(0, window.innerWidth, 0, window.innerHeight, -10000, 10000);
  camera.position.z = 2.5;
  near = camera.position.z - .5;
  far = camera.position.z + .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);
  const metaData = {foo:"bar"};
  windowManager.init(metaData);
  windowsUpdated();
}

function windowsUpdated(){
  updateNumberOfCubes();
}

function updateNumberOfCubes(){
  const wins = windowManager.getWindows();
  cubes.forEach(c => world.remove(c));
  cubes = [];
  for(let i=0;i<wins.length;i++){
    const win = wins[i];
    const c = new t.Color();
    c.setHSL(i*.1,1.0,.5);
    const s = 100 + i*50;
    const 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(){
  const t = getTime();
  windowManager.update();
  const falloff = .05;
  sceneOffset.x += (sceneOffsetTarget.x - sceneOffset.x) * falloff;
  sceneOffset.y += (sceneOffsetTarget.y - sceneOffset.y) * falloff;
  world.position.x = sceneOffset.x;
  world.position.y = sceneOffset.y;
  const wins = windowManager.getWindows();
  for(let i=0;i<cubes.length;i++){
    const cube = cubes[i];
    const win = wins[i];
    const posTarget = {x: win.shape.x + (win.shape.w*.5), y: win.shape.y + (win.shape.h*.5)};
    cube.position.x += (posTarget.x - cube.position.x) * falloff;
    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(){
  const width = window.innerWidth;
  const height = window.innerHeight;
  camera = new t.OrthographicCamera(0, width, 0, height, -10000, 10000);
  camera.updateProjectionMatrix();
  renderer.setSize(width, height);
}

The core WindowManager class tracks all windows, assigns unique IDs, listens for the storage event to detect changes from other tabs, and provides callbacks for shape changes and window list updates. It stores the window list in localStorage so that every tab can read the same state.

class WindowManager {
  #windows;
  #count;
  #id;
  #winData;
  #winShapeChangeCallback;
  #winChangeCallback;

  constructor(){
    addEventListener("storage", (event) => {
      if(event.key == "windows"){
        const newWindows = JSON.parse(event.newValue);
        const winChange = this.#didWindowsChange(this.#windows, newWindows);
        this.#windows = newWindows;
        if(winChange && this.#winChangeCallback) this.#winChangeCallback();
      }
    });
    window.addEventListener('beforeunload', (e) => {
      const index = this.getWindowIndexFromId(this.#id);
      this.#windows.splice(index,1);
      this.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;
    const shape = this.getWinShape();
    this.#winData = {id:this.#id, shape, 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){
    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(){
    const 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;
      const 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;

By opening the project with a local server (e.g., Live Server) you can see multiple cubes moving in sync across different browser windows, demonstrating how pure front‑end code can achieve cross‑window coordination without a back‑end.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Three.js3DlocalStoragewindow management
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.