1 pixel wide lines

  • 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.

    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:

        // Turn off antialiasing
        NSGraphicsContext* gc =  [NSGraphicsContext currentContext];
        [gc setShouldAntialias:NO];

        // Move to the center of the pixels
        p1.x = floorf(p1.x) + 0.5;
        p1.y = floorf(p1.y) + 0.5;
        p2.x = floorf(p2.x) + 0.5;
        p2.y = floorf(p2.y) + 0.5;

        NSBezierPath* path = [NSBezierPath bezierPath];
        [path setLineWidth:0.0];
        [path moveToPoint:p1];
        [path lineToPoint:p2];
        [path stroke];

    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.

    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?

    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?
    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.

    BTW, the lines that I want to draw 1 pizel wide are things like a grid,
      paper margins. selection rectangles and other kinds of drawing aids.
    Actual lines that I am drawing I do want to scale and those work great.
  • On Nov 4, 2004, at 5:31 PM, 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 [...]
    >
    > 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?
    > 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.

    Concerning the narrowing, inflating line width, I'm not sure to
    understand the point that is preventing you to set the width of the
    line to be 1/zoom_ratio.`
  • 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
  • >
    > 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.
    >

    The usefulness of my suggestion may be questionable to you, but you
    could do the drawing "manually" in a bitmap and render that. Horizontal
    and vertical line drawing is trivial and a bresenham or dda is pretty
    quickly coded. It's kinda old-school.. I remember having done this some
    time in the distant past :)

    Ciao
    Nat!
    ------------------------------------------------------
    A good dog, though a fool.
    Who wants a smart dog!  -- R.A. Lafferty
  • > Concerning the narrowing, inflating line width, I'm not sure to
    > understand the point that is preventing you to set the width of the
    > line to be 1/zoom_ratio.`

    Anti-aliasing. If the line is not centered properly, it can "spill over"
    across 2 pixels...

    --
    Scott Ribe
    <scott_ribe...>
    http://www.killerbytes.com/
    (303) 665-7007 voice
previous month november 2004 next month
MTWTFSS
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          
Go to today
MindNode
MindNode offered a free license !