A couple of years ago a video went round of two browser windows side by side, each with a glowing ball of particles, and the particles flowed from one window into the other. Drag a window and the stream bent to follow it, as if the windows were magnets. It was Bjørn Staal's multi-window piece from November 2023, and I never forgot it. So I built my own version. It is live at avivashishta.com/lab/entangled: open it, click Open a second window, and move them around.
The short version: every window works in screen coordinates instead of page coordinates. Each one tells the others where it sits on the monitor over a BroadcastChannel, and every particle is worked out from the shared clock and the orb positions alone. So every window draws the exact same particles, each through its own hole in the screen, and a stream that leaves one window lines up with where it enters the other. No server, no WebSockets.
The trick: one screen, many holes
A web page normally thinks in its own coordinates: (0, 0) is the top left of the page. Here, everything lives on the monitor instead. A window knows where its page sits on the screen:
const rect = {
x: window.screenX + (window.outerWidth - window.innerWidth) / 2,
y: window.screenY + (window.outerHeight - window.innerHeight), // the tabs and address bar sit above the page
w: window.innerWidth,
h: window.innerHeight,
};
Each window puts its orb at the middle of its own rect, in screen pixels. Then the vertex shader draws every particle at a screen position and subtracts this window's top left corner at the very end:
vec4 toClip(vec2 s) {
vec2 q = (s - uOrigin) / uView; // uOrigin = this window's corner on the screen
return vec4(q.x * 2. - 1., 1. - q.y * 2., 0., 1.);
}
That's the whole illusion. Both windows draw the same scene, and each just looks at a different part of it. With two monitors it works the same way, because screen coordinates run across both displays.
Telling the other windows
Windows from the same site can talk to each other with BroadcastChannel, no server needed. Each window sends its id, when it opened and its rect whenever it moves and a few times a second anyway. There's no event for "the window moved", so every frame it checks screenX and screenY and sends an update if they changed. A window that stops sending for 2.5 seconds is dropped, and closing one sends a goodbye straight away.
Windows are put in order by when they opened, and the order picks the colour: blue, red, yellow, then paper. Up to four link up, which makes six streams.
Particles with no state
The part that makes it line up: no particle is ever stored or synced. Each one has a few random numbers baked in, and its position at any moment comes from those, the time and the orb positions. Every window reads the same wall clock (performance.timeOrigin + performance.now()), so two windows working out particle 4,012 get the same answer without ever talking about it. All of it runs in the vertex shader, about 70,000 points in a few draw calls.
What's drawn
Each orb is a small magnet. Its axis points at the nearest other orb, so when you move a window the other orb swings round to face it. There are three layers:
- A shell, a sphere spinning on the magnet's axis, brighter at the edge so it reads as round, and bulging toward the other orb the closer it is.
- Field lines, thirty loops from pole to pole following the dipole curve r = L sin²θ, with particles flowing along them.
- A core, a small, hot, fast swirl in the middle.
The streams are field lines between two magnets. Each particle rides one of about forty lanes. A lane leaves orb A at an angle, bows out and lands on orb B, as a cubic Bézier:
vec3 e0 = vec3( dir * cos(th), 0.) + side; // leaving A
vec3 e1 = vec3(-dir * cos(th), 0.) + side; // arriving at B, mirrored
vec3 p0 = vec3(A.xy, 0.) + e0 * A.z * .85;
vec3 p3 = vec3(B.xy, 0.) + e1 * B.z * .85;
vec3 p = bez(p0, p0 + e0 * reach, p3 + e1 * reach, p3, u);
u moves from 0 to 1 over time. Half the particles go each way, and they fade from one orb's colour to the other's on the way. Small lane angles make the bright straight beam between the orbs and big ones make the wide loops around it, which is why it looks like iron filings round two magnets. One particle in twenty is a brighter comet head.
Under the particles, a full-screen shader draws a soft glow round each orb and along each link, in the same screen space. Behind everything is a starfield fixed to the monitor, not the window, so the windows really do look out onto one sky.
Movement: each orb eases toward its window's centre on a spring, so a dragged window tows its orb a little behind it instead of snapping.
The catch: Brave
My first test was in Brave across two displays, and both windows drew the same picture on top of each other. Brave Shields protects you from fingerprinting by faking window.screenX and screenY, so every window says it's in the same place. The page now notices when two windows claim the same spot and tells you to turn Shields off for the site or use Chrome or Safari. Phones only have one window, so they get a note instead of the button.
Build your own
Build a full-screen page with Three.js where every browser window that has the page open shows a glowing orb of particles, and the orbs are joined by streams of particles that cross the gap between windows and follow them as they move. Work in screen coordinates: each window's page rect on the monitor is screenX + (outerWidth - innerWidth) / 2, screenY + (outerHeight - innerHeight), innerWidth, innerHeight. Each window sends its id, open time and rect over a BroadcastChannel whenever it moves (check every frame) and every 400ms, answers a hello, sends bye on pagehide, and drops windows not heard from in 2.5s. Order windows by open time; the order sets the colour. Ease each orb toward its window's centre.
Keep no particle state. Draw everything in vertex shaders from per-particle random attributes, uniforms for up to four orbs (x, y, radius, on) and the shared wall clock (performance.timeOrigin + performance.now()), and convert screen px to clip space by subtracting this window's corner. Orbs: a rim-lit spinning shell that bulges toward the nearest orb, dipole field loops (r = L sin²θ) around an axis aimed at the nearest orb, and a hot core, with a simple perspective divide. Streams: for every pair, particles on about 40 lanes, each a cubic Bézier that leaves one orb at an angle off the axis and lands on the other mirrored, flowing both ways and mixing colours. Add a full-screen additive glow round orbs and along links, and a starfield fixed to the screen. Use additive blending on a dark background, a button that opens a second window beside this one with window.open, and a warning when two windows report the same position (Brave Shields fakes it).