KhronosGroup / KhronosGroup/WebGL
CSS pixel size -> WebGL canvas size example is subtly wrong?
- Dominant language
- HTML
- Stars
- 2.9k
- Forks
- 703
- Avg merge
- 2d 9h
- Merged PRs (30d)
- 4
Description
Consider the example of choosing a WebGL backbuffer size based on the CSS pixel size of the canvas `window.devicePixelRatio` to match 1:1 with high DPI/physical screen pixels at
https://www.khronos.org/webgl/wiki/HandlingHighDPI
The example reads
```javascript
#theCanvas {
width: 50%;
height: 50%;
}
<style>
<canvas id="theCanvas"></canvas>
<script>
window.onload = main();
function main() {
var canvas = document.getElementById("theCanvas");
var devicePixelRatio = window.devicePixelRatio || 1;
// set the size of the drawingBuffer based on the size it's displayed.
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;
var gl = canvas.getContext("webgl");
...
}
</script>
```
Here `window.devicePixelRatio` can be an arbitrary number like `1.353239x` (it is affected by page zoom level) and the CSS pixel size (`canvas.clientWidth/Height`) can also be arbitrary float numbers like `732.4315135px` as they come from `width: 50%, height: 50%` sizes, but `canvas.width/.height` need to be integers, so there's a double->int conversion that takes place.
In this example the physical size of the canvas is set to truncate/round down the computed CSS pixel size to integer units at
```javascript
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;
```
however to my understanding there is no guarantee that rounding down is the correct action to perform, nor that rounding to nearest, or rounding up would be the correct action to perform either, in order to get a WebGL rendering output that matches up 1:1 with the pixel size on screen.
In practice this kind of "I don't know whether I should round down, round up or round to nearest" problem can result in off-by-one pixel scenarios for the WebGL render target in different browser implementations, and as result, either the whole WebGL canvas output becomes blurry, as its composited to widen/tallen/shrink by one pixel, or one column or row of the WebGL canvas is duplicated in the middle, or there will be a one-pixel black/white border on the canvas.
To my experience the only correct way to choose the backbuffer size of a WebGL canvas, given an (arbitrary) CSS pixel size, is to first compute the WebGL backbuffer size from the CSS pixel size, e.g. using the above formula, but then after having set the WebGL backbuffer size, propagate that integer size back to the CSS pixel size to ensure that the two match up. That is,
```javascript
// set the size of the drawingBuffer based on the size it's displayed.
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;
// propagate the integer size back to CSS pixels to ensure they align up 1:1.
canvas.style.width = (canvas.width / devicePixelRatio) + 'px';
canvas.style.height = (canvas.height / devicePixelRatio) + 'px';
```
A result of this issue is that to avoid compositing artifacts, it's not possible to use automatic CSS directives such as `width: 50%` to size a canvas, while expecting a 1:1 match with physical screen size.
Is there anything better to ensure 1:1 match for a canvas size without glitches, or if not, perhaps the example should be updated to instruct developers with this gotcha?
Contributor guide
Research direction
Start with the HandlingHighDPI example linked in the issue and review the JavaScript canvas sizing snippet, including the CSS percentage dimensions. Check the reported fractional CSS-pixel and devicePixelRatio cases across relevant browser behavior. Done means the example accurately documents the sizing limitation or a supported way to avoid the described compositing artifacts.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- css, html, javascript
- Domain
- computer-graphics, documentation, web-dev
- Issue type
- Documentation
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100