Pixel Considerations


Antialiasing makes lines and shapes look smooth - though sometimes at the expense of sharpness. What if you're trying to draw a horizontal or vertical line where you don't need antialiasing? You might be under the impression that if you position your shapes at round integer positions, you will avoid antialiasing.
But it's not quite that simple - so avoid trying to be smart with layout code like this:


...
label.layoutX = (width - labelWidth) / 2 as Integer;
....

The idea here is that when you're placing something in the middle you might end up with a fractional value, say 42.5, and so you added the "as Integer" to round things to a whole number to avoid introducing antialiasing.



Well, this may have exactly the opposite effect! Take a look at the following picture, which shows two rectangles. Both rectangles have a stroke width of 1.0, and one of them is positioned at a round integer, and the other one is positioned at 0.5.







Here's a zoomed in view which makes things clearer:







Obviously, the rectangle on the left is blurry because antialiasing is attempting to show the line as being somewhere in the middle between them. The rectangle
on the right on the other hand is clear and crisp because the lines overlap EXACTLY with the pixel grid the line is rendered into.



Here's the thing though: The rectangle on the left is the one that was positioned at round integers, and the rectangle on the right is the one positioned
at round integer + 0.5 !



So should you run out and ensure that all your horizontal and vertical edges are positioned at 0.5? No. The key here is the stroke width. Take a look at the following figure, where I have position rectangles with different stroke widths (1, 2, 3) and different pixel positions (0, 0.25, 0.5).







Zoomed in:







As you can see, whether you match the pixel grid perfectly depends on the stroke width and the pixel positions. This actually makes sense. Think of your pixel grid as having x coordinates at the boundaries of each pixel. In other words, "0.0" is the left edge of the first pixel, and 1.0 is the right edge of the first pixel. The line position has to be the center of the stroke. So if you want to have a line of thickness 1, then that line will run exactly through the pixel, so we must position its center at x=0.5. When the stroke width increases to 2 however, the center will be in the middle (e.g. 1), and so we should position it at a round number. And so on.



When you're dealing with large shapes this isn't a big deal. But if you're trying to paint a grid (like the one below), a pattern, or small controls (like disclosure arrows - which is how I came to look into this), it can pay off.







By the way -- on OSX there's a nice screen zoom (hold the Option key and then do a two-fingered drag on the trackpad up or down) which makes it easy to zoom in and look at the pixels for anything on the screen. But unfortunately it doesn't show pixels as square, it does more blending, so it's much harder to tell what's going on at the individual pixel level. Get an image editor which lets you zoom in with clear pixel boundaries, or even a screen magnifying lens. Here's how the builtin screen zoom looks - as you can see it's not as clear as the pixel zooms above:







UPDATE: Marius taught me in the comments that you can turn off the OSX zoom smoothing in the Universal Access options. Sweet! I can now instantly check the pixels without going to an intermediate screenshot! Thanks!



Finally: Jonathan Giles from the JavaFX controls team has been doing a great job aggregating and summarizing interesting FX articles each week -- highly recommended if you're doing anything with JavaFX.



Comments:

About the OSX... On my mac it's Control+scroll either with trackpad or my mouse's scroll wheel. You can also hit Option+Command+\\ to turn the smoothing on and off. Or you can use System Preferences/Universal Access/Zoom: Options to do the same.

Posted by Marius on May 10, 2010 at 04:33 PM PDT #

Marius, thank you thank you thank you! This is going to save me time!

Posted by Tor Norbye on May 11, 2010 at 12:22 AM PDT #

Java's renderer is too mathematically correct. It does not convey the intention of the developer. For instance, fillRect and drawRect does not produce a rectangle of the same size! I understand the math behind the reasoning but still that's just crazy. And it has made so many UI's ugly.

The renderer should have two modes. One explicit, which is mathematically correct, and one pixel snapping where the different shapes are painted as one intends, even if not mathematically correct. Java has too many modes, implemented as RenderingHints. The combinations are endless and the results are as well, few of them good.

Issues like these shouldn't be issues other than for the 2% of developers that actually know what their doing regarding 2d.

Posted by MIkael Grev on May 12, 2010 at 07:08 PM PDT #

Thx a lot !
This is one more "How didn't I think about it earlier ?".
I'm going to test it in Flash. They may have set (0,0) in the middle of the first pixel (which seems more logical to me).

Posted by Olivier Allouch on May 12, 2010 at 10:28 PM PDT #

I completely disagree:
- First this change is breaking with the renderer of JavaFX 1.2 and previous versions.
- Second this kind of rendering does not occur when Prism is enabled.
- Third it is not consistent with Java2D's own rendering mechanisms.
- Fourth it is AFAK the only graphical lib around that exhibits this very odd (though mathematically correct) rendering behavior
- And fifth, when people want to render something as simple as a grid they do not want to know that they have to offset everything by 0.5px. If you draw a vertical or horizontal line at an integer coordinate with a stroke of 1, you expect a 1-pixel wide solid line, period.
In the end the impacted demographics is not only those programmers who do 2D but also those who create controls as of course most base custom controls are made of 2D bricks.
A grid is an easy example, where doing an offset is easy. On a much larger and complex shape such as a polygon or a spline having to take care of that extra-calculation just for the sake of having a proper rendering is a hassle. And this happens often when creating a custom control.
Even the current JavaFX charts do not render correctly because of this:

function createChart(): Chart {
def max: Integer = 10;
def chart: LineChart = LineChart {
width: 200
height: 200
layoutInfo: LayoutInfo {
width: bind chart.width
height: bind chart.height
}
xAxis: NumberAxis {
lowerBound: 0
upperBound: max
}
yAxis: NumberAxis {
lowerBound: 0
upperBound: Math.pow(2, max)
tickUnit: 200
}
data: [
LineChart.Series {
name: "Power of 2"
data: [
for (i in [0..max]) {
LineChart.Data {
xValue: i
yValue: Math.pow(2, i)
}
}
]
}
]
}
}

def chart1: Chart = createChart();
def chart2: Chart = createChart();

chart2.layoutX = 0.5;
chart2.layoutY = 210 + 0.5;

Stage {
title: "Test JavaFX 1.3 bad rendering"
scene: Scene {
content: [
chart1,
chart2
]
}
}

Posted by Fabrice Bouye on May 16, 2010 at 08:16 AM PDT #

Post a Comment:
Comments are closed for this entry.
About

Tor Norbye

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today