How to draw triangle using Core Graphics - Mac? - objective-c

I want something like this: (light part, ignore background)
How can I draw this using Cocoa? In drawRect:? I don't know how to draw.

Use NSBezierPath:
- (void)drawRect:(NSRect)rect
{
NSBezierPath *path = [NSBezierPath bezierPath];
[path moveToPoint:NSMakePoint(0, 0)];
[path lineToPoint:NSMakePoint(50, 100)];
[path lineToPoint:NSMakePoint(100, 0)];
[path closePath];
[[NSColor redColor] set];
[path fill];
}
This should get you started, it draws a red triangle on a 100x100 sized view. You'd usually calculate the coordinates dynamically, based on the view's size, instead of using hard-coded values of course.

iOS + Swift
(1) Create a Swift extension
// Centered, equilateral triangle
extension UIBezierPath {
convenience init(equilateralSide: CGFloat, center: CGPoint) {
self.init()
let altitude = CGFloat(sqrt(3.0) / 2.0 * equilateralSide)
let heightToCenter = altitude / 3
moveToPoint(CGPoint(x:center.x, y:center.y - heightToCenter*2))
addLineToPoint(CGPoint(x:center.x + equilateralSide/2, y:center.y + heightToCenter))
addLineToPoint(CGPoint(x:center.x - equilateralSide/2, y:center.y + heightToCenter))
closePath()
}
}
(2) Override drawRect
override func drawRect(rect: CGRect) {
let path = UIBezierPath(
equilateralSide: self.bounds.size.width,
center: CGPoint(x: self.bounds.size.width/2, y: self.bounds.size.height/2))
self.tintColor.set()
path!.fill()
}

iOS + Swift5 (Updated Context handling, allow for diff widths)
let triangleWidth: CGFloat = 8.0
let heightToCenter: CGFloat = 4.0 // This controls how "narrow" the triangle is
let center: CGPoint = centerPoint
context.setFillColor(UIColor.red.cgColor)
context.move(to: CGPoint(x:center.x, y:center.y - heightToCenter*2))
context.addLine(to: CGPoint(x:center.x + triangleWidth/2, y:center.y + heightToCenter))
context.addLine(to: CGPoint(x:center.x - triangleWidth/2, y:center.y + heightToCenter))
context.closePath()
context.fillPath()

Related

Using NSBezierPath addClip - How to invert clip

Using NSBezierPath addClip only limits drawing to inside the path used for clipping. I'd like to do the opposite - Only draw outside.
- (void)drawRect:(NSRect)dirtyRect {
NSBezierPath *dontDrawInThis = ...;
//We want an opposite mask of [dontDrawInThis setClip];
//Drawing comes here
}
This was my solution:
- (void)drawRect:(NSRect)dirtyRect {
NSBezierPath *dontDrawInThis = ...;
// The mask is the whole bounds rect, subtracted dontDrawInThis
NSBezierPath *clip = [NSBezierPath bezierPathWithRect:self.bounds];
[clip appendBezierPath:dontDrawInThis.bezierPathByReversingPath];
[clip setClip];
//Drawing comes here
}
For iOS replace NSRect with CGRect.
Swift version of #avishic's answer
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let restrictedPath = NSBezierPath()
// fill the restricted path with shapes/paths you want transparent...
let fullRect = NSBezierPath(rect: self.bounds)
fullRect.append(restrictedPath.reversed)
fullRect.setClip()
NSColor.blue.setFill()
frame.fill()
}

Make NSBezierPath to stay relative to the NSView on windowResize

In my program I'm drawing some shapes inside an NSView using NSBezierPath. Everything works and looks great except when I resize the window.
Example
Initial drawing
Window resize
Does anyone know what I should use to prevent this square from being anchored to the initial position, but make it readjust relative the the initial scale.
Any help is appreciated!
If you are doing your drawing in drawRect: then the answer is NO. You will need to rebuild and reposition your path each time. What you can do is something along the following lines:
- (void) drawRect:(NSRect)dirtyRect
{
// Assuming that _relPos with x and y having values bewteen 0 and 1.
// To keep the square in the middle of the view you would set _relPos to
// CGPointMake(0.5, 0.5).
CGRect bounds = self.bounds;
CGRect rect;
rect.size.width = 100;
rect.size.height = 100;
rect.origin.x = bounds.origin.x + bounds.size.width * _relPos.x - rect.size.width /2;
rect.origin.y = bounds.origin.y + bounds.size.height * _relPos.y - rect.size.height/2;
NSBezierPath *path = [NSBezierPath bezierPathWithRect:rect];
[[NSColor redColor] set];
path.lineWidth = 2;
[path stroke];
}

How to add shadow to a group of CALayer using CATransform3D

How can I add shadow to a group of CALayers?
I have a "FoldingView"-class which maintans several "SliceView"-classes. Each sliceview's layer will be givven a CATransform3D where I'm using the perspective property (.m34 = 1.0 / -1000).
How can I add a shadow with good visual logic? This is what I've been thinking so far:
I could get the path of each slice and combine these to get a shadow path.
I don't know how the get a path of CALayer when the layer is using CATransform3D
It might work visually, but I'm afraid it won't be totally right if the light is supposed to come from top left.
I could just apply standard CALayer shadow to all layers
It does not look good due to the shadow is overlapping each other
If anyone has any other suggestions or know how to code idea number 1 I'll be very happy! Here is a link to the sample project you see screenshot from.
Download zipped application with code
This seems to work as wanted. This is done per sliceview.
- (UIBezierPath *)shadowPath
{
if(self.progress == 0 && self.position != VGFoldSliceCenter)
{
UIBezierPath *path = [UIBezierPath bezierPath];
return path;
}
else
{
CGPoint topLeft = pointForAnchorPointInRect(CGPointMake(0, 0), self.bounds);
CGPoint topRight = pointForAnchorPointInRect(CGPointMake(1, 0), self.bounds);
CGPoint bottomLeft = pointForAnchorPointInRect(CGPointMake(0, 1), self.bounds);
CGPoint bottomRight = pointForAnchorPointInRect(CGPointMake(1, 1), self.bounds);
CGPoint topLeftTranslated = [self.superview convertPoint:topLeft fromView:self];
CGPoint topRightTranslated = [self.superview convertPoint:topRight fromView:self];
CGPoint bottomLeftTranslated = [self.superview convertPoint:bottomLeft fromView:self];
CGPoint bottomRightTranslated = [self.superview convertPoint:bottomRight fromView:self];
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:topLeftTranslated];
[path addLineToPoint:topRightTranslated];
[path addLineToPoint:bottomRightTranslated];
[path addLineToPoint:bottomLeftTranslated];
[path closePath];
return path;
}
}

Mac OS X: Drawing into an offscreen NSGraphicsContext using CGContextRef C functions has no effect. Why?

Mac OS X 10.7.4
I am drawing into an offscreen graphics context created via +[NSGraphicsContext graphicsContextWithBitmapImageRep:].
When I draw into this graphics context using the NSBezierPath class, everything works as expected.
However, when I draw into this graphics context using the CGContextRef C functions, I see no results of my drawing. Nothing works.
For reasons I won't get into, I really need to draw using the CGContextRef functions (rather than the Cocoa NSBezierPath class).
My code sample is listed below. I am attempting to draw a simple "X". One stroke using NSBezierPath, one stroke using CGContextRef C functions. The first stroke works, the second does not. What am I doing wrong?
NSRect imgRect = NSMakeRect(0.0, 0.0, 100.0, 100.0);
NSSize imgSize = imgRect.size;
NSBitmapImageRep *offscreenRep = [[[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:imgSize.width
pixelsHigh:imgSize.height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bitmapFormat:NSAlphaFirstBitmapFormat
bytesPerRow:0
bitsPerPixel:0] autorelease];
// set offscreen context
NSGraphicsContext *g = [NSGraphicsContext graphicsContextWithBitmapImageRep:offscreenRep];
[NSGraphicsContext setCurrentContext:g];
NSImage *img = [[[NSImage alloc] initWithSize:imgSize] autorelease];
CGContextRef ctx = [g graphicsPort];
// lock and draw
[img lockFocus];
// draw first stroke with Cocoa. this works!
NSPoint p1 = NSMakePoint(NSMaxX(imgRect), NSMinY(imgRect));
NSPoint p2 = NSMakePoint(NSMinX(imgRect), NSMaxY(imgRect));
[NSBezierPath strokeLineFromPoint:p1 toPoint:p2];
// draw second stroke with Core Graphics. This doesn't work!
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 0.0, 0.0);
CGContextAddLineToPoint(ctx, imgSize.width, imgSize.height);
CGContextClosePath(ctx);
CGContextStrokePath(ctx);
[img unlockFocus];
You don't specify how you are looking at the results. I assume you are looking at the NSImage img and not the NSBitmapImageRep offscreenRep.
When you call [img lockFocus], you are changing the current NSGraphicsContext to be a context to draw into img. So, the NSBezierPath drawing goes into img and that's what you see. The CG drawing goes into offscreenRep which you aren't looking at.
Instead of locking focus onto an NSImage and drawing into it, create an NSImage and add the offscreenRep as one of its reps.
NSRect imgRect = NSMakeRect(0.0, 0.0, 100.0, 100.0);
NSSize imgSize = imgRect.size;
NSBitmapImageRep *offscreenRep = [[[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:imgSize.width
pixelsHigh:imgSize.height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bitmapFormat:NSAlphaFirstBitmapFormat
bytesPerRow:0
bitsPerPixel:0] autorelease];
// set offscreen context
NSGraphicsContext *g = [NSGraphicsContext graphicsContextWithBitmapImageRep:offscreenRep];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext:g];
// draw first stroke with Cocoa
NSPoint p1 = NSMakePoint(NSMaxX(imgRect), NSMinY(imgRect));
NSPoint p2 = NSMakePoint(NSMinX(imgRect), NSMaxY(imgRect));
[NSBezierPath strokeLineFromPoint:p1 toPoint:p2];
// draw second stroke with Core Graphics
CGContextRef ctx = [g graphicsPort];
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 0.0, 0.0);
CGContextAddLineToPoint(ctx, imgSize.width, imgSize.height);
CGContextClosePath(ctx);
CGContextStrokePath(ctx);
// done drawing, so set the current context back to what it was
[NSGraphicsContext restoreGraphicsState];
// create an NSImage and add the rep to it
NSImage *img = [[[NSImage alloc] initWithSize:imgSize] autorelease];
[img addRepresentation:offscreenRep];
// then go on to save or view the NSImage
The solution by #Robin Stewart worked well for me. I was able to condense it to an NSImage extension.
extension NSImage {
convenience init(size: CGSize, actions: (CGContext) -> Void) {
self.init(size: size)
lockFocusFlipped(false)
actions(NSGraphicsContext.current!.cgContext)
unlockFocus()
}
}
Usage:
let image = NSImage(size: CGSize(width: 100, height: 100), actions: { ctx in
// Drawing commands here for example:
// ctx.setFillColor(.white)
// ctx.fill(pageRect)
})
I wonder why everyone writes such complicated code for drawing to an image. Unless you care for the exact bitmap representation of an image (and usually you don't!), there is no need to create one. You can just create a blank image and directly draw to it. In that case the system will create an appropriate bitmap representation (or maybe a PDF representation or whatever the system believes to be more suitable for drawing).
The documentation of the init method
- (instancetype)initWithSize:(NSSize)aSize
which exists since MacOS 10.0 and still isn't deprecated, clearly says:
After using this method to initialize an image object, you are
expected to provide the image contents before trying to draw the
image. You might lock focus on the image and draw to the image or you
might explicitly add an image representation that you created.
So here's how I would have written that code:
NSRect imgRect = NSMakeRect(0.0, 0.0, 100.0, 100.0);
NSImage * image = [[NSImage alloc] initWithSize:imgRect.size];
[image lockFocus];
// draw first stroke with Cocoa
NSPoint p1 = NSMakePoint(NSMaxX(imgRect), NSMinY(imgRect));
NSPoint p2 = NSMakePoint(NSMinX(imgRect), NSMaxY(imgRect));
[NSBezierPath strokeLineFromPoint:p1 toPoint:p2];
// draw second stroke with Core Graphics
CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort];
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 0.0, 0.0);
CGContextAddLineToPoint(ctx, imgRect.size.width, imgRect.size.height);
CGContextClosePath(ctx);
CGContextStrokePath(ctx);
[image unlockFocus];
That's all folks.
graphicsPort is actually void *:
#property (readonly) void * graphicsPort
and documented as
The low-level, platform-specific graphics context represented
by the graphic port.
Which may be pretty much everything, but the final note says
In OS X, this is the Core Graphics context,
a CGContextRef object (opaque type).
This property has been deprecated in 10.10 in favor of the new property
#property (readonly) CGContextRef CGContext
which is only available in 10.10 and later. If you have to support older systems, it's fine to still use graphicsPort.
Swift 4: I use this code, which replicates the convenient API from UIKit (but runs on macOS):
public class UIGraphicsImageRenderer {
let size: CGSize
init(size: CGSize) {
self.size = size
}
func image(actions: (CGContext) -> Void) -> NSImage {
let image = NSImage(size: size)
image.lockFocusFlipped(true)
actions(NSGraphicsContext.current!.cgContext)
image.unlockFocus()
return image
}
}
Usage:
let renderer = UIGraphicsImageRenderer(size: imageSize)
let image = renderer.image { ctx in
// Drawing commands here
}
Here are 3 ways of drawing same image (Swift 4).
The method suggested by #Mecki produces an image without blurring artefacts (like blurred curves). But this can be fixed by adjusting CGContext settings (not included in this example).
public struct ImageFactory {
public static func image(size: CGSize, fillColor: NSColor, rounded: Bool = false) -> NSImage? {
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
return drawImage(size: size) { context in
if rounded {
let radius = min(size.height, size.width)
let path = NSBezierPath(roundedRect: rect, xRadius: 0.5 * radius, yRadius: 0.5 * radius).cgPath
context.addPath(path)
context.clip()
}
context.setFillColor(fillColor.cgColor)
context.fill(rect)
}
}
}
extension ImageFactory {
private static func drawImage(size: CGSize, drawingCalls: (CGContext) -> Void) -> NSImage? {
return drawImageInLockedImageContext(size: size, drawingCalls: drawingCalls)
}
private static func drawImageInLockedImageContext(size: CGSize, drawingCalls: (CGContext) -> Void) -> NSImage? {
let image = NSImage(size: size)
image.lockFocus()
guard let context = NSGraphicsContext.current else {
image.unlockFocus()
return nil
}
drawingCalls(context.cgContext)
image.unlockFocus()
return image
}
// Has scalling or antialiasing issues, like blurred curves.
private static func drawImageInBitmapImageContext(size: CGSize, drawingCalls: (CGContext) -> Void) -> NSImage? {
guard let offscreenRep = NSBitmapImageRep(pixelsWide: Int(size.width), pixelsHigh: Int(size.height),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true,
isPlanar: false, colorSpaceName: .deviceRGB) else {
return nil
}
guard let context = NSGraphicsContext(bitmapImageRep: offscreenRep) else {
return nil
}
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = context
drawingCalls(context.cgContext)
NSGraphicsContext.restoreGraphicsState()
let img = NSImage(size: size)
img.addRepresentation(offscreenRep)
return img
}
// Has scalling or antialiasing issues, like blurred curves.
private static func drawImageInCGContext(size: CGSize, drawingCalls: (CGContext) -> Void) -> NSImage? {
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
guard let context = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8,
bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else {
return nil
}
drawingCalls(context)
guard let image = context.makeImage() else {
return nil
}
return NSImage(cgImage: image, size: size)
}
}

NSScroller graphical glitches/lag

I have the following NSScroller subclass that creates a scroll bar with a rounded white knob and no arrows/slot (background):
#implementation IGScrollerVertical
- (void)drawKnob
{
NSRect knobRect = [self rectForPart:NSScrollerKnob];
NSRect newRect = NSMakeRect(knobRect.origin.x, knobRect.origin.y, knobRect.size.width - 4, knobRect.size.height);
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:newRect xRadius:7 yRadius:7];
[[NSColor whiteColor] set];
[path fill];
}
- (void)drawArrow:(NSScrollerArrow)arrow highlightPart:(int)flag
{
// We don't want arrows
}
- (void)drawKnobSlotInRect:(NSRect)rect highlight:(BOOL)highlight
{
// Don't want a knob background
}
#end
This all works fine, except there is a noticable lag when I use the scroller. See this video:
http://twitvid.com/70E7C
I'm confused as to what I'm doing wrong, any suggestions?
Fixed the issue, just had to fill the rect in drawKnobSlotInRect: