Skip navigation.
 
mlRe: 1 pixel wide lines
FROM : Greg Titus
DATE : Thu Nov 04 18:16:14 2004

On Nov 4, 2004, at 8:31 AM, Joe Esch wrote:

> Before you flame me, let me say that I've looked in the archives and
> found that questions about drawing 1 pixel wide lines using
> NSBezierPath or Quartz have been asked many times.  The reason that
> I'm submitting yet another question is that I am still having problems
> and was hoping that since this has been asked so many times that maybe
> Apple has done something to solve the problem in a nice way.


Nope, as far as I know, Apple hasn't done anything to make this nice -
although once you see the necessary code, I think you might agree that
it isn't _that_ hard now. Or perhaps not. :-)

> First off, let me say that I am only interested in horizontal or
> vertical lines, so answers like "1 pixel wide lines don't make any
> sense for diagonal lines" are not that interesting.
>
> Here is what I am currently doing:

[...]

> I think that these are all the steps that are suggested for drawing 1
> pixel wide lines.  In fact, this seems to work if I do not scale the
> view.  The problem is that I am putting the view inside of a scaling
> view (modeled after the TextEdit sample).  I change the clip view
> bounds which results in the view getting scaled.  I assume that this
> results in the positions moving off the center of the pixels.  The
> result is that as I zoom in and out on the view the lines get wider or
> narrower and it is definitely not what I want.


Yep.

> My questions are:
>
> 1. Has Apple realized that this is a useful thing to want to do and
> provided a good way to do it yet?


Nope.

> 2. If not, how do I do it?  Am I going to have to somehow invert the
> view transform and figure out how to adjust the point coordinates so
> that the final transformed points end up in the center of a pixel?


Yes, exactly.

> This seems like an awful lot of work for something that should be
> easy.  For some reason, I don't see the problem with scrolling.  I
> assume that the scrolling code must adjust the scroll amount to only
> scroll by even pixel amounts.


The problem with your code is basically the floor()ing of the
coordinate positions and the addition of 0.5. That only gets you to
centers of pixels when the scaling is normal. To account for cases
where scaling isn't 100% you need to get a little more complicated.

First, let's deal with drawing through the centers of pixels rather
than on the edges. It looks nicer if you do this for _all_ of your
drawing, not just horizontal and vertical lines that you don't want
scaled. So what we do is set an NSAffineTransform on the drawing
context to shift by that half of a pixel before doing any of the
drawing, and then it is set up for you the whole time, and none of the
individual lines have to be adjusted by 0.5 (or by more complicated 
amounts at different scales). If you are in a scrollview and zoomed it
is possible you can also be scrolled to a weird position. Scrollviews
don't scroll to partial pixels, but they can make the calculations more
complex when zoomed - thus the calcs based on the visible rect below:

- (void)performAntialiasShift;
{
    NSRect visible = [self visibleRect];
    NSPoint shift;
    NSSize size = {1,1};

    size = [self convertSize:size fromView:nil];
    float error = fmod(NSMinX(visible), size.width);
    if (error < size.width/2)
        shift.x = error + size.width/2;
    else
        shift.x = error - size.width/2;
    error = fmod(NSMaxY(visible), size.height);
    if (error < size.height/2)
        shift.y = error + size.height/2;
    else
        shift.y = error - size.height/2;
    NSAffineTransform *transform = [NSAffineTransform transform];
    [transform translateXBy:shift.x yBy:shift.y];
    [transform concat];
}

This code uses -convertSize:fromView: to determine how a 1 by 1 pixel
area is transformed by the current zoom level, then shifts in such a
way so as to adjust coordinates the minimal possible amount to get to
the center of pixels. With an unscaled view, "error" will always be 0,
and "size" will always be {1,1}, so that this code simplifies down to
the case of adding 0.5 to both x and y. In scaled views with scrolling,
it will still do the right thing. Just call this -performAntialiasShift
method at the beginning of your -drawRect:.

So that's half the job: we've taken care of the +0.5 part. Now we just
need to generalize the floor() ing of coordinates for horizontal or
vertical lines. To do that we subtract the remainder of dividing by the
pixel size:

    float lineWidth = [self convertSize:NSMakeSize(1.0, 1.0)
fromView:nil].width;
    p1.x = p1.x - remainder(p1.x, lineWidth);
    p1.y = p1.y - remainder(p1.y, lineWidth);
    p2.x = p2.x - remainder(p2.x, lineWidth);
    p2.y = p2.y - remainder(p2.y, lineWidth);

    [path setLineWidth:lineWidth];
    [path moveToPoint:p1];
    [path lineToPoint:p2];
    [path stroke];

Again, in an unscaled view, this does the exact same thing as floor().
But in a scaled view you want to get to a multiple of the pixel size,
whatever that may be. Notice also that we're not turning off
antialiasing - we don't need to. We're drawing a line exactly one pixel
wide exactly through the middle of pixels, so Quartz's aliasing does
the right thing.

Hope this helps,
   - Greg

Related mailsAuthorDate
ml1 pixel wide lines Joe Esch Nov 4, 17:31
mlRe: 1 pixel wide lines stephane sudre Nov 4, 17:41
mlRe: 1 pixel wide lines Greg Titus Nov 4, 18:16
mlRe: 1 pixel wide lines Nat! Nov 4, 22:33
mlRe: 1 pixel wide lines Scott Ribe Nov 6, 23:30