├── .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.
--------------------------------------------------------------------------------