Iron Dragon Games

Making games, because shouting into the void gets old

← All posts

The Windows Display-Scaling Bug That Made Our Bullets Immortal

September 24, 2026  ·  HTML5 canvas, JavaScript  ·  Iron Dragon Games

In Bad Triangles, player bullets are supposed to disappear when they leave the right edge of the screen. On some Windows laptops they did not. They kept flying, invisible, forever. The cause was one wrong variable name, and the same mistake can bite any HTML5 canvas game.

Ad Here 1

Two kinds of pixels

A canvas has two sizes. There is the size on the page, measured in CSS pixels, and there is the size of the actual pixel buffer it draws into. On a normal 100% display they are equal. On a high-DPI screen, or a Windows laptop set to 125% or 150% scaling, the buffer has more pixels than the page shows. The ratio between them is window.devicePixelRatio.

To keep drawing crisp, you make the buffer bigger than the element and then scale the drawing context so your game code can keep thinking in CSS pixels:

canvas.width  = vw * dpr;   // physical pixels
canvas.height = vh * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
// from here on, draw in logical (CSS) pixels

Everything you draw is in logical units: vw and vh. But canvas.width and canvas.height are now physical units.

The bug

Our culling code, the check that removes bullets once they leave the screen, compared a bullet's x position (logical) against canvas.width (physical). At 100% scaling those are the same number, so everything worked. At 150% scaling canvas.width is one and a half times larger than the visible area, so a bullet had to travel half a screen farther offscreen before it was removed. The starfield used the same check, so stars also drifted past the edge. Nothing looked broken on the development machine, and the problem only showed up on other people's displays.

The fix and the rule

The fix was to use vw and vh everywhere in game logic and never read canvas.width or canvas.height outside the resize function. It is now a written rule in our project notes, and it is the first thing we check in every new game. We also test at 100%, 125% and 150% scaling before calling a game finished.

Related trap: one resolution for every screen

While fixing this we also moved all three games to a virtual resolution. Each game pretends the screen is 900 units wide and scales everything by parentWidth / 900, so a ship or a brick takes up the same fraction of the screen on a phone as on a desktop monitor. Mouse and touch coordinates get divided by that same scale before they reach the game.

One more coordinate mistake

Our games sit beside ad bars, so the canvas does not start at the left edge of the browser window. When we first wrote the Main Menu button on the pause screen, we converted a click to game coordinates with clientX / gameScale. That is only right when the canvas starts at x = 0. With a side bar present, every click landed 120 to 160 pixels off, and the button seemed dead. The correct conversion subtracts the canvas's own position first:

const r = canvas.getBoundingClientRect();
const x = (clientX - r.left) / gameScale;
const y = (clientY - r.top)  / gameScale;

Takeaways

You can see the result in Bad Triangles, where bullets now leave the screen exactly when they should. There is also a how-to-play guide if you want to try it.

Ad Here 2
© 2026 Iron Dragon Games  ·  About  ·  Contact  ·  Dev Blog  ·  Privacy Policy