A tiny physics engine that makes every window on your Windows desktop fall, bounce and get thrown around.
WinGravity finds every visible window on your Windows desktop and gives it mass. Drop them, throw them, watch them bounce off the edges of your screen.
These windows run the same physics, ported line-for-line from the C.
- Turn on gravity in the taskbar.
- Grab a window by its title bar.
- Fling it and let go.
#define GRAVITY 444.0f
#define FRICTION 5.0f
#define BOUNCE 0.6f
#define BOUNCE_THRESHOLD 80.0f
#define BOUNCE_THRESHOLD_GROUND 120.0f
waiting for input…
The idea
I wanted to know whether I could make Windows itself feel physical — not a game in a window, but the actual windows on my desktop falling to the bottom of the screen and bouncing when they landed. It turned out the Win32 API gives you everything you need: a list of every window, events when the user drags one, and a way to move any window you like.
How it works
Finding the windows
On start-up, EnumWindows walks every top-level window. Anything that's visible and has a title
becomes an EngineObject with a position, velocity and size, stored in an array that grows with
realloc.
Listening for the user
SetWinEventHook registers for EVENT_SYSTEM_MOVESIZESTART,
EVENT_SYSTEM_MOVESIZEEND and EVENT_OBJECT_LOCATIONCHANGE. When you grab a window,
it's marked as grabbed and the physics stops fighting you. While you drag, its position is sampled every
20 frames; when you let go, the distance travelled over that time becomes the throw velocity.
The physics loop
Every frame, the engine measures elapsed time with QueryPerformanceCounter, adds gravity to
each window's vertical velocity, and checks it against the edges of the virtual screen (all monitors
combined). Hitting the floor bounces the window back up with 60% of its speed — unless it's moving too
slowly, in which case it settles and friction takes over. Finally, SetWindowPos moves every
real window to its new position.
Bringing it to the browser
The demo above is a TypeScript port of UpdateEnginePhysics. The Win32 parts are replaced —
pointer events stand in for the window hooks, CSS transforms stand in for SetWindowPos — but
the constants, the collision rules and the throw calculation are the same. Here's the floor collision in
both:
//Check if object is on floor
if (objectBottom >= boundary->bottom){
engine->objects[i].pos.y = (float)(boundary->bottom - engine->objects[i].height);
if (fabs(engine->objects[i].vel.y) > BOUNCE_THRESHOLD_GROUND){
engine->objects[i].vel.y *= -BOUNCE;
}
else{
engine->objects[i].vel.y = 0.0f;
}
//Apply friction
if (fabs(engine->objects[i].vel.x) <= FRICTION){
engine->objects[i].vel.x = 0.0f;
}
else {
engine->objects[i].vel.x -= fsign(engine->objects[i].vel.x) * FRICTION;
}
}
if (bottom >= bounds.bottom) {
o.pos.y = bounds.bottom - o.height;
if (Math.abs(o.vel.y) > BOUNCE_THRESHOLD_GROUND) o.vel.y *= -BOUNCE;
else o.vel.y = 0;
if (Math.abs(o.vel.x) <= FRICTION) o.vel.x = 0;
else o.vel.x -= Math.sign(o.vel.x) * FRICTION;
}
What I learned
-
Callbacks and the Windows message loop. Nothing happens unless you keep pumping messages
with
PeekMessage, so the physics and the event handling have to share one loop. - Fixed timesteps. Running physics in fixed steps with an accumulator keeps the simulation stable no matter how fast or slow the frames come in.
-
Manual memory. Growing the window list with
realloc, and checking it didn't fail before using it.