Devices have various physical resolutions and densities. One phone may be 10cm wide and have 800pixels across, while another may be 8cm wide and have 1200pixels across. A UI engine needs to provide some consistent way to deal with sizes across the various devices.
This article is part of a series on Writing a UI Engine.
We define a point to be a unit with similar size across all devices. In the domain of the UI, a “point” has no concrete size. On a particular device, there is a mapping from points to the device pixels (the little dots that make up the actual display).
Some systems call these device independent pixels but calling it a “pixel seems wrong, and it’s also somewhat wordy. Also don’t confuse this with the traditional meaning of a point for printing either: that meaning of point has no use in a UI engine.
The “pixel-to-point” ratio is the number of pixels contained within a point. For example, if you have a 3:1 pixel-to-point ratio, a shape with a point width of 10 will render across 30 pixels.
The hardware screen density usually determines this mapping. Given two devices of the same physical size, one may have a resolution of 400×800 pixels, and the other 800×1600. The second device has a density twice that of the first one. If the first one is using a 1:1 pixel-to-point ratio, we’d expect the second one to use a 2:1 ratio. A point defined this way would have the same visual size on both devices.
Rarely do we have this kind of precise relationship. Device vendors define pixel-to-point ratios that make sense for a device, but don’t necessarily have a consistent meaning. A UI engine doesn’t need to have the same meaning of a point as a particular OS or device, but it’s valuable to consider the vendor’s mapping to match expectations set by other apps.
The concept of density has become tenuous: in some contexts, it measures a pixel-per-mm ratio, in others a logical-pixel-to-point ratio, in others a ratio between resolutions of devices. It’s best to refer to specific things like pixel-to-point ratio.
It might at first seem desirable to have a fixed point-to-mm mapping. You could then design a UI and get the same real-world sizes of all your elements, regardless of the specific device.
If we consider a device’s physical size, it’s resolution, and both user and coder expectations though, there are reasons not to give point a consistent physical size. Perhaps a slightly larger phone model is intended to produce larger visuals. Other than the display it may share the same graphics hardware and software settings as the smaller phone. Keeping the same point-to-pixel ration can satisfy both the user and the hardware requirements.
For graphic design, it can be convenient to have common groupings. Our phones have common ratios like 1:1, 1.5:1, 2:1, 3:1, and 4:1. By sticking to these standard scalings, we can better prepare image assets. Images can target a specific density and up/down-scaled by powers of two, which results in crisper images than scaling to arbitrary sizes — more on this below. It’s better to have a somewhat inconsistent physical size of a point, rather than a ratio like 2.1837:1.
Yet there will always be devices that have unusual densities. An app may also desire to render to a texture with a different ratio, or for a higher target density. The UI engine has to deal with any pixel-to-point ratio.
Points are often expressed as integral values, and the graphic design is also specified that way. There is no requirement for that though. The point system in a UI engine should be using floating point. Conversion to-from points and pixels must retain the same value, especially in layouts that require pixel precision.
Pixel snapping is an inevitable part of the discussion of points. Regardless of how you wish to specify you UI sizes and positions, you’d like to have a sharp display. To get a crisp line, or image border, it needs to be aligned to pixels, not points.
For various reasons the coded layout will result in positions that don’t align with pixels. You may have a ratio of 1.5:1, in which case a rectangle with width 5 will cover 7.5 pixels. Or you may be trying to centre elements and end up covering half-pixels everywhere. It’s almost the rule, rather than the exception that you’ll end up with sizes and positions that don’t map cleanly to pixels.
To get sharp rendering, we want to align straight lines and images on pixel borders. We may instead change the size of that 5 point width rectangle to 5.3333, so it covers exactly 8 pixels. We may alter a 0.5 stroke width to be precisely 1, or 2 pixels, and shift it left/right partial points to get it aligned correctly.
The UI engine should be working in the realm of points. It will be necessary to define a mapping form points to pixels that satisfies the needs of the hardware and ui design. Layout must allow floating point values and has to deal with mapping to pixel boundaries to ensure a sharp display. Image elements, in particular icons, will need sizing modes that account for pixel mapping.
When we talk about layout we’ll revisit this issue of pixel correctness. It can lead to problems of spatial aliasing in the layout itself, not just blurry visuals.