├── .gitignore ├── README.md ├── WindowManager.js ├── index.html ├── main.js ├── three-LICENSE └── three.r124.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | deploy 4 | screens* 5 | npm-debug.log 6 | *.png 7 | .env 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multiple Window 3D Scene using Three.js 2 | 3 | ## Introduction 4 | This project demonstrates a unique approach to creating and managing a 3D scene across multiple browser windows using Three.js and localStorage. It's designed for developers interested in advanced web graphics and window management techniques. 5 | 6 | ## Features 7 | - 3D scene creation and rendering with Three.js. 8 | - Synchronization of 3D scenes across multiple browser windows. 9 | - Dynamic window management and state synchronization using localStorage. 10 | 11 | ## Installation 12 | Clone the repository and open `index.html` in your browser to start exploring the 3D scene. 13 | 14 | ``` 15 | git clone https://github.com/bgstaal/multipleWindow3dScene 16 | ``` 17 | ## Usage 18 | The main application logic is contained within `main.js` and `WindowManager.js`. The 3D scene is rendered in `index.html`, which serves as the entry point of the application. 19 | 20 | ## Structure and Components 21 | - `index.html`: Entry point that sets up the HTML structure and includes the Three.js library and the main script. 22 | - `WindowManager.js`: Core class managing window creation, synchronization, and state management across multiple windows. 23 | - `main.js`: Contains the logic for initializing the 3D scene, handling window events, and rendering the scene. 24 | - `three.r124.min.js`: Minified version of the Three.js library used for 3D graphics rendering. 25 | 26 | ## Detailed Functionality 27 | - `WindowManager.js` handles the lifecycle of multiple browser windows, including creation, synchronization, and removal. It uses localStorage to maintain state across windows. 28 | - `main.js` initializes the 3D scene using Three.js, manages the window's resize events, and updates the scene based on window interactions. 29 | 30 | ## Contributing 31 | Contributions to enhance or expand the project are welcome. Feel free to fork the repository, make changes, and submit pull requests. 32 | 33 | ## License 34 | This project is open-sourced under the MIT License. 35 | 36 | ## Acknowledgments 37 | - The Three.js team for their comprehensive 3D library. 38 | - x.com/didntdrinkwater for this readme. 39 | 40 | ## Contact 41 | For more information and updates, follow [@_nonfigurativ_](https://twitter.com/_nonfigurativ_) on Twitter. 42 | -------------------------------------------------------------------------------- /WindowManager.js: -------------------------------------------------------------------------------- 1 | class WindowManager 2 | { 3 | #windows; 4 | #count; 5 | #id; 6 | #winData; 7 | #winShapeChangeCallback; 8 | #winChangeCallback; 9 | 10 | constructor () 11 | { 12 | let that = this; 13 | 14 | // event listener for when localStorage is changed from another window 15 | addEventListener("storage", (event) => 16 | { 17 | if (event.key == "windows") 18 | { 19 | let newWindows = JSON.parse(event.newValue); 20 | let winChange = that.#didWindowsChange(that.#windows, newWindows); 21 | 22 | that.#windows = newWindows; 23 | 24 | if (winChange) 25 | { 26 | if (that.#winChangeCallback) that.#winChangeCallback(); 27 | } 28 | } 29 | }); 30 | 31 | // event listener for when current window is about to ble closed 32 | window.addEventListener('beforeunload', function (e) 33 | { 34 | let index = that.getWindowIndexFromId(that.#id); 35 | 36 | //remove this window from the list and update local storage 37 | that.#windows.splice(index, 1); 38 | that.updateWindowsLocalStorage(); 39 | }); 40 | } 41 | 42 | // check if theres any changes to the window list 43 | #didWindowsChange (pWins, nWins) 44 | { 45 | if (pWins.length != nWins.length) 46 | { 47 | return true; 48 | } 49 | else 50 | { 51 | let c = false; 52 | 53 | for (let i = 0; i < pWins.length; i++) 54 | { 55 | if (pWins[i].id != nWins[i].id) c = true; 56 | } 57 | 58 | return c; 59 | } 60 | } 61 | 62 | // initiate current window (add metadata for custom data to store with each window instance) 63 | init (metaData) 64 | { 65 | this.#windows = JSON.parse(localStorage.getItem("windows")) || []; 66 | this.#count= localStorage.getItem("count") || 0; 67 | this.#count++; 68 | 69 | this.#id = this.#count; 70 | let shape = this.getWinShape(); 71 | this.#winData = {id: this.#id, shape: shape, metaData: metaData}; 72 | this.#windows.push(this.#winData); 73 | 74 | localStorage.setItem("count", this.#count); 75 | this.updateWindowsLocalStorage(); 76 | } 77 | 78 | getWinShape () 79 | { 80 | let shape = {x: window.screenLeft, y: window.screenTop, w: window.innerWidth, h: window.innerHeight}; 81 | return shape; 82 | } 83 | 84 | getWindowIndexFromId (id) 85 | { 86 | let index = -1; 87 | 88 | for (let i = 0; i < this.#windows.length; i++) 89 | { 90 | if (this.#windows[i].id == id) index = i; 91 | } 92 | 93 | return index; 94 | } 95 | 96 | updateWindowsLocalStorage () 97 | { 98 | localStorage.setItem("windows", JSON.stringify(this.#windows)); 99 | } 100 | 101 | update () 102 | { 103 | //console.log(step); 104 | let winShape = this.getWinShape(); 105 | 106 | //console.log(winShape.x, winShape.y); 107 | 108 | if (winShape.x != this.#winData.shape.x || 109 | winShape.y != this.#winData.shape.y || 110 | winShape.w != this.#winData.shape.w || 111 | winShape.h != this.#winData.shape.h) 112 | { 113 | 114 | this.#winData.shape = winShape; 115 | 116 | let index = this.getWindowIndexFromId(this.#id); 117 | this.#windows[index].shape = winShape; 118 | 119 | //console.log(windows); 120 | if (this.#winShapeChangeCallback) this.#winShapeChangeCallback(); 121 | this.updateWindowsLocalStorage(); 122 | } 123 | } 124 | 125 | setWinShapeChangeCallback (callback) 126 | { 127 | this.#winShapeChangeCallback = callback; 128 | } 129 | 130 | setWinChangeCallback (callback) 131 | { 132 | this.#winChangeCallback = callback; 133 | } 134 | 135 | getWindows () 136 | { 137 | return this.#windows; 138 | } 139 | 140 | getThisWindowData () 141 | { 142 | return this.#winData; 143 | } 144 | 145 | getThisWindowID () 146 | { 147 | return this.#id; 148 | } 149 | } 150 | 151 | export default WindowManager; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 3d example using three.js and multiple windows 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import WindowManager from './WindowManager.js' 2 | 3 | 4 | 5 | const t = THREE; 6 | let camera, scene, renderer, world; 7 | let near, far; 8 | let pixR = window.devicePixelRatio ? window.devicePixelRatio : 1; 9 | let cubes = []; 10 | let sceneOffsetTarget = {x: 0, y: 0}; 11 | let sceneOffset = {x: 0, y: 0}; 12 | 13 | let today = new Date(); 14 | today.setHours(0); 15 | today.setMinutes(0); 16 | today.setSeconds(0); 17 | today.setMilliseconds(0); 18 | today = today.getTime(); 19 | 20 | let internalTime = getTime(); 21 | let windowManager; 22 | let initialized = false; 23 | 24 | // get time in seconds since beginning of the day (so that all windows use the same time) 25 | function getTime () 26 | { 27 | return (new Date().getTime() - today) / 1000.0; 28 | } 29 | 30 | 31 | if (new URLSearchParams(window.location.search).get("clear")) 32 | { 33 | localStorage.clear(); 34 | } 35 | else 36 | { 37 | // this code is essential to circumvent that some browsers preload the content of some pages before you actually hit the url 38 | document.addEventListener("visibilitychange", () => 39 | { 40 | if (document.visibilityState != 'hidden' && !initialized) 41 | { 42 | init(); 43 | } 44 | }); 45 | 46 | window.onload = () => { 47 | if (document.visibilityState != 'hidden') 48 | { 49 | init(); 50 | } 51 | }; 52 | 53 | function init () 54 | { 55 | initialized = true; 56 | 57 | // add a short timeout because window.offsetX reports wrong values before a short period 58 | setTimeout(() => { 59 | setupScene(); 60 | setupWindowManager(); 61 | resize(); 62 | updateWindowShape(false); 63 | render(); 64 | window.addEventListener('resize', resize); 65 | }, 500) 66 | } 67 | 68 | function setupScene () 69 | { 70 | camera = new t.OrthographicCamera(0, 0, window.innerWidth, window.innerHeight, -10000, 10000); 71 | 72 | camera.position.z = 2.5; 73 | near = camera.position.z - .5; 74 | far = camera.position.z + 0.5; 75 | 76 | scene = new t.Scene(); 77 | scene.background = new t.Color(0.0); 78 | scene.add( camera ); 79 | 80 | renderer = new t.WebGLRenderer({antialias: true, depthBuffer: true}); 81 | renderer.setPixelRatio(pixR); 82 | 83 | world = new t.Object3D(); 84 | scene.add(world); 85 | 86 | renderer.domElement.setAttribute("id", "scene"); 87 | document.body.appendChild( renderer.domElement ); 88 | } 89 | 90 | function setupWindowManager () 91 | { 92 | windowManager = new WindowManager(); 93 | windowManager.setWinShapeChangeCallback(updateWindowShape); 94 | windowManager.setWinChangeCallback(windowsUpdated); 95 | 96 | // here you can add your custom metadata to each windows instance 97 | let metaData = {foo: "bar"}; 98 | 99 | // this will init the windowmanager and add this window to the centralised pool of windows 100 | windowManager.init(metaData); 101 | 102 | // call update windows initially (it will later be called by the win change callback) 103 | windowsUpdated(); 104 | } 105 | 106 | function windowsUpdated () 107 | { 108 | updateNumberOfCubes(); 109 | } 110 | 111 | function updateNumberOfCubes () 112 | { 113 | let wins = windowManager.getWindows(); 114 | 115 | // remove all cubes 116 | cubes.forEach((c) => { 117 | world.remove(c); 118 | }) 119 | 120 | cubes = []; 121 | 122 | // add new cubes based on the current window setup 123 | for (let i = 0; i < wins.length; i++) 124 | { 125 | let win = wins[i]; 126 | 127 | let c = new t.Color(); 128 | c.setHSL(i * .1, 1.0, .5); 129 | 130 | let s = 100 + i * 50; 131 | let cube = new t.Mesh(new t.BoxGeometry(s, s, s), new t.MeshBasicMaterial({color: c , wireframe: true})); 132 | cube.position.x = win.shape.x + (win.shape.w * .5); 133 | cube.position.y = win.shape.y + (win.shape.h * .5); 134 | 135 | world.add(cube); 136 | cubes.push(cube); 137 | } 138 | } 139 | 140 | function updateWindowShape (easing = true) 141 | { 142 | // storing the actual offset in a proxy that we update against in the render function 143 | sceneOffsetTarget = {x: -window.screenX, y: -window.screenY}; 144 | if (!easing) sceneOffset = sceneOffsetTarget; 145 | } 146 | 147 | 148 | function render () 149 | { 150 | let t = getTime(); 151 | 152 | windowManager.update(); 153 | 154 | 155 | // calculate the new position based on the delta between current offset and new offset times a falloff value (to create the nice smoothing effect) 156 | let falloff = .05; 157 | sceneOffset.x = sceneOffset.x + ((sceneOffsetTarget.x - sceneOffset.x) * falloff); 158 | sceneOffset.y = sceneOffset.y + ((sceneOffsetTarget.y - sceneOffset.y) * falloff); 159 | 160 | // set the world position to the offset 161 | world.position.x = sceneOffset.x; 162 | world.position.y = sceneOffset.y; 163 | 164 | let wins = windowManager.getWindows(); 165 | 166 | 167 | // loop through all our cubes and update their positions based on current window positions 168 | for (let i = 0; i < cubes.length; i++) 169 | { 170 | let cube = cubes[i]; 171 | let win = wins[i]; 172 | let _t = t;// + i * .2; 173 | 174 | let posTarget = {x: win.shape.x + (win.shape.w * .5), y: win.shape.y + (win.shape.h * .5)} 175 | 176 | cube.position.x = cube.position.x + (posTarget.x - cube.position.x) * falloff; 177 | cube.position.y = cube.position.y + (posTarget.y - cube.position.y) * falloff; 178 | cube.rotation.x = _t * .5; 179 | cube.rotation.y = _t * .3; 180 | }; 181 | 182 | renderer.render(scene, camera); 183 | requestAnimationFrame(render); 184 | } 185 | 186 | 187 | // resize the renderer to fit the window size 188 | function resize () 189 | { 190 | let width = window.innerWidth; 191 | let height = window.innerHeight 192 | 193 | camera = new t.OrthographicCamera(0, width, 0, height, -10000, 10000); 194 | camera.updateProjectionMatrix(); 195 | renderer.setSize( width, height ); 196 | } 197 | } -------------------------------------------------------------------------------- /three-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2010-2023 three.js authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. --------------------------------------------------------------------------------