Vladimir Prus


Wednesday, July 09, 2014

Rectangles with SVG

One would think drawing a rectangle using SVG is easy - it's a basic shape, and there are numerous tutorials with straightforward instructions, such as:
<rect x="10" y="10" width="100" height="100" stroke="black" stroke-width="1" fill="white"/>
What is rarely explained is the exact meaning of the coordinates, and dimensions.

SVG operates in a pure mathematical coordinate system. Coordinates are real numbers and the lines of the shapes are infinitely thin. Width and height of a rectangle are distances between these infinitely thin lines. The stroke is applied alongside these mathematical lines, with equal width on each size. So, the above rectangle is actually 101 units wide - with 0.5 unit of stroke applied to the left of left line, then 100 pixels to the right right and 0.5 unit of stroke to the right. And there's a total of 1 unit of stroke inside the rectangle, so the empty inside of the rectangle is 99 units wide.

Coordinates don't need to be integer, and it can even be more logical to have fractional coordinates. The picture below shows a rectangle with extra content that is scaled up, in SVG, for exposition. The grid lines corresponds to integer coordinates in the unscaled world. The y coordinate of the rectangle is 2.5. With 0.5 of stroke added on both sides, it nicely occupies space between y=2 and y=3.

But when we try to display SVG using default coordinate system, when one uint is a pixel, things break down. There, integer values of coordinates fall between the pixels, and the browsers cannot quite agree about how to apply 0.5px stroke on both sides of a line. The below picture shows rendering of rectangles by Chrome and Firefox, using both whole-pixel and half-pixel coordinates.

Chromium does a logical thing. With integer coordinates, it applies 0.5px stroke on both sides of the line by making the pixel half-saturated - so we get line that is twice as wide and twice less dark. With half-pixel coordinates the mathematical lines go through the middle of pixels, so 0.5px stroke on both side result in a single pixel painted with the stroke color. What Firefox does, I don't know. If you look carefully, vertical and horizontal lines are pained differently, and top line is painted differently from the bottom line. Both with integer and half-pixel coordinates most of the lines are two-pixel, with different saturation of pixels. This looks like explicit programming, but I miss the logic. Of all of these I prefer Chromium rendering with half-pixel coordinates.

Of course, drawing a rectangle was not my goal. Rather, I was about to draw program control flow graphs using JointJS and Dagre. Overall, these libraries are rather good - I could quickly produce a reasonable prototype and JointJS code was easy to read and extend. However, it also effectively forces integer coordinates. There is one place in code where it did rounding unintentionally, and another place where coordinates are rounded explicitly. Will certainly work around this locally, but it would be great if people building on top of SVG pay attention to this issue.

If you want to follow along, here is the first example and second example.

No comments: