Programming

Bringing blurry device independent pixels into focus

Device independent pixels, or points, are a convenient way to design for multiple devices. They allow a consistent layout without worrying about exact pixel densities. It’s quite convenient, but it can lead to blurriness if we aren’t careful. To remedy this I endeavoured to add some pixel snapping and stroke adjustment features to our product Fuse last week.

In this article I’ll use the iOS term “points” to refer to “device independent pixels”.

Why it’s blurry

The problem stems from abstract units that don’t map 1:1 with the physical device pixels. Our request to draw a line in these units ends up partially covering several physical pixels. The GPU interpolates the values and draws a somewhat fuzzy line. It’s not the correct color due to blending, usually ending up lighter than intended.

diagram1

Consider an element, perhaps a button, positioned at 5,7 (our designer likes odd margin values). I wish to draw a 1 point border along the top: a line along Y=7 (represented as the dark blue line in the picture). These point sizes are abstract and must be converted to actual device pixels. Android defines a point to be equivalent to a physical pixel at 160PPI. What happens if our device is a 240DPI device? The density is 150% of the reference, so our values are scaled up. Our line along Y=7 points becomes a line along Y=10.5 pixels and the width of 1 point becomes 1.5 pixels.

Snapping locations

My first step to getting a crisper display is lining up the UI elements. I want all the borders of our elements to fall exactly on the pixel edges of the device.

The button at 5,7 on a device with 150% density translates to 7.5,10.5 which is not on the edge of a pixel. I round the values and get 8,11 as the desired pixel location. For layout I need to ensure the logical position reflect its real position on the device. From the rounded pixel values I reverse the calculation to get the point location at 5.3333,7.3333. This aligns the top-left corner of the element with device pixels.

I must also consider the size of the element. A size of 40,45 points at 150% density results in a pixel size of 60,67.5. I round this to 60,68 and reverse to get a point size of 40,45.3333. Since the top-left of the element is pixel aligned, adding this pixel snapped size will result in the bottom-right corner also being pixel aligned.

What does that do to the border?

diagram2

The edge of the element aligns to a pixel edge (the dark blue dashed line). The border however is still drawn partially in both of the surrounding pixels.

Stroke adjustment

A stroke has two potential problems: it isn’t a integral pixel width; or it doesn’t align with the pixels. The first problem is easy to resolve by rounding the width of the stroke to the nearest pixel. This produces a noticeable change in the size of the stroke, but it achieves our goal of being sharp and the correct color.

diagram3

In this case we got lucky since the pixel size of the stroke was rounded to 2. Centered on the edge of the element this fills the surrounding two pixels. What if the size of the stroke were rounded down to 1 pixel? Centering on the element edge would fail again since it’d partially cover the two surrounding pixels.

diagram4

Any odd-pixel width will have this problem. I therefore offset these a half-pixel so that the stroke edges both align with a pixel boundary. The offset could either be inside or outside of the element. I’ve chosen to put it inside at the moment, so that a 1-pixel border doesn’t require any extra space in our elements. All of this is obviously adjustable at the API level.

Clarity

With just those few simple adjustments all of our borders are sharp. They cover entire pixels so the GPU doesn’t have to interpolate partially. It does mean that the width of strokes, and sizes of elements, differs slightly from device to device.

While for many UIs this is okay, sometimes sub-pixel correctness is more important than sharpness. All of this will need to be configurable, plus I suspect I’ll be revisiting the code often.

My work on Fuse is full of interesting coding. Follow me on Twitter to get further insights and anecdotes. Should something in particular about cross-platform tools pique your curiosity just let me know.

Categories: Programming, Use Case

Tagged as: , , ,

4 replies »

  1. This seems good for things like dividing lines, but not for general UI. Consider a worst case scenario: a row of boxes with a stroke outline, and a small gap (perhaps 1 or 2 points) between them.

    As you round the values, depending on the point/pixel ratio you’re going to move some lines to the left and some to the right. If the lines on each side of the gap move in opposite directions, you’re making the gap narrower or wider. You’ll end up with uneven spacing, and I’d honestly take blurry lines over uneven spaces.

    The other thing to consider is scaling. What you’re suggesting here is basically nearest neighbour interpolation. There are cases where the UI is scaled down (e.g. a button that shows a control, which when tapped expands to show the control in a larger area). In that situation you definitely want blurry lines that fade nicely at <1.0px width.

    Anyway, sorry if I sound a bit hard on this – I do think there's a place for it (I've implemented something very similar to clean up lines on iOS) but be careful where you use it :)

    • There are obviously tradeoffs involved. Getting consistent layout, with regards to spacing, is one of the things I look at, and have identified areas for improvment. Currently it works well since general purpose UIs have a lot of whitespace between the elements, thus pixel snapping and stroke alignment doesn’t visibly alter the whitespace much. To get a true cross-platform sharpness, and perfect spacing, will require the use of actual pixel values in the layout, not just abstract points.

      Certainly there will be interfaces where sub-pixel correctness is preferred. That’s why it’s a simple toggle in our product. You can even turn it off on individual elements. So for something like an image carousel you’d most likely turn it off.

    • Good to hear. If there’s one thing I’ve learned, it’s that customers will do a lot of things I never even considered :D

      By the way, this is mostly an issue at lower pixel densities where the blurriness is very visible (but uneven pixel spacing is most obvious too). Perhaps it could be auto-disabled on high PPI devices to help performance?

    • As the PPI increases the corrections decrease naturally, so I’ve not really considered disabling it automatically on those. Plus, at this point I’ve developed a kind of unnatural sensitivity and notice the problem even at the highest resolutions. :)

      Performance isn’t really an issue here, the alignment costs virtually nothing.

      Note, the style system will however make it simple to turn off this feature on high PPI if that is what is desired.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s