Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to set up a user Quartz2D coordinate system with scaling that avoids fuzzy drawing?

This topic has been scratched once or twice, but I am still puzzled. And Google was not friendly either.

Since Quartz allows for arbitrary coordinate systems using affine transforms, I want to be able to draw things such as floorplans using real-life coordinate, e.g. feet.

So basically, for the sake of an example, I want to scale the view so that when I draw a 10x10 rectangle (think a 10-inch box for example), I get a 60x60 pixels rectangle.

It works, except the rectangle I get is quite fuzzy. Another question here got an answer that explains why. However, I'm not sure I understood that reason why, and moreover, I don't know how to fix it. Here is my code:

I set my coordinate system in my awakeFromNib custom view method:

- (void) awakeFromNib {
    CGAffineTransform scale = CGAffineTransformMakeScale(6.0, 6.0);
    self.transform = scale;
}

And here is my draw routine:

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGRect r = CGRectMake(10., 10., 10., 10.);
    CGFloat lineWidth = 1.0;
    CGContextStrokeRectWithWidth(context, r, lineWidth);
}

The square I get is scaled just fine, but totally fuzzy. Playing with lineWidth doesn't help: when lineWidth is set smaller, it gets lighter, but not crisper.

So is there a way to set up a view to have a scaled coordinate system, so that I can use my domain coordinates? Or should I go back and implementing scaling in my drawing routines?

Note that this issue doesn't occur for translation or rotation.

Thanks

like image 797
Jean-Denis Muys Avatar asked Mar 21 '10 18:03

Jean-Denis Muys


2 Answers

the [stroked] rectangle I get is quite fuzzy.

Usually, this is because you plotted the rectangle on whole-number co-ordinates and your line width is 1.

In PostScript (and thus in its descendants: AppKit, PDF, and Quartz), drawing units default to points, 1 point being exactly 1/72 inch. The Mac and iPhone currently* treat every such point as 1 pixel, regardless of the actual resolution of the screen(s), so, in a practical sense, points (by default, on the Mac and iPhone) are equal to pixels.

In PostScript and its descendants, integral co-ordinates run between points. 0, 0, for example, is the lower-left corner of the lower-left point. 1, 0 is the lower-right corner of that same point (and the lower-left corner of the next point to the right).

A stroke is centered on the path you're stroking. Thus, half will be inside the path, half outside.

In the (conceptually) 72-dpi world of the Mac, these two facts combine to produce a problem. If 1 pt is equal to 1 pixel, and you apply a 1-pt stroke between two pixels, then half of the stroke will hit each of those pixels.

Quartz, at least, will render this by painting the current color into both pixels at one-half of the color's alpha. It determines this by how much of the pixel is covered by the conceptual stroke; if you used a 1.5-pt line width, half of that is 0.75 pt, which is three-quarters of each 1-pt pixel, so the color will be rendered at 0.75 alpha. This, of course, goes to the natural conclusion: If you use a 2-pt line width, each pixel is completely covered, so the alpha will be 1. That's why you can see this effect with a 1-pt stroke and not a 2-pt stroke.

There are several workarounds:

  • Half-point translation: Exactly what it says on the box, you translate up and right by half a point, compensating for the aforementioned 1-pt-cut-in-half division.

    This works in simple cases, but flakes out when you involve any other co-ordinate transformations except whole-point translations. That is to say, you can translate by 30, 20 and it'll still work, but if you translate by 33+1/3, 25.252525…, or if you scale or rotate at all, your half-point translation will be useless.

  • Inner stroke: Clip first, then double the line width (because you're only going to draw half of it), then stroke.

    This can require gstate juggling if you have a lot of other drawing to do, since you don't want that clipping path affecting your other drawing.

  • Outer stroke: Essentially the same as an inner stroke, except that you reverse the path before clipping.

    Can be better (less gstate juggling) than an inner stroke if you're sure that the paths you want to stroke won't overlap. On the other hand, if you also want to fill the path, the gstate juggling returns.

*This won't last forever. Apple's been dropping hints for some time that they're going to change at least the Mac's drawing resolution at some point. The API foundation for such a change is pretty much all there now; it's all a matter of Apple throwing the switch.

like image 175
Peter Hosey Avatar answered Nov 15 '22 00:11

Peter Hosey


Well, as often, explaining the issue lead me to a solution.

The problem is that the view transform property is applied to it after it has been drawn into a bit buffer. The scaling transform has to be applied before drawing, ie. in the drawRect method. So scratch the awakeFromNib I gave, and here is a correct drawRect:

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGAffineTransform scale = CGAffineTransformMakeScale(6.0, 6.0);
    CGContextConcatCTM(context, scale);
    CGRect r = CGRectMake(10., 10., 10., 10.);
    CGFloat lineWidth = 0.1;
    CGContextStrokeRectWithWidth(context, r, lineWidth);
}
like image 20
Jean-Denis Muys Avatar answered Nov 15 '22 00:11

Jean-Denis Muys