Parankush Bhardwaj
Parankush Bhardwaj

Reputation: 27

Draw a Point using NSBezierPath

I want to create a single pixel-size dot. I assumed the way to do so was to draw a line using NSBezierPath with the starting point being equal to the end point (see code below) but doing so makes a completely empty window.

func drawPoint() {
    let path = NSBezierPath()
    NSColor.blue.set()
    let fromPoint = NSMakePoint(CGFloat(100) , CGFloat(100))
    let toPoint = NSMakePoint(CGFloat(100) , CGFloat(100))
    path.move(to: fromPoint)
    path.line(to: toPoint)
    path.lineWidth = 1.0
    path.stroke()
    path.fill()
}

However, if I change the toPoint coordinates from 100,100 to 101,101 (or any set of coordinates other than 100,100), a blue shape does show up. Does anyone know why this is the case?

Another issue I found is if you made toPoint's coordinates equivalent to (for instance) 101,101, then it would create a square with a length of two pixels, but also add a another pixel-length layer of blur (see image below, zoomed in for detail).

pictureOfSquare

What I wish to do, however, is just put a small single blue square the size of one pixel, with no blur. Does anyone know how I can do this?

Upvotes: 1

Views: 1122

Answers (3)

mikeD
mikeD

Reputation: 195

To draw a dot at: x, y of size: width, height.

NSBezierPath(ovalIn: CGRect(x: 5.5, y: 5.5, width: 4.0, height: 4.0)).fill()

To fine tune "pixel" placement and size, use the decimals in Double or CGFloat arguments.

Upvotes: 1

Parankush Bhardwaj
Parankush Bhardwaj

Reputation: 27

On top of rob mayoff's solution, I figured out a different solution using NSGraphicsContext. I just grabbed the Core Graphics context and turned off antialiasing (see below)

 let context = NSGraphicsContext.current?.cgContext
 context?.setShouldAntialias(false)

Then, I created a path with the size of one pixel:

 context.setLineWidth(0.5)
 context.setStrokeColor(color)
 context.beginPath()
 context.move(to: CGPoint(x: 100, y: 100))
 context.addLine(to: CGPoint(x: 100.15, y: 100.15))
 context.strokePath()

When adding a endpoint to the line, I noticed that by giving it a value greater than 100, but less than 100.5, you can get exactly one pixel shaded in.

Upvotes: 0

rob mayoff
rob mayoff

Reputation: 385500

You have a few problems:

I want to create a single pixel-size dot.

Are you sure? What if you're on a Retina display? What if you're drawing to a PDF that will be printed on a 600 dpi laser printer?

I assumed the way to do so was to draw a line using NSBezierPath with the starting point being equal to the end point

That is a way to draw a lineWidth-sized dot, if you set the lineCap property to .round or .square. The default lineCap is .butt, which results in an empty stroke. If you set lineCap to .round or .square, you'll get a non-empty stroke.

let fromPoint = NSMakePoint(CGFloat(100) , CGFloat(100))
let toPoint = NSMakePoint(CGFloat(100) , CGFloat(100))
path.move(to: fromPoint)
path.line(to: toPoint)

You need to understand how the coordinate grid relates to pixels:

  • On a non-Retina screen, integer coordinates are by default at the edges of pixels, not at the centers of pixels. A stroke along integer coordinates straddles the boundary between pixels, so (with lineWidth = 1.0) you get multiple partially-filled pixels. To fill a single pixel, you need to use coordinates that end in .5 (the center of the pixel).

  • On a Retina screen, integer and half-integer coordinates are by default at the edges of pixels, not at the centers of pixels. A stroke along integer or half-integer coordinates straddles the boundary between pixels. With lineWidth = 1.0, pixels on both sides are completely filled. If you want to fill only a single pixel, you need to use coordinates that end in .25 or .75 and a lineWidth of 0.5.

  • In an unscaled PDF context, a lineWidth of 1.0 nominally corresponds to 1/72 of an inch, and the number of filled pixels depends on the output device.

path.lineWidth = 1.0

This can only give you “a single pixel-size dot” on a non-Retina screen (or if you scaled the graphics context). You need to adjust it if you truly want a single pixel dot on a Retina screen. But you're better off sticking to points rather than pixels.

All that said, here's how you can create and stroke an NSBezierPath to fill one pixel:

import AppKit

func dotImage() -> CGImage {
    let gc = CGContext(data: nil, width: 20, height: 20, bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
    let nsGc = NSGraphicsContext(cgContext: gc, flipped: false)
    NSGraphicsContext.current = nsGc; do {

        let path = NSBezierPath()
        path.move(to: .init(x: 10.5, y: 10.5))
        path.line(to: .init(x: 10.5, y: 10.5))
        path.lineWidth = 1
        path.lineCapStyle = .round
        NSColor.blue.set()
        path.stroke()

    }; NSGraphicsContext.current = nil
    return gc.makeImage()!
}

let image = dotImage()

Result:

dot image

However, you might prefer to simply fill a rectangle directly:

func dotImage2() -> CGImage {
    let gc = CGContext(data: nil, width: 20, height: 20, bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
    let nsGc = NSGraphicsContext(cgContext: gc, flipped: false)
    NSGraphicsContext.current = nsGc; do {

        NSColor.blue.set()
        CGRect(x: 10, y: 10, width: 1, height: 1).fill()

    }; NSGraphicsContext.current = nil
    return gc.makeImage()!
}

let image2 = dotImage2()

Result:

dot image 2

Upvotes: 5

Related Questions