简体   繁体   中英

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?

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:

点图像

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:

点图像2

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM