Reputation: 27
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).
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
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
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
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:
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:
Upvotes: 5