├── .gitignore ├── GrafDemo.xcodeproj └── project.pbxproj ├── GrafDemo ├── AppDelegate.h ├── AppDelegate.m ├── ArcEditingView.swift ├── ArcToPointEditingView.swift ├── BNRArcEditingView.h ├── BNRArcEditingView.m ├── BNRArcToPointEditingView.h ├── BNRArcToPointEditingView.m ├── BNRArcTypesWindowController.swift ├── BNRArcTypesWindowController.xib ├── BNRCheckboxBox.h ├── BNRCheckboxBox.m ├── BNRLinesView.h ├── BNRLinesView.m ├── BNRLinesWindowController.swift ├── BNRLinesWindowController.xib ├── BNRPathPartsWindowController.swift ├── BNRPathPartsWindowController.xib ├── BNRPostScriptWindowController.swift ├── BNRPostScriptWindowController.xib ├── BNRRelativeArcEditingView.h ├── BNRRelativeArcEditingView.m ├── BNRSimpleView.h ├── BNRSimpleView.m ├── BNRSimpleWindowController.h ├── BNRSimpleWindowController.m ├── BNRSimpleWindowController.xib ├── BNRTransformsWindowController.h ├── BNRTransformsWindowController.m ├── BNRTransformsWindowController.xib ├── BNRUtilities.h ├── BNRUtilities.m ├── Base.lproj │ └── MainMenu.xib ├── CGContext+Protection.swift ├── CGPath+Utilities.swift ├── ConvenienceView.swift ├── GrafDemo-Bridging-Header.h ├── GrafDemo.entitlements ├── Images.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── PathKey.imageset │ │ ├── Contents.json │ │ └── PathKey.png ├── Info.plist ├── LinesView.swift ├── NSView+CGStuff.swift ├── PDFView.swift ├── PathChunksView.swift ├── PathSamplerView.swift ├── PathWindowController.swift ├── PathWindowController.xib ├── RelativeArcEditingView.swift ├── SimpleView.swift ├── TransformView.swift └── main.m ├── GrafDemoTests ├── GrafDemoTests.m └── Info.plist ├── README.md └── assets ├── arcs-window.png ├── lines-window.png ├── main-menu.png ├── paths-window.png ├── postscript-window.png ├── simple-window.png └── transforms-window.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | build/* 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | *.xcworkspace 12 | !default.xcworkspace 13 | xcuserdata 14 | profile 15 | *.moved-aside 16 | DerivedData 17 | 18 | # Finder droppings 19 | .DS_Store 20 | 21 | # emacs droppings 22 | *~ 23 | \#* 24 | .\#* 25 | 26 | # AppCode droppings 27 | .idea/* 28 | 29 | -------------------------------------------------------------------------------- /GrafDemo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface AppDelegate : NSObject 4 | 5 | 6 | @end 7 | 8 | -------------------------------------------------------------------------------- /GrafDemo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import "BNRSimpleWindowController.h" 4 | #import "GrafDemo-Swift.h" 5 | 6 | @interface AppDelegate () 7 | @property (strong) NSMutableArray *windowControllers; 8 | 9 | @end 10 | 11 | 12 | @implementation AppDelegate 13 | 14 | - (void) applicationDidFinishLaunching: (NSNotification *) aNotification { 15 | self.windowControllers = NSMutableArray.new; 16 | } 17 | 18 | - (void) applicationWillTerminate: (NSNotification *) aNotification { 19 | } 20 | 21 | 22 | - (void) showViewControllerNamed: (NSString *) vcClassName { 23 | Class clas = NSClassFromString(vcClassName); 24 | if (clas == Nil) { 25 | NSString *swiftClassName = [@"GrafDemo." stringByAppendingString: vcClassName]; 26 | clas = NSClassFromString(swiftClassName); 27 | } 28 | assert(clas); 29 | 30 | id wc = [[clas alloc] initWithWindowNibName: vcClassName]; 31 | [wc showWindow: self]; 32 | 33 | [self.windowControllers addObject: wc]; 34 | 35 | } // showViewControllerNamed 36 | 37 | 38 | - (void) hackToGetXcodeToLinkTheClassesIn { 39 | (void)BNRSimpleWindowController.new; 40 | (void)BNRLinesWindowController.new; 41 | } // #ilyxc 42 | 43 | 44 | - (IBAction)showPostScript: (NSButton *) sender { 45 | [self showViewControllerNamed: @"BNRPostScriptWindowController"]; 46 | } // showSimpleView 47 | 48 | 49 | - (IBAction)showSimpleView: (NSButton *) sender { 50 | [self showViewControllerNamed: @"BNRSimpleWindowController"]; 51 | } // showSimpleView 52 | 53 | 54 | - (IBAction) showLinesController: (NSButton *) sender { 55 | [self showViewControllerNamed: @"BNRLinesWindowController"]; 56 | } // showLinesController 57 | 58 | - (IBAction) showPathPartsController: (NSButton *) sender { 59 | [self showViewControllerNamed: @"BNRPathPartsWindowController"]; 60 | } 61 | 62 | - (IBAction) showPathController: (NSButton *) sender { 63 | [self showViewControllerNamed: @"PathWindowController"]; 64 | } // showPathController 65 | 66 | - (IBAction) showArcsController: (NSButton *) sender { 67 | [self showViewControllerNamed: @"BNRArcTypesWindowController"]; 68 | } // showArcsController 69 | 70 | - (IBAction) showTransformsController: (NSButton *) sender { 71 | [self showViewControllerNamed: @"BNRTransformsWindowController"]; 72 | } // showTransformsController 73 | 74 | 75 | 76 | @end // AppDelegate 77 | 78 | -------------------------------------------------------------------------------- /GrafDemo/ArcEditingView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | 4 | class ArcEditingView: NSView { 5 | enum ControlPoint: Int { 6 | case pathStart 7 | case firstSegment 8 | case secondSegment 9 | case pathEnd 10 | 11 | case arcCenter 12 | case radiusHandle 13 | 14 | case count 15 | } 16 | 17 | @objc var radius: CGFloat { 18 | let center = controlPoints[.arcCenter]! 19 | let radiusHandle = controlPoints[.radiusHandle]! 20 | return CGFloat(hypot(Double(center.x - radiusHandle.x), 21 | Double(center.y - radiusHandle.y))) 22 | } 23 | 24 | @objc var center: CGPoint { 25 | return controlPoints[.arcCenter]! 26 | } 27 | 28 | 29 | @objc var startAngle: CGFloat = 0 { 30 | didSet { 31 | needsDisplay = true 32 | } 33 | } 34 | @objc var endAngle: CGFloat = 0 { 35 | didSet { 36 | needsDisplay = true 37 | } 38 | } 39 | @objc var clockwise: Bool = true { 40 | didSet { 41 | needsDisplay = true 42 | } 43 | } 44 | 45 | let boxSize = 4 46 | var trackingPoint: ControlPoint? 47 | var controlPoints = [ControlPoint: CGPoint]() 48 | 49 | private func commonInit(withSize size: CGSize) { 50 | let defaultRadius: CGFloat = 25.0 51 | let margin: CGFloat = 5.0 52 | let lineLength = size.width / 3.0 53 | 54 | startAngle = 3 * (π / 4.0) 55 | endAngle = π / 4.0 56 | clockwise = true 57 | 58 | let midX = size.width / 2.0 59 | let midY = size.height / 2.0 60 | 61 | let leftX = margin 62 | let rightX = size.width - margin 63 | 64 | controlPoints[.pathStart] = CGPoint(x: leftX, y: midY) 65 | controlPoints[.firstSegment] = CGPoint(x: leftX + lineLength, 66 | y: midY) 67 | controlPoints[.secondSegment] = CGPoint(x: rightX - lineLength, 68 | y: midY) 69 | controlPoints[.pathEnd] = CGPoint(x: rightX, y: midY) 70 | controlPoints[.arcCenter] = CGPoint(x: midX, y: midY) 71 | controlPoints[.radiusHandle] = CGPoint(x: midX, 72 | y: midY - defaultRadius) 73 | } 74 | 75 | override init(frame: NSRect) { 76 | super.init(frame: frame) 77 | commonInit(withSize: frame.size) 78 | } 79 | 80 | required init?(coder: NSCoder) { 81 | super.init(coder: coder) 82 | commonInit(withSize: frame.size) 83 | } 84 | 85 | func drawPath() { 86 | let context = currentContext 87 | 88 | let path = CGMutablePath() 89 | 90 | path.move(to: controlPoints[.pathStart]!) 91 | path.addLine(to: controlPoints[.firstSegment]!) 92 | path.addArc(center: controlPoints[.arcCenter]!, 93 | radius: radius, 94 | startAngle: startAngle, 95 | endAngle: endAngle, 96 | clockwise: clockwise) 97 | path.addLine(to: controlPoints[.secondSegment]!) 98 | path.addLine(to: controlPoints[.pathEnd]!) 99 | 100 | context.addPath(path) 101 | context.strokePath() 102 | } 103 | 104 | fileprivate func boxForPoint(_ point: CGPoint) -> CGRect { 105 | let BoxSize: CGFloat = 4.0 106 | let boxxy = CGRect(x: point.x - BoxSize / 2.0, 107 | y: point.y - BoxSize / 2.0, 108 | width: BoxSize, height: BoxSize) 109 | return boxxy 110 | } 111 | 112 | func drawControlPoints() { 113 | let context = currentContext 114 | 115 | context.protectGState { 116 | for (type, point) in controlPoints { 117 | let color: NSColor 118 | switch type { 119 | case .pathStart, .firstSegment, .secondSegment, .pathEnd: 120 | color = NSColor.blue 121 | case .arcCenter: 122 | color = NSColor.red 123 | case .radiusHandle: 124 | color = NSColor.orange 125 | default: 126 | color = NSColor.magenta // it's a Magenta Alert 127 | } 128 | color.set() 129 | context.fill(boxForPoint(point)) 130 | } 131 | } 132 | } 133 | 134 | // Need to dust off the trig book and figure out the proper places to draw 135 | // gray influence lines to beginning/ending angle 136 | func drawInfluenceLines() { 137 | let context = currentContext 138 | 139 | let influenceOverspill: CGFloat = 20.0 // how many points beyond the circle 140 | 141 | context.protectGState { 142 | NSColor.lightGray.set() 143 | let pattern: [CGFloat] = [2.0, 2.0] 144 | context.setLineDash(phase: 0.0, lengths: pattern) 145 | 146 | let radius = Double(self.radius + influenceOverspill) 147 | 148 | let startAngleDouble = Double(startAngle) // I love you Swift. 149 | let endAngleDouble = Double(endAngle) 150 | 151 | let startAnglePoint = CGPoint(x: center.x + CGFloat(radius * cos(startAngleDouble)), 152 | y: center.y + CGFloat(radius * sin(startAngleDouble))) 153 | let endAnglePoint = CGPoint(x: center.x + CGFloat(radius * cos(endAngleDouble)), 154 | y: center.y + CGFloat(radius * sin(endAngleDouble))) 155 | 156 | let startAngleSegments = [center, startAnglePoint] 157 | let endAngleSegments = [center, endAnglePoint] 158 | 159 | context.strokeLineSegments(between: startAngleSegments) 160 | context.strokeLineSegments(between: endAngleSegments) 161 | } 162 | } 163 | 164 | override func draw(_ rect: NSRect) { 165 | NSColor.white.set() 166 | bounds.fill() 167 | NSColor.black.set() 168 | bounds.frame() 169 | 170 | drawInfluenceLines() 171 | drawPath() 172 | drawControlPoints() 173 | } 174 | 175 | override func mouseDown(with event: NSEvent) { 176 | trackingPoint = nil 177 | 178 | let localPoint = convert(event.locationInWindow, from: nil) 179 | 180 | for (type, point) in controlPoints { 181 | let box = boxForPoint(point).insetBy(dx: -10, dy: -10) 182 | 183 | if box.contains(localPoint) { 184 | trackingPoint = type 185 | break 186 | } 187 | } 188 | } 189 | 190 | private func dragTo(point: CGPoint) { 191 | guard let trackingIndex = trackingPoint else { 192 | return 193 | } 194 | 195 | controlPoints[trackingIndex] = point 196 | 197 | needsDisplay = true 198 | } 199 | 200 | override func mouseDragged(with event: NSEvent) { 201 | let localPoint = convert(event.locationInWindow, from: nil) 202 | dragTo(point: localPoint) 203 | } 204 | 205 | override func mouseUp(with event: NSEvent) { 206 | trackingPoint = nil 207 | } 208 | 209 | } 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | -------------------------------------------------------------------------------- /GrafDemo/ArcToPointEditingView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | 4 | class ArcToPointEditingView: NSView { 5 | 6 | enum ControlPoint: Int { 7 | case pathStart 8 | case firstSegment 9 | case secondSegment 10 | case pathEnd 11 | 12 | case control1 13 | case control2 14 | case radiusHandle 15 | 16 | case count 17 | } 18 | 19 | var radius: CGFloat = 0 20 | var control1: CGPoint = CGPoint() 21 | var control2: CGPoint = CGPoint() 22 | 23 | let boxSize = 4 24 | var trackingPoint: ControlPoint? 25 | var controlPoints = [ControlPoint: CGPoint]() 26 | 27 | private func commonInit(withSize size: CGSize) { 28 | let defaultRadius: CGFloat = 25.0 29 | let margin: CGFloat = 5.0 30 | let lineLength = size.width / 3.0 31 | 32 | let midX = size.width / 2.0 33 | let midY = size.height / 2.0 34 | 35 | radius = defaultRadius 36 | control1 = CGPoint(x: 69, y: 163) 37 | control2 = CGPoint(x: 187, y: 61) 38 | 39 | let leftX = margin 40 | let rightX = size.width - margin 41 | 42 | controlPoints[.pathStart] = CGPoint(x: leftX, y: midY) 43 | controlPoints[.firstSegment] = CGPoint(x: leftX + lineLength, y: midY) 44 | controlPoints[.secondSegment] = CGPoint(x: rightX - lineLength, y: midY) 45 | controlPoints[.pathEnd] = CGPoint(x: rightX, y: midY) 46 | controlPoints[.control1] = control1 47 | controlPoints[.control2] = control2 48 | controlPoints[.radiusHandle] = CGPoint(x: midX, y: midY - defaultRadius) 49 | } 50 | 51 | override init(frame: NSRect) { 52 | super.init(frame: frame) 53 | commonInit(withSize: frame.size) 54 | } 55 | 56 | required init?(coder: NSCoder) { 57 | super.init(coder: coder) 58 | commonInit(withSize: frame.size) 59 | } 60 | 61 | func drawPath() { 62 | let context = currentContext 63 | 64 | let path = CGMutablePath() 65 | 66 | path.move(to: controlPoints[.pathStart]!) 67 | path.addLine(to: controlPoints[.firstSegment]!) 68 | path.addArc(tangent1End: controlPoints[.control1]!, 69 | tangent2End: controlPoints[.control2]!, 70 | radius: radius) 71 | path.addLine(to: controlPoints[.secondSegment]!) 72 | path.addLine(to: controlPoints[.pathEnd]!) 73 | 74 | context.addPath(path) 75 | context.strokePath() 76 | } 77 | 78 | fileprivate func boxForPoint(_ point: CGPoint) -> CGRect { 79 | let BoxSize: CGFloat = 4.0 80 | let boxxy = CGRect(x: point.x - BoxSize / 2.0, 81 | y: point.y - BoxSize / 2.0, 82 | width: BoxSize, height: BoxSize) 83 | return boxxy 84 | } 85 | 86 | func drawControlPoints() { 87 | let context = currentContext 88 | 89 | context.protectGState { 90 | for (type, point) in controlPoints { 91 | let color: NSColor 92 | switch type { 93 | case .pathStart, .firstSegment, .secondSegment, .pathEnd: 94 | color = NSColor.blue 95 | case .control1, .control2: 96 | color = NSColor.gray 97 | case .radiusHandle: 98 | color = NSColor.orange 99 | default: 100 | color = NSColor.magenta // it's a Magenta Alert 101 | } 102 | color.set() 103 | context.fill(boxForPoint(point)) 104 | } 105 | } 106 | } 107 | 108 | // Need to dust off the trig book and figure out the proper places to draw 109 | // gray influence lines to beginning/ending angle 110 | func drawInfluenceLines() { 111 | let context = currentContext 112 | 113 | context.protectGState { 114 | NSColor.lightGray.set() 115 | let pattern: [CGFloat] = [2.0, 2.0] 116 | context.setLineDash(phase: 0.0, lengths: pattern) 117 | let midX = self.bounds.midX 118 | let midY = self.bounds.midY 119 | 120 | let radiusSegments: [CGPoint] = [CGPoint(x: midX, y: midY), 121 | controlPoints[.radiusHandle]!] 122 | context.strokeLineSegments(between: radiusSegments) 123 | 124 | let controlSegments: [CGPoint] = [controlPoints[.firstSegment]!, 125 | controlPoints[.control1]!, 126 | controlPoints[.control1]!, 127 | controlPoints[.control2]!] 128 | context.strokeLineSegments(between: controlSegments) 129 | } 130 | } 131 | 132 | override func draw(_ rect: NSRect) { 133 | NSColor.white.set() 134 | bounds.fill() 135 | NSColor.black.set() 136 | bounds.frame() 137 | 138 | drawInfluenceLines() 139 | drawPath() 140 | drawControlPoints() 141 | } 142 | 143 | override func mouseDown(with event: NSEvent) { 144 | trackingPoint = nil 145 | 146 | let localPoint = convert(event.locationInWindow, from: nil) 147 | 148 | for (type, point) in controlPoints { 149 | let box = boxForPoint(point).insetBy(dx: -10, dy: -10) 150 | 151 | if box.contains(localPoint) { 152 | trackingPoint = type 153 | break 154 | } 155 | } 156 | } 157 | 158 | private func dragTo(point: CGPoint) { 159 | guard let trackingIndex = trackingPoint else { 160 | return 161 | } 162 | 163 | if trackingIndex == .radiusHandle { 164 | let midX = bounds.midX 165 | let midY = bounds.midY 166 | radius = CGFloat(hypot(Double(midX - point.x), Double(midY - point.y))) 167 | } 168 | 169 | controlPoints[trackingIndex] = point 170 | 171 | needsDisplay = true 172 | } 173 | 174 | override func mouseDragged(with event: NSEvent) { 175 | let localPoint = convert(event.locationInWindow, from: nil) 176 | dragTo(point: localPoint) 177 | } 178 | 179 | override func mouseUp(with event: NSEvent) { 180 | trackingPoint = nil 181 | } 182 | 183 | } 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /GrafDemo/BNRArcEditingView.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface BNRArcEditingView : NSView 4 | 5 | @property (assign) CGPoint center; 6 | @property (assign) CGFloat radius; 7 | @property (assign) CGFloat startAngle; 8 | @property (assign) CGFloat endAngle; 9 | @property (assign) BOOL clockwise; 10 | 11 | @end // BNRArcEditingView 12 | -------------------------------------------------------------------------------- /GrafDemo/BNRArcEditingView.m: -------------------------------------------------------------------------------- 1 | #import "BNRArcEditingView.h" 2 | #import "BNRUtilities.h" 3 | 4 | typedef enum { 5 | kPathStart = 0, 6 | kFirstSegment, 7 | kSecondSegment, 8 | kPathEnd, 9 | 10 | kArcCenter, 11 | kRadiusHandle, 12 | 13 | kControlPointCount 14 | 15 | } ControlPointIndex; 16 | 17 | static const CGFloat kBoxSize = 4.0; 18 | static const NSInteger kNotTrackingIndex = -1; 19 | 20 | @interface BNRArcEditingView() 21 | @property (assign) NSInteger trackingIndex; 22 | 23 | @end // extension 24 | 25 | @implementation BNRArcEditingView { 26 | CGPoint _controlPoints[kControlPointCount]; 27 | } 28 | @synthesize clockwise = _clockwise; 29 | @synthesize startAngle = _startAngle; 30 | @synthesize endAngle = _endAngle; 31 | 32 | 33 | - (void) commonInitWithSize: (CGSize) size { 34 | self.trackingIndex = kNotTrackingIndex; 35 | 36 | static const CGFloat kDefaultRadius = 25.0; 37 | static const CGFloat kMargin = 5.0; 38 | CGFloat kLineLength = size.width / 3.0; 39 | 40 | _startAngle = 3 * (M_PI / 4.0); 41 | _endAngle = M_PI / 4.0; 42 | _clockwise = YES; 43 | 44 | CGFloat midX = size.width / 2.0; 45 | CGFloat midY = size.height / 2.0; 46 | 47 | CGFloat leftX = kMargin; 48 | CGFloat rightX = size.width - kMargin; 49 | 50 | _controlPoints[kPathStart] = (CGPoint) { leftX, midY}; 51 | _controlPoints[kFirstSegment] = (CGPoint) { leftX + kLineLength, midY }; 52 | _controlPoints[kSecondSegment] = (CGPoint) { rightX - kLineLength, midY }; 53 | _controlPoints[kPathEnd] = (CGPoint) { rightX, midY }; 54 | 55 | _controlPoints[kArcCenter] = (CGPoint) { midX, midY, }; 56 | _controlPoints[kRadiusHandle] = (CGPoint) { midX, midY - kDefaultRadius }; 57 | 58 | } // commonInit 59 | 60 | 61 | - (instancetype) initWithFrame: (CGRect) frame { 62 | if ((self = [super initWithFrame: frame])) { 63 | [self commonInitWithSize: frame.size]; 64 | } 65 | return self; 66 | } // initWithCoder 67 | 68 | 69 | - (CGFloat) radius { 70 | CGFloat radius = hypotf(_controlPoints[kArcCenter].x - _controlPoints[kRadiusHandle].x, 71 | _controlPoints[kArcCenter].y - _controlPoints[kRadiusHandle].y); 72 | return radius; 73 | } // radius 74 | 75 | 76 | - (void) setRadius: (CGFloat) radius { 77 | assert(!"too lazy to actually implement this"); 78 | } // radius 79 | 80 | 81 | // Turd methods just to get a setNeedsDisplay, and we can't 'send super' to the 82 | // compiler's generated version of proper setters :-( 83 | - (CGPoint) center { 84 | return _controlPoints[kArcCenter]; 85 | } // center 86 | 87 | 88 | - (void) setCenter: (CGPoint) center { 89 | _controlPoints[kArcCenter] = center; 90 | [self setNeedsDisplay: YES]; 91 | } // setCenter 92 | 93 | 94 | - (BOOL) clockwise { 95 | return _clockwise; 96 | } // clockwise 97 | 98 | 99 | - (void) setClockwise: (BOOL) clockwise { 100 | _clockwise = clockwise; 101 | [self setNeedsDisplay: YES]; 102 | } // setClockwise 103 | 104 | 105 | - (CGFloat) startAngle { 106 | return _startAngle; 107 | } // startAngle 108 | 109 | 110 | - (void) setStartAngle: (CGFloat) startAngle { 111 | _startAngle = startAngle; 112 | [self setNeedsDisplay: YES]; 113 | } // setStartAngle 114 | 115 | - (CGFloat) endAngle { 116 | return _endAngle; 117 | } // endAngle 118 | 119 | 120 | - (void) setEndAngle: (CGFloat) endAngle { 121 | _endAngle = endAngle; 122 | [self setNeedsDisplay: YES]; 123 | } // setEndAngle 124 | 125 | // Drawing 126 | 127 | - (void) drawPath { 128 | CGContextRef context = CurrentContext (); 129 | CGMutablePathRef path = CGPathCreateMutable (); 130 | 131 | CGPathMoveToPoint (path, nil, 132 | _controlPoints[kPathStart].x, 133 | _controlPoints[kPathStart].y); 134 | CGPathAddLineToPoint (path, nil, 135 | _controlPoints[kFirstSegment].x, 136 | _controlPoints[kFirstSegment].y); 137 | CGPathAddArc (path, nil, self.center.x, self.center.y, self.radius, 138 | self.startAngle, self.endAngle, self.clockwise); 139 | 140 | CGPathAddLineToPoint (path, nil, 141 | _controlPoints[kSecondSegment].x, 142 | _controlPoints[kSecondSegment].y); 143 | CGPathAddLineToPoint (path, nil, 144 | _controlPoints[kPathEnd].x, 145 | _controlPoints[kPathEnd].y); 146 | 147 | CGContextAddPath (context, path); 148 | CGContextStrokePath (context); 149 | 150 | CGPathRelease (path); 151 | 152 | } // drawPath 153 | 154 | 155 | - (CGRect) boxAtPoint: (CGPoint) point { 156 | CGRect rect = (CGRect) { point.x - kBoxSize / 2.0, 157 | point.y - kBoxSize / 2.0, 158 | kBoxSize, kBoxSize }; 159 | return rect; 160 | } // boxAtPont 161 | 162 | 163 | - (void) drawControlPoints { 164 | CGContextRef context = CurrentContext(); 165 | 166 | CGContextSaveGState (context); { 167 | for (ControlPointIndex i = 0; i < kControlPointCount; i++) { 168 | NSColor *color = NSColor.blackColor; 169 | switch (i) { 170 | case kPathStart: 171 | case kFirstSegment: 172 | case kSecondSegment: 173 | case kPathEnd: 174 | color = NSColor.blueColor; 175 | break; 176 | 177 | case kArcCenter: 178 | color = NSColor.redColor; 179 | break; 180 | 181 | case kRadiusHandle: 182 | color = NSColor.orangeColor; 183 | break; 184 | 185 | default: 186 | color = NSColor.magentaColor; // It's a Magenta Alert 187 | } 188 | 189 | [color set]; 190 | CGRect rect = [self boxAtPoint: _controlPoints[i]]; 191 | CGContextAddRect (context, rect); 192 | CGContextFillPath (context); 193 | } 194 | } CGContextRestoreGState (context); 195 | 196 | } // drawControlPoints 197 | 198 | 199 | // Need to dust off the trig book and figure out the proper places to draw 200 | // gray influence lines to beginning/ending angle 201 | - (void) drawInfluenceLines { 202 | CGContextRef context = CurrentContext(); 203 | 204 | static const CGFloat kInfluenceOverspill = 20.0; // how many points beyond the circle 205 | 206 | CGContextSaveGState (context); { 207 | [NSColor.lightGrayColor set]; 208 | CGFloat pattern[] = { 2.0, 2.0 }; 209 | CGContextSetLineDash (context, 0.0, pattern, sizeof(pattern) / sizeof(*pattern)); 210 | 211 | CGFloat radius = self.radius + kInfluenceOverspill; 212 | 213 | CGPoint startAnglePoint = 214 | (CGPoint) { self.center.x + radius * cos(self.startAngle), 215 | self.center.y + radius * sin(self.startAngle) }; 216 | CGPoint endAnglePoint = 217 | (CGPoint) { self.center.x + radius * cos(self.endAngle), 218 | self.center.y + radius * sin(self.endAngle) }; 219 | 220 | CGPoint startAngleSegments[2] = { self.center, startAnglePoint }; 221 | CGPoint endAngleSegments[2] = { self.center, endAnglePoint }; 222 | 223 | CGContextStrokeLineSegments (context, startAngleSegments, 2); 224 | CGContextStrokeLineSegments (context, endAngleSegments, 2); 225 | 226 | } CGContextRestoreGState (context); 227 | 228 | } // drawInfluenceLines 229 | 230 | 231 | - (void) drawRect: (NSRect) dirtyRect { 232 | CGRect bounds = self.bounds; 233 | 234 | [NSColor.whiteColor set]; 235 | NSRectFill (bounds); 236 | 237 | [NSColor.blackColor set]; 238 | NSFrameRect (bounds); 239 | 240 | [self drawInfluenceLines]; 241 | [self drawPath]; 242 | [self drawControlPoints]; 243 | 244 | } // drawRect 245 | 246 | 247 | - (void) startDragWithControlPointIndex: (ControlPointIndex) index { 248 | self.trackingIndex = index; 249 | } // startDragWithControlPointIndex 250 | 251 | 252 | - (void) dragToNewPoint: (CGPoint) point { 253 | 254 | // Pull the radius handle along with the center. 255 | if (self.trackingIndex == kArcCenter) { 256 | CGPoint center = _controlPoints[kArcCenter]; 257 | CGPoint radius = _controlPoints[kRadiusHandle]; 258 | 259 | CGFloat deltaX = radius.x - center.x; 260 | CGFloat deltaY = radius.y - center.y; 261 | 262 | CGPoint newRadius = (CGPoint){ point.x + deltaX, 263 | point.y + deltaY }; 264 | _controlPoints[kRadiusHandle] = newRadius; 265 | } 266 | 267 | _controlPoints[self.trackingIndex] = point; 268 | 269 | [self setNeedsDisplay: YES]; 270 | } // dragToNewPoint 271 | 272 | 273 | - (void) stopDrag { 274 | self.trackingIndex = kNotTrackingIndex; 275 | [self setNeedsDisplay: YES]; 276 | } // stopDrag 277 | 278 | 279 | - (void) mouseDown: (NSEvent *) event { 280 | self.trackingIndex = kNotTrackingIndex; 281 | 282 | CGPoint localPoint = [self convertPoint: event.locationInWindow fromView: nil]; 283 | 284 | for (NSInteger i = 0; i < kControlPointCount; i++) { 285 | CGRect box = [self boxAtPoint: _controlPoints[i]]; 286 | box = CGRectInset(box, -10.0, -10.0); 287 | if (CGRectContainsPoint(box, localPoint)) { 288 | [self startDragWithControlPointIndex: (ControlPointIndex)i]; 289 | break; 290 | } 291 | } 292 | 293 | } // mouseDown 294 | 295 | 296 | - (void) mouseDragged: (NSEvent *) event { 297 | if (self.trackingIndex == kNotTrackingIndex) return; 298 | 299 | CGPoint localPoint = [self convertPoint: event.locationInWindow fromView: nil]; 300 | [self dragToNewPoint: localPoint]; 301 | 302 | } // mouseDragged 303 | 304 | 305 | - (void) mouseUp: (NSEvent *) event { 306 | if (self.trackingIndex == kNotTrackingIndex) return; 307 | 308 | [self stopDrag]; 309 | } // mouseUp 310 | 311 | 312 | @end // BNRArcEditingView 313 | -------------------------------------------------------------------------------- /GrafDemo/BNRArcToPointEditingView.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface BNRArcToPointEditingView : NSView 4 | 5 | @property (assign) CGFloat radius; 6 | @property (assign) CGPoint control1; 7 | @property (assign) CGPoint control2; 8 | 9 | @end // BNRArcToPointEditingView 10 | -------------------------------------------------------------------------------- /GrafDemo/BNRArcToPointEditingView.m: -------------------------------------------------------------------------------- 1 | #import "BNRArcToPointEditingView.h" 2 | #import "BNRUtilities.h" 3 | 4 | typedef enum { 5 | kPathStart = 0, 6 | kFirstSegment, 7 | kSecondSegment, 8 | kPathEnd, 9 | 10 | kControl1, 11 | kControl2, 12 | kRadiusHandle, 13 | 14 | kControlPointCount 15 | 16 | } ControlPointIndex; 17 | 18 | static const CGFloat kBoxSize = 4.0; 19 | static const NSInteger kNotTrackingIndex = -1; 20 | 21 | @interface BNRArcToPointEditingView() 22 | @property (assign) NSInteger trackingIndex; 23 | 24 | @end // extension 25 | 26 | @implementation BNRArcToPointEditingView { 27 | CGPoint _controlPoints[kControlPointCount]; 28 | } 29 | @synthesize control1 = _control1; 30 | @synthesize control2 = _control2; 31 | 32 | 33 | - (void) commonInitWithSize: (CGSize) size { 34 | self.trackingIndex = kNotTrackingIndex; 35 | 36 | static const CGFloat kDefaultRadius = 25.0; 37 | static const CGFloat kMargin = 5.0; 38 | CGFloat kLineLength = size.width / 3.0; 39 | 40 | CGFloat midX = size.width / 2.0; 41 | CGFloat midY = size.height / 2.0; 42 | 43 | _radius = kDefaultRadius; 44 | _control1 = (CGPoint) { 69, 163 }; 45 | _control2 = (CGPoint) { 187, 61 }; 46 | 47 | CGFloat leftX = kMargin; 48 | CGFloat rightX = size.width - kMargin; 49 | 50 | _controlPoints[kPathStart] = (CGPoint) { leftX, midY}; 51 | _controlPoints[kFirstSegment] = (CGPoint) { leftX + kLineLength, midY }; 52 | _controlPoints[kSecondSegment] = (CGPoint) { rightX - kLineLength, midY }; 53 | _controlPoints[kPathEnd] = (CGPoint) { rightX, midY }; 54 | 55 | _controlPoints[kControl1] = _control1; 56 | _controlPoints[kControl2] = _control2; 57 | _controlPoints[kRadiusHandle] = (CGPoint) { midX, midY - kDefaultRadius }; 58 | 59 | } // commonInit 60 | 61 | 62 | - (instancetype) initWithFrame: (CGRect) frame { 63 | if ((self = [super initWithFrame: frame])) { 64 | [self commonInitWithSize: frame.size]; 65 | } 66 | return self; 67 | } // initWithCoder 68 | 69 | 70 | 71 | // Turd methods just to get a setNeedsDisplay, and we can't 'send super' to the 72 | // compiler's generated version of proper setters :-( 73 | 74 | - (CGPoint) control1 { 75 | return _controlPoints[kControl1]; 76 | } // center 77 | 78 | 79 | - (void) setControl1: (CGPoint) control1 { 80 | _controlPoints[kControl1] = control1; 81 | [self setNeedsDisplay: YES]; 82 | } // setCenter 83 | 84 | 85 | - (CGPoint) control2 { 86 | return _controlPoints[kControl2]; 87 | } // center 88 | 89 | 90 | - (void) setControl2: (CGPoint) control2 { 91 | _controlPoints[kControl2] = control2; 92 | [self setNeedsDisplay: YES]; 93 | } // setCenter 94 | 95 | // Drawing 96 | 97 | - (void) drawPath { 98 | CGContextRef context = CurrentContext (); 99 | CGMutablePathRef path = CGPathCreateMutable (); 100 | 101 | CGPathMoveToPoint (path, nil, 102 | _controlPoints[kPathStart].x, 103 | _controlPoints[kPathStart].y); 104 | CGPathAddLineToPoint (path, nil, 105 | _controlPoints[kFirstSegment].x, 106 | _controlPoints[kFirstSegment].y); 107 | CGPathAddArcToPoint (path, nil, self.control1.x, self.control1.y, 108 | self.control2.x, self.control2.y, self.radius); 109 | 110 | CGPathAddLineToPoint (path, nil, 111 | _controlPoints[kSecondSegment].x, 112 | _controlPoints[kSecondSegment].y); 113 | CGPathAddLineToPoint (path, nil, 114 | _controlPoints[kPathEnd].x, 115 | _controlPoints[kPathEnd].y); 116 | 117 | CGContextAddPath (context, path); 118 | CGContextStrokePath (context); 119 | 120 | CGPathRelease (path); 121 | 122 | } // drawPath 123 | 124 | 125 | - (CGRect) boxAtPoint: (CGPoint) point { 126 | CGRect rect = (CGRect) { point.x - kBoxSize / 2.0, 127 | point.y - kBoxSize / 2.0, 128 | kBoxSize, kBoxSize }; 129 | return rect; 130 | } // boxAtPont 131 | 132 | 133 | - (void) drawControlPoints { 134 | CGContextRef context = CurrentContext(); 135 | 136 | CGContextSaveGState (context); { 137 | for (ControlPointIndex i = 0; i < kControlPointCount; i++) { 138 | NSColor *color = NSColor.blackColor; 139 | switch (i) { 140 | case kPathStart: 141 | case kFirstSegment: 142 | case kSecondSegment: 143 | case kPathEnd: 144 | color = NSColor.blueColor; 145 | break; 146 | 147 | case kControl1: 148 | case kControl2: 149 | color = NSColor.grayColor; 150 | break; 151 | 152 | case kRadiusHandle: 153 | color = NSColor.orangeColor; 154 | break; 155 | 156 | default: 157 | color = NSColor.magentaColor; // It's a Magenta Alert 158 | } 159 | 160 | [color set]; 161 | CGRect rect = [self boxAtPoint: _controlPoints[i]]; 162 | CGContextAddRect (context, rect); 163 | CGContextFillPath (context); 164 | } 165 | } CGContextRestoreGState (context); 166 | 167 | } // drawControlPoints 168 | 169 | 170 | // Need to dust off the trig book and figure out the proper places to draw 171 | // gray influence lines to beginning/ending angle 172 | - (void) drawInfluenceLines { 173 | CGContextRef context = CurrentContext(); 174 | 175 | CGContextSaveGState (context); { 176 | [NSColor.lightGrayColor set]; 177 | CGFloat pattern[] = { 2.0, 2.0 }; 178 | CGContextSetLineDash (context, 0.0, pattern, sizeof(pattern) / sizeof(*pattern)); 179 | 180 | CGRect bounds = self.bounds; 181 | 182 | CGFloat midX = CGRectGetMidX(bounds); 183 | CGFloat midY = CGRectGetMidY(bounds); 184 | 185 | CGPoint radiusSegments[2] = { (CGPoint){ midX, midY}, 186 | _controlPoints[kRadiusHandle] }; 187 | CGContextStrokeLineSegments (context, radiusSegments, 2); 188 | 189 | CGPoint controlSegments[4] = 190 | { _controlPoints[kFirstSegment], _controlPoints[kControl1], 191 | _controlPoints[kControl1], _controlPoints[kControl2] }; 192 | CGContextStrokeLineSegments (context, controlSegments, 4); 193 | 194 | } CGContextRestoreGState (context); 195 | 196 | } // drawInfluenceLines 197 | 198 | 199 | - (void) drawRect: (NSRect) dirtyRect { 200 | CGRect bounds = self.bounds; 201 | 202 | [NSColor.whiteColor set]; 203 | NSRectFill (bounds); 204 | 205 | [NSColor.blackColor set]; 206 | NSFrameRect (bounds); 207 | 208 | [self drawInfluenceLines]; 209 | [self drawPath]; 210 | [self drawControlPoints]; 211 | 212 | } // drawRect 213 | 214 | 215 | - (void) startDragWithControlPointIndex: (ControlPointIndex) index { 216 | self.trackingIndex = index; 217 | } // startDragWithControlPointIndex 218 | 219 | 220 | - (void) dragToNewPoint: (CGPoint) point { 221 | 222 | if (self.trackingIndex == kRadiusHandle) { 223 | CGRect bounds = self.bounds; 224 | 225 | CGFloat midX = CGRectGetMidX(bounds); 226 | CGFloat midY = CGRectGetMidY(bounds); 227 | 228 | self.radius = hypotf(midX - point.x, midY - point.y); 229 | } 230 | 231 | _controlPoints[self.trackingIndex] = point; 232 | 233 | [self setNeedsDisplay: YES]; 234 | } // dragToNewPoint 235 | 236 | 237 | - (void) stopDrag { 238 | self.trackingIndex = kNotTrackingIndex; 239 | [self setNeedsDisplay: YES]; 240 | } // stopDrag 241 | 242 | 243 | - (void) mouseDown: (NSEvent *) event { 244 | self.trackingIndex = kNotTrackingIndex; 245 | 246 | CGPoint localPoint = [self convertPoint: event.locationInWindow fromView: nil]; 247 | 248 | for (NSInteger i = 0; i < kControlPointCount; i++) { 249 | CGRect box = [self boxAtPoint: _controlPoints[i]]; 250 | box = CGRectInset(box, -10.0, -10.0); 251 | 252 | if (CGRectContainsPoint(box, localPoint)) { 253 | [self startDragWithControlPointIndex: (ControlPointIndex)i]; 254 | break; 255 | } 256 | } 257 | 258 | } // mouseDown 259 | 260 | 261 | - (void) mouseDragged: (NSEvent *) event { 262 | if (self.trackingIndex == kNotTrackingIndex) return; 263 | 264 | CGPoint localPoint = [self convertPoint: event.locationInWindow fromView: nil]; 265 | [self dragToNewPoint: localPoint]; 266 | 267 | } // mouseDragged 268 | 269 | 270 | - (void) mouseUp: (NSEvent *) event { 271 | if (self.trackingIndex == kNotTrackingIndex) return; 272 | 273 | [self stopDrag]; 274 | } // mouseUp 275 | 276 | 277 | @end // BNRArcToPointEditingView 278 | -------------------------------------------------------------------------------- /GrafDemo/BNRArcTypesWindowController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class BNRArcTypesWindowController: NSWindowController { 4 | 5 | @IBOutlet var arcEditingView: BNRArcEditingView! 6 | 7 | @IBOutlet var startAngleSlider: NSSlider! 8 | @IBOutlet var endAngleSlider: NSSlider! 9 | @IBOutlet var startAngleLabel: NSTextField! 10 | @IBOutlet var endAngleLabel: NSTextField! 11 | 12 | @IBOutlet var relativeArcEditingView: BNRRelativeArcEditingView! 13 | 14 | @IBOutlet var raStartAngleSlider: NSSlider! // ra for relative angle controls 15 | @IBOutlet var raRelativeAngleSlider: NSSlider! 16 | @IBOutlet var raStartAngleLabel: NSTextField! 17 | @IBOutlet var raRelativeAngleLabel: NSTextField! 18 | 19 | 20 | 21 | override func windowDidLoad() { 22 | super.windowDidLoad() 23 | startAngleSlider.floatValue = Float(arcEditingView.startAngle) 24 | endAngleSlider.floatValue = Float(arcEditingView.endAngle) 25 | 26 | raStartAngleSlider.floatValue = Float(relativeArcEditingView.startAngle) 27 | raRelativeAngleSlider.floatValue = Float(relativeArcEditingView.deltaAngle) 28 | 29 | 30 | updateSliderLabels() 31 | } 32 | 33 | fileprivate func updateSliderLabels() { 34 | let startStringValue = NSString(format: "%0.2f", arcEditingView.startAngle) 35 | let endStringValue = NSString(format: "%0.2f", arcEditingView.endAngle) 36 | 37 | startAngleLabel.stringValue = String(startStringValue) 38 | endAngleLabel.stringValue = String(endStringValue) 39 | 40 | 41 | let relativeStartStringValue = NSString(format: "%0.2f", relativeArcEditingView.startAngle) 42 | let relativeDeltaStringValue = NSString(format: "%0.2f", relativeArcEditingView.deltaAngle) 43 | 44 | raStartAngleLabel.stringValue = String(relativeStartStringValue) 45 | raRelativeAngleLabel.stringValue = String(relativeDeltaStringValue) 46 | } 47 | 48 | @IBAction func toggleArcClockwise(_ toggle: NSButton) { 49 | arcEditingView.clockwise = toggle.state == NSControl.StateValue.on 50 | } 51 | 52 | 53 | @IBAction func setStartAngle(_ slider: NSSlider) { 54 | arcEditingView.startAngle = CGFloat(slider.floatValue) 55 | updateSliderLabels() 56 | } 57 | 58 | @IBAction func setEndAngle(_ slider: NSSlider) { 59 | arcEditingView.endAngle = CGFloat(slider.floatValue) 60 | updateSliderLabels() 61 | } 62 | 63 | @IBAction func setRelativeStartAngle(_ slider: NSSlider) { 64 | relativeArcEditingView.startAngle = CGFloat(slider.floatValue) 65 | updateSliderLabels() 66 | } 67 | 68 | @IBAction func setRelativeDeltaAngle(_ slider: NSSlider) { 69 | relativeArcEditingView.deltaAngle = CGFloat(slider.floatValue) 70 | updateSliderLabels() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /GrafDemo/BNRArcTypesWindowController.xib: -------------------------------------------------------------------------------- 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 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /GrafDemo/BNRCheckboxBox.h: -------------------------------------------------------------------------------- 1 | // NSBox subclass with a checkbox for a title that enables/disables its contents. 2 | 3 | #import 4 | 5 | @interface BNRCheckboxBox : NSBox 6 | 7 | @property(assign, getter=isEnabled) BOOL enabled; 8 | 9 | @property(weak) id target; 10 | @property SEL action; 11 | 12 | @end // BNRCheckboxBox 13 | 14 | 15 | -------------------------------------------------------------------------------- /GrafDemo/BNRCheckboxBox.m: -------------------------------------------------------------------------------- 1 | #import "BNRCheckboxBox.h" 2 | 3 | @interface BNRCheckboxBox () 4 | @property(strong) NSButtonCell *buttonCell; // for the checkbox 5 | @end // extension 6 | 7 | 8 | @implementation BNRCheckboxBox 9 | @synthesize enabled = _enabled; 10 | 11 | - (void) awakeFromNib { 12 | [super awakeFromNib]; 13 | self.enabled = YES; 14 | 15 | self.buttonCell = [[NSButtonCell alloc] initTextCell: self.title]; 16 | [self.buttonCell setButtonType: NSSwitchButton]; 17 | self.buttonCell.state = NSControlStateValueOn; 18 | 19 | self.buttonCell.target = self; 20 | self.buttonCell.action = @selector(toggleEnabledState:); 21 | 22 | [self.buttonCell setControlView:self]; 23 | 24 | // TODO(markd): find a better way than whacking the cell directly. 25 | // _titleCell = self.buttonCell; 26 | 27 | } // awakeFromNib 28 | 29 | 30 | - (void) walkView: (NSView *) view settingEnabled: (BOOL) enabled { 31 | if (view != self && [view respondsToSelector: @selector(setEnabled:)]) { 32 | [(id)view setEnabled: enabled]; 33 | } 34 | 35 | if ([view isKindOfClass: NSTextField.class]) { 36 | [(id)view setTextColor: (enabled) ? NSColor.controlTextColor : NSColor.disabledControlTextColor]; 37 | } 38 | 39 | for (NSView *subview in view.subviews) { 40 | [self walkView: subview settingEnabled: enabled]; 41 | } 42 | 43 | } // walkView 44 | 45 | 46 | - (void) setContentEnabledState: (BOOL) enabled { 47 | [self walkView: self settingEnabled: enabled]; 48 | } // setContentEnabledState 49 | 50 | 51 | - (void) toggleEnabledState: (id) sender { 52 | self.enabled = !self.enabled; 53 | [self setContentEnabledState: self.enabled]; 54 | 55 | [NSApp sendAction: self.action to: self.target from: self]; 56 | } // toggleEnabledState 57 | 58 | 59 | - (void) setEnabled: (BOOL) enabled { 60 | _enabled = enabled; 61 | [self setContentEnabledState: enabled]; 62 | self.buttonCell.state = enabled ? NSControlStateValueOn : NSOffState; 63 | } // setEnabled 64 | 65 | 66 | - (BOOL) isEnabled { 67 | return _enabled; 68 | } // isEnabled 69 | 70 | 71 | - (void) mouseDown:(NSEvent *)theEvent { 72 | CGPoint localPoint = [self convertPoint: theEvent.locationInWindow 73 | fromView: nil]; 74 | if (CGRectContainsPoint(self.titleRect, localPoint)) { 75 | // TODO(markd 10-DEC-2014): this doesn't draw properly when tracking the mouse. 76 | [self.titleCell trackMouse: theEvent 77 | inRect: self.titleRect 78 | ofView: self 79 | untilMouseUp: YES]; 80 | } else { 81 | [super mouseDown: theEvent]; 82 | } 83 | 84 | } // mouseDown 85 | 86 | 87 | @end // BNRCheckboxBox 88 | 89 | 90 | 91 | 92 | // Modern AppKits are dying when these calls are being made 93 | @interface NSButtonCell(ToolkitHack) 94 | // Getting a runtime error deep in appkit: 95 | // -[NSButtonCell _isToolbarMode]: unrecognized selector sent to instance 96 | 97 | - (BOOL) _isToolbarMode; 98 | - (void) setTextColor: (NSColor *) color; 99 | 100 | @end // ToolkitHack 101 | 102 | 103 | @implementation NSButtonCell(ToolkitHack) 104 | - (BOOL) _isToolbarMode { 105 | return NO; 106 | } 107 | 108 | 109 | - (void) setTextColor: (NSColor *) color { 110 | // nom 111 | } 112 | 113 | @end // ToolkitHack 114 | -------------------------------------------------------------------------------- /GrafDemo/BNRLinesView.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @class BNRLinesView; 4 | typedef void (^BNRLinesViewPreRenderHook)(BNRLinesView *linesView, CGContextRef cgContext); 5 | 6 | typedef NS_ENUM(NSInteger, BNRLinesViewRenderMode) { 7 | kRenderModeSinglePath, // make one path manually and stroke it 8 | kRenderModeAddLines, // make one path via CGPathAddLines 9 | kRenderModeMultiplePaths, // one stroke per line segment 10 | kRenderModeSegments // use CGContextStrokeLineSegments 11 | }; 12 | 13 | @interface BNRLinesView : NSView 14 | 15 | // OBTW, these are non-atomic because I want to setNeedsDisplay when they change, 16 | // and don't want to jump through KVO hoops, or implement my own atomicity in the setter 17 | // and getter. Granted, atomic guarantees aren't necessary when UI is involved, but 18 | // they tend to be typical in OS X code. 19 | @property (copy, nonatomic) BNRLinesViewPreRenderHook preRenderHook; 20 | @property (assign, nonatomic) BNRLinesViewRenderMode renderMode; 21 | @property (assign, nonatomic) BOOL showLogicalPath; 22 | 23 | @end // NRLinesView 24 | -------------------------------------------------------------------------------- /GrafDemo/BNRLinesView.m: -------------------------------------------------------------------------------- 1 | #import "BNRLinesView.h" 2 | #import "BNRUtilities.h" 3 | 4 | @interface BNRLinesView () 5 | 6 | @property (readonly) CGPoint *points; 7 | @property (assign) NSInteger draggedPoint; 8 | 9 | @end // extension 10 | 11 | // The points for the line are stored in a C array for conveience, to avoid a bunch of 12 | // boxing and unboxing from NSArray. 13 | 14 | static const NSInteger kPointCount = 4; 15 | 16 | static const NSInteger kNoDraggedPoint = -1; 17 | 18 | @implementation BNRLinesView { 19 | CGPoint _pointStorage[kPointCount]; 20 | } 21 | 22 | - (CGPoint *) points { 23 | return _pointStorage; 24 | } 25 | 26 | 27 | - (void) commonInit { 28 | _pointStorage[0] = (CGPoint){ 17.0, 400.0 }; 29 | _pointStorage[1] = (CGPoint){ 175.0, 20.0 }; 30 | _pointStorage[2] = (CGPoint){ 330.0, 275.0 }; 31 | _pointStorage[3] = (CGPoint){ 150.0, 371.0 }; 32 | 33 | _showLogicalPath = YES; 34 | } // commonInit 35 | 36 | 37 | - (instancetype) initWithCoder: (NSCoder *) coder { 38 | if ((self = [super initWithCoder: coder])) { 39 | [self commonInit]; 40 | } 41 | 42 | return self; 43 | } // initWithCoder 44 | 45 | 46 | - (instancetype) initWithFrame: (CGRect) frame { 47 | if ((self = [super initWithFrame: frame])) { 48 | [self commonInit]; 49 | } 50 | 51 | return self; 52 | } // initWithCoder 53 | 54 | 55 | - (void) setPreRenderHook: (BNRLinesViewPreRenderHook) hook { 56 | _preRenderHook = [hook copy]; 57 | [self setNeedsDisplay: YES]; 58 | } // setPreRenderHook 59 | 60 | 61 | - (void) setRenderMode: (BNRLinesViewRenderMode) renderMode { 62 | _renderMode = renderMode; 63 | [self setNeedsDisplay: YES]; 64 | } // setRenderMode 65 | 66 | 67 | - (void) setShowLogicalPath: (BOOL) showLogicalPath { 68 | _showLogicalPath = showLogicalPath; 69 | [self setNeedsDisplay: YES]; 70 | } // showLogicalPath 71 | 72 | 73 | 74 | - (void) drawNiceBackground { 75 | CGContextRef context = CurrentContext(); 76 | 77 | CGContextSaveGState (context); { 78 | CGContextSetRGBFillColor (context, 1.0, 1.0, 1.0, 1.0); // White 79 | 80 | CGContextFillRect (context, self.bounds); 81 | } CGContextRestoreGState (context); 82 | 83 | } // drawNiceBackground 84 | 85 | 86 | - (void) drawNiceBorder { 87 | CGContextRef context = CurrentContext(); 88 | 89 | CGContextSaveGState (context); { 90 | CGContextSetRGBStrokeColor (context, 0.0, 0.0, 0.0, 1.0); // Black 91 | CGContextStrokeRect (context, self.bounds); 92 | } CGContextRestoreGState (context); 93 | 94 | } // drawNiceBorder 95 | 96 | 97 | 98 | - (void) renderAsSinglePath { 99 | CGContextRef context = CurrentContext(); 100 | 101 | CGMutablePathRef path = CGPathCreateMutable(); 102 | 103 | CGPathMoveToPoint (path, NULL, self.points[0].x, self.points[0].y); 104 | 105 | for (NSInteger i = 1; i < kPointCount; i++) { 106 | CGPathAddLineToPoint (path, NULL, self.points[i].x, self.points[i].y); 107 | } 108 | 109 | CGContextAddPath (context, path); 110 | CGContextStrokePath (context); 111 | 112 | CGPathRelease (path); 113 | 114 | } // renderAsSinglePath 115 | 116 | 117 | - (void) renderAsSinglePathByAddingLines { 118 | CGContextRef context = CurrentContext(); 119 | 120 | CGMutablePathRef path = CGPathCreateMutable(); 121 | CGPathAddLines (path, NULL, self.points, kPointCount); 122 | 123 | CGContextAddPath (context, path); 124 | CGContextStrokePath (context); 125 | 126 | CGPathRelease (path); 127 | 128 | } // renderAsSinglePathByAddingLines 129 | 130 | 131 | - (void) renderAsMultiplePaths { 132 | CGContextRef context = CurrentContext(); 133 | 134 | for (NSInteger i = 0; i < kPointCount - 1; i++) { 135 | CGMutablePathRef path = CGPathCreateMutable(); 136 | CGPathMoveToPoint (path, NULL, self.points[i].x, self.points[i].y); 137 | CGPathAddLineToPoint (path, NULL, self.points[i + 1].x, self.points[i + 1].y); 138 | 139 | CGContextAddPath (context, path); 140 | CGContextStrokePath (context); 141 | 142 | CGPathRelease(path); 143 | } 144 | 145 | } // renderAsMultiplePaths 146 | 147 | 148 | - (void) renderAsSegments { 149 | CGContextRef context = CurrentContext(); 150 | 151 | CGPoint segments[kPointCount * 2]; 152 | CGPoint *scan = segments; 153 | 154 | for (NSInteger i = 0; i < kPointCount - 1; i++) { 155 | *scan++ = self.points[i]; 156 | *scan++ = self.points[i + 1]; 157 | } 158 | 159 | // Strokes points 0->1 2->3 4->5 160 | CGContextStrokeLineSegments (context, segments, kPointCount * 2); 161 | 162 | } // renderAsSegments 163 | 164 | 165 | 166 | - (void) renderPath { 167 | 168 | switch (self.renderMode) { 169 | case kRenderModeSinglePath: 170 | [self renderAsSinglePath]; 171 | break; 172 | case kRenderModeAddLines: 173 | [self renderAsSinglePathByAddingLines]; 174 | break; 175 | case kRenderModeMultiplePaths: 176 | [self renderAsMultiplePaths]; 177 | break; 178 | case kRenderModeSegments: 179 | [self renderAsSegments]; 180 | break; 181 | } 182 | 183 | } // renderPath 184 | 185 | 186 | - (void) drawRect: (NSRect) dirtyRect { 187 | CGContextRef context = CurrentContext(); 188 | 189 | [super drawRect: dirtyRect]; 190 | 191 | [self drawNiceBackground]; 192 | 193 | CGContextSaveGState (context); { 194 | [NSColor.greenColor set]; 195 | 196 | if (self.preRenderHook) { 197 | self.preRenderHook (self, CurrentContext()); 198 | } 199 | [self renderPath]; 200 | 201 | } CGContextRestoreGState (context); 202 | 203 | if (self.showLogicalPath) { 204 | CGContextSetRGBStrokeColor (context, 1.0, 1.0, 1.0, 1.0); // White 205 | [self renderPath]; 206 | } 207 | 208 | [self drawNiceBorder]; 209 | 210 | } // drawRect 211 | 212 | 213 | // Behave more like iOS, or most sane toolkits. 214 | - (BOOL) isFlipped { 215 | return YES; 216 | } // isFlipped 217 | 218 | 219 | // -------------------------------------------------- 220 | 221 | 222 | // Which point of the multi-segment line is close to the mouse point? 223 | - (NSInteger) pointIndexForMouse: (CGPoint) mousePoint { 224 | NSInteger index = kNoDraggedPoint; 225 | 226 | static const CGFloat kClickTolerance = 10.0; 227 | 228 | for (NSInteger i = 0; i < kPointCount; i++) { 229 | CGFloat distance = hypotf(mousePoint.x - self.points[i].x, 230 | mousePoint.y - self.points[i].y); 231 | if (distance < kClickTolerance) { 232 | index = i; 233 | break; 234 | } 235 | } 236 | 237 | return index; 238 | } // mousePoint 239 | 240 | 241 | - (void) mouseDown: (NSEvent *) event { 242 | CGPoint localPoint = [self convertPoint: event.locationInWindow 243 | fromView: nil]; 244 | self.draggedPoint = [self pointIndexForMouse: localPoint]; 245 | [self setNeedsDisplay: YES]; 246 | } // mouseDown 247 | 248 | 249 | - (void) mouseDragged: (NSEvent *) event { 250 | if (self.draggedPoint != kNoDraggedPoint) { 251 | CGPoint localPoint = [self convertPoint: event.locationInWindow 252 | fromView: nil]; 253 | self.points[self.draggedPoint] = localPoint; 254 | [self setNeedsDisplay: YES]; 255 | } 256 | 257 | } // mouseDragged 258 | 259 | 260 | - (void) mouseUp: (NSEvent *) event { 261 | self.draggedPoint = kNoDraggedPoint; 262 | } // mouseUp 263 | 264 | 265 | @end // BNRLinesView 266 | 267 | -------------------------------------------------------------------------------- /GrafDemo/BNRLinesWindowController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | open class BNRLinesWindowController: NSWindowController { 4 | @IBOutlet weak var linesView: BNRLinesView! 5 | @IBOutlet weak var swiftLinesView: LinesView! 6 | 7 | // Line Attributes box 8 | @IBOutlet weak var lineWidthSlider: NSSlider! 9 | @IBOutlet weak var miterLimitSlider: NSSlider! 10 | @IBOutlet weak var endCapPopUp: NSPopUpButton! 11 | @IBOutlet weak var lineJoinPopUp: NSPopUpButton! 12 | @IBOutlet weak var renderModePopUp: NSPopUpButton! 13 | @IBOutlet weak var lineAlphaCheckbox : NSButton! 14 | 15 | // Line phase box 16 | @IBOutlet weak var linePhaseBox: BNRCheckboxBox! 17 | @IBOutlet weak var linePhaseSlider: NSSlider! 18 | @IBOutlet weak var dash0Slider: NSSlider! 19 | @IBOutlet weak var space0Slider: NSSlider! 20 | @IBOutlet weak var dash1Slider: NSSlider! 21 | @IBOutlet weak var space1Slider: NSSlider! 22 | @IBOutlet weak var dash2Slider: NSSlider! 23 | @IBOutlet weak var space2Slider: NSSlider! 24 | 25 | open override func awakeFromNib() { 26 | linesView.preRenderHook = { 27 | linesView, cgContext in 28 | self.setupContext(cgContext) 29 | } 30 | 31 | swiftLinesView.preRenderHook = { 32 | linesView, cgContext in 33 | self.setupContext(cgContext) 34 | } 35 | 36 | linePhaseBox.target = self 37 | linePhaseBox.action = #selector(BNRLinesWindowController.refreshViews(_:)) 38 | linePhaseBox.isEnabled = false 39 | } 40 | 41 | // Called by the line view prior to constructing and stroking the example path 42 | open func setupContext(_ context: CGContext!) { 43 | context.setLineWidth(CGFloat(lineWidthSlider.floatValue)) 44 | context.setMiterLimit(CGFloat(miterLimitSlider.floatValue)) 45 | context.setLineCap(CGLineCap(rawValue: Int32(endCapPopUp.indexOfSelectedItem))!) 46 | context.setLineJoin(CGLineJoin(rawValue: Int32(lineJoinPopUp.indexOfSelectedItem))!) 47 | 48 | if self.lineAlphaCheckbox.state == NSControl.StateValue.on { 49 | NSColor.blue.withAlphaComponent(0.50).set() 50 | } else { 51 | NSColor.blue.set() 52 | } 53 | 54 | if linePhaseBox.isEnabled { 55 | let phase = CGFloat(linePhaseSlider.floatValue) 56 | let lengths = [ 57 | dash0Slider.floatValue, space0Slider.floatValue, 58 | dash1Slider.floatValue, space1Slider.floatValue, 59 | dash2Slider.floatValue, space2Slider.floatValue 60 | ].map { CGFloat($0) } 61 | 62 | context.setLineDash(phase: phase, lengths: lengths) 63 | } 64 | } 65 | 66 | // A change was made to a control that affects what the render hook uses. 67 | // Don't care what the control was, just cause a redraw to happen. 68 | @IBAction func refreshViews(_ smarf: NSControl) { 69 | linesView.needsDisplay = true 70 | swiftLinesView.needsDisplay = true 71 | } 72 | 73 | // Two of the checkboxes actually change the lines view configuration. 74 | @IBAction func toggleShowLogicalPath(_ sender: NSButton) { 75 | linesView.showLogicalPath = (sender.state == NSControl.StateValue.on) 76 | swiftLinesView.showLogicalPath = (sender.state == NSControl.StateValue.on) 77 | } 78 | 79 | @IBAction func changeRenderMode(_ sender: NSPopUpButton) { 80 | linesView.renderMode = BNRLinesViewRenderMode(rawValue: sender.indexOfSelectedItem)! 81 | swiftLinesView.renderMode = LinesView.RenderMode(rawValue: sender.indexOfSelectedItem)! 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /GrafDemo/BNRPathPartsWindowController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class BNRPathPartsWindowController: NSWindowController { 4 | @IBOutlet var rectView: ConvenienceView! 5 | @IBOutlet var ovalView: ConvenienceView! 6 | @IBOutlet var roundedRectView: ConvenienceView! 7 | 8 | @IBOutlet var lineToView: PathChunksView! 9 | @IBOutlet var quadCurveView: PathChunksView! 10 | @IBOutlet var bezierCurveView: PathChunksView! 11 | 12 | override func windowDidLoad() { 13 | super.windowDidLoad() 14 | 15 | rectView.type = .rect 16 | ovalView.type = .oval 17 | roundedRectView.type = .roundedRect 18 | 19 | lineToView.type = .lineTo 20 | quadCurveView.type = .quadCurve 21 | bezierCurveView.type = .bezierCurve 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /GrafDemo/BNRPathPartsWindowController.xib: -------------------------------------------------------------------------------- 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 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /GrafDemo/BNRPostScriptWindowController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | let initialText = "" + 4 | "/ComicSansMS findfont\n" + 5 | "40 scalefont\n" + 6 | "setfont\n" + 7 | "\n" + 8 | "20 50 translate\n" + 9 | "30 rotate\n" + 10 | "2.5 1 scale\n" + 11 | "\n" + 12 | "newpath\n" + 13 | "0 0 moveto\n" + 14 | "(Bork) true charpath\n" + 15 | "0.9 setgray\n" + 16 | "fill\n" + 17 | "\n" + 18 | "newpath\n" + 19 | "0 0 moveto\n" + 20 | "(Bork) true charpath\n" + 21 | "0.3 setgray\n" + 22 | "1 setlinewidth\n" + 23 | "stroke\n" 24 | 25 | 26 | 27 | class BNRPostScriptWindowController: NSWindowController { 28 | 29 | @IBOutlet var codeText : NSTextView! 30 | @IBOutlet var pdfView : PDFView! 31 | 32 | override func windowDidLoad() { 33 | super.windowDidLoad() 34 | self.codeText.string = initialText 35 | } 36 | 37 | @IBAction func draw(_: AnyObject) { 38 | var callbacks = CGPSConverterCallbacks() 39 | guard let converter = CGPSConverter(info: nil, callbacks: &callbacks, options: nil) else { 40 | return 41 | } 42 | 43 | guard let codeData = self.codeText.string.data(using: .utf8), 44 | let provider = CGDataProvider(data: codeData as CFData) else { 45 | return 46 | } 47 | 48 | let nsdata = NSMutableData(data: codeData) 49 | 50 | guard let consumer = CGDataConsumer(data: nsdata as CFMutableData) else { 51 | return 52 | } 53 | 54 | let converted = converter.convert(provider, consumer: consumer, options: nil) 55 | if !converted { 56 | print("Could not convert postscript text") 57 | } 58 | 59 | let pdfDataProvider = CGDataProvider(data: nsdata) 60 | let pdf = CGPDFDocument(pdfDataProvider!) 61 | self.pdfView.pdfDocument = pdf 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /GrafDemo/BNRPostScriptWindowController.xib: -------------------------------------------------------------------------------- 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 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /GrafDemo/BNRRelativeArcEditingView.h: -------------------------------------------------------------------------------- 1 | // Totally copy and pasted from BNRArcEditingView - refactor after CocoaConf/Columbus. 2 | 3 | #import 4 | 5 | @interface BNRRelativeArcEditingView : NSView 6 | 7 | @property (assign) CGPoint center; 8 | @property (assign) CGFloat radius; 9 | @property (assign) CGFloat startAngle; 10 | @property (assign) CGFloat deltaAngle; 11 | 12 | @end // BNRArcEditingView 13 | -------------------------------------------------------------------------------- /GrafDemo/BNRRelativeArcEditingView.m: -------------------------------------------------------------------------------- 1 | #import "BNRRelativeArcEditingView.h" 2 | #import "BNRUtilities.h" 3 | 4 | typedef enum { 5 | kPathStart = 0, 6 | kFirstSegment, 7 | kSecondSegment, 8 | kPathDelta, 9 | 10 | kArcCenter, 11 | kRadiusHandle, 12 | 13 | kControlPointCount 14 | 15 | } ControlPointIndex; 16 | 17 | static const CGFloat kBoxSize = 4.0; 18 | static const NSInteger kNotTrackingIndex = -1; 19 | 20 | @interface BNRRelativeArcEditingView() 21 | @property (assign) NSInteger trackingIndex; 22 | 23 | @end // extension 24 | 25 | @implementation BNRRelativeArcEditingView { 26 | CGPoint _controlPoints[kControlPointCount]; 27 | } 28 | 29 | @synthesize startAngle = _startAngle; 30 | @synthesize deltaAngle = _deltaAngle; 31 | 32 | 33 | - (void) commonInitWithSize: (CGSize) size { 34 | self.trackingIndex = kNotTrackingIndex; 35 | 36 | static const CGFloat kDefaultRadius = 25.0; 37 | static const CGFloat kMargin = 5.0; 38 | CGFloat kLineLength = size.width / 3.0; 39 | 40 | _startAngle = 3 * (M_PI / 4.0); 41 | _deltaAngle = -M_PI / 2.0; 42 | 43 | CGFloat midX = size.width / 2.0; 44 | CGFloat midY = size.height / 2.0; 45 | 46 | CGFloat leftX = kMargin; 47 | CGFloat rightX = size.width - kMargin; 48 | 49 | _controlPoints[kPathStart] = (CGPoint) { leftX, midY}; 50 | _controlPoints[kFirstSegment] = (CGPoint) { leftX + kLineLength, midY }; 51 | _controlPoints[kSecondSegment] = (CGPoint) { rightX - kLineLength, midY }; 52 | _controlPoints[kPathDelta] = (CGPoint) { rightX, midY }; 53 | 54 | _controlPoints[kArcCenter] = (CGPoint) { midX, midY, }; 55 | _controlPoints[kRadiusHandle] = (CGPoint) { midX, midY - kDefaultRadius }; 56 | 57 | } // commonInit 58 | 59 | 60 | - (instancetype) initWithFrame: (CGRect) frame { 61 | if ((self = [super initWithFrame: frame])) { 62 | [self commonInitWithSize: frame.size]; 63 | } 64 | return self; 65 | } // initWithCoder 66 | 67 | 68 | - (CGFloat) radius { 69 | CGFloat radius = hypotf(_controlPoints[kArcCenter].x - _controlPoints[kRadiusHandle].x, 70 | _controlPoints[kArcCenter].y - _controlPoints[kRadiusHandle].y); 71 | return radius; 72 | } // radius 73 | 74 | 75 | - (void) setRadius: (CGFloat) radius { 76 | assert(!"too lazy to actually implement this"); 77 | } // radius 78 | 79 | 80 | // Turd methods just to get a setNeedsDisplay, and we can't 'sdelta super' to the 81 | // compiler's generated version of proper setters :-( 82 | - (CGPoint) center { 83 | return _controlPoints[kArcCenter]; 84 | } // center 85 | 86 | 87 | - (void) setCenter: (CGPoint) center { 88 | _controlPoints[kArcCenter] = center; 89 | [self setNeedsDisplay: YES]; 90 | } // setCenter 91 | 92 | 93 | - (CGFloat) startAngle { 94 | return _startAngle; 95 | } // startAngle 96 | 97 | 98 | - (void) setStartAngle: (CGFloat) startAngle { 99 | _startAngle = startAngle; 100 | [self setNeedsDisplay: YES]; 101 | } // setStartAngle 102 | 103 | - (CGFloat) deltaAngle { 104 | return _deltaAngle; 105 | } // deltaAngle 106 | 107 | 108 | - (void) setDeltaAngle: (CGFloat) deltaAngle { 109 | _deltaAngle = deltaAngle; 110 | [self setNeedsDisplay: YES]; 111 | } // setDeltaAngle 112 | 113 | // Drawing 114 | 115 | - (void) drawPath { 116 | CGContextRef context = CurrentContext (); 117 | CGMutablePathRef path = CGPathCreateMutable (); 118 | 119 | CGPathMoveToPoint (path, nil, 120 | _controlPoints[kPathStart].x, 121 | _controlPoints[kPathStart].y); 122 | CGPathAddLineToPoint (path, nil, 123 | _controlPoints[kFirstSegment].x, 124 | _controlPoints[kFirstSegment].y); 125 | CGPathAddRelativeArc (path, nil, self.center.x, self.center.y, self.radius, 126 | self.startAngle, self.deltaAngle); 127 | 128 | CGPathAddLineToPoint (path, nil, 129 | _controlPoints[kSecondSegment].x, 130 | _controlPoints[kSecondSegment].y); 131 | CGPathAddLineToPoint (path, nil, 132 | _controlPoints[kPathDelta].x, 133 | _controlPoints[kPathDelta].y); 134 | 135 | CGContextAddPath (context, path); 136 | CGContextStrokePath (context); 137 | 138 | CGPathRelease (path); 139 | 140 | } // drawPath 141 | 142 | 143 | - (CGRect) boxAtPoint: (CGPoint) point { 144 | CGRect rect = (CGRect) { point.x - kBoxSize / 2.0, 145 | point.y - kBoxSize / 2.0, 146 | kBoxSize, kBoxSize }; 147 | return rect; 148 | } // boxAtPont 149 | 150 | 151 | - (void) drawControlPoints { 152 | CGContextRef context = CurrentContext(); 153 | 154 | CGContextSaveGState (context); { 155 | for (ControlPointIndex i = 0; i < kControlPointCount; i++) { 156 | NSColor *color = NSColor.blackColor; 157 | switch (i) { 158 | case kPathStart: 159 | case kFirstSegment: 160 | case kSecondSegment: 161 | case kPathDelta: 162 | color = NSColor.blueColor; 163 | break; 164 | 165 | case kArcCenter: 166 | color = NSColor.redColor; 167 | break; 168 | 169 | case kRadiusHandle: 170 | color = NSColor.orangeColor; 171 | break; 172 | 173 | default: 174 | color = NSColor.magentaColor; // It's a Magenta Alert 175 | } 176 | 177 | [color set]; 178 | CGRect rect = [self boxAtPoint: _controlPoints[i]]; 179 | CGContextAddRect (context, rect); 180 | CGContextFillPath (context); 181 | } 182 | } CGContextRestoreGState (context); 183 | 184 | } // drawControlPoints 185 | 186 | 187 | // Need to dust off the trig book and figure out the proper places to draw 188 | // gray influence lines to beginning/deltaing angle 189 | - (void) drawInfluenceLines { 190 | CGContextRef context = CurrentContext(); 191 | 192 | static const CGFloat kInfluenceOverspill = 20.0; // how many points beyond the circle 193 | 194 | CGContextSaveGState (context); { 195 | [NSColor.lightGrayColor set]; 196 | CGFloat pattern[] = { 2.0, 2.0 }; 197 | CGContextSetLineDash (context, 0.0, pattern, sizeof(pattern) / sizeof(*pattern)); 198 | 199 | CGFloat radius = self.radius + kInfluenceOverspill; 200 | CGFloat deltaAngle = self.startAngle + self.deltaAngle; 201 | 202 | CGPoint startAnglePoint = 203 | (CGPoint) { self.center.x + radius * cos(self.startAngle), 204 | self.center.y + radius * sin(self.startAngle) }; 205 | CGPoint deltaAnglePoint = 206 | (CGPoint) { self.center.x + radius * cos(deltaAngle), 207 | self.center.y + radius * sin(deltaAngle) }; 208 | 209 | CGPoint startAngleSegments[2] = { self.center, startAnglePoint }; 210 | CGPoint deltaAngleSegments[2] = { self.center, deltaAnglePoint }; 211 | 212 | CGContextStrokeLineSegments (context, startAngleSegments, 2); 213 | CGContextStrokeLineSegments (context, deltaAngleSegments, 2); 214 | 215 | } CGContextRestoreGState (context); 216 | 217 | } // drawInfluenceLines 218 | 219 | 220 | - (void) drawRect: (NSRect) dirtyRect { 221 | CGRect bounds = self.bounds; 222 | 223 | [NSColor.whiteColor set]; 224 | NSRectFill (bounds); 225 | 226 | [NSColor.blackColor set]; 227 | NSFrameRect (bounds); 228 | 229 | [self drawInfluenceLines]; 230 | [self drawPath]; 231 | [self drawControlPoints]; 232 | 233 | } // drawRect 234 | 235 | 236 | - (void) startDragWithControlPointIndex: (ControlPointIndex) index { 237 | self.trackingIndex = index; 238 | } // startDragWithControlPointIndex 239 | 240 | 241 | - (void) dragToNewPoint: (CGPoint) point { 242 | 243 | // Pull the radius handle along with the center. 244 | if (self.trackingIndex == kArcCenter) { 245 | CGPoint center = _controlPoints[kArcCenter]; 246 | CGPoint radius = _controlPoints[kRadiusHandle]; 247 | 248 | CGFloat deltaX = radius.x - center.x; 249 | CGFloat deltaY = radius.y - center.y; 250 | 251 | CGPoint newRadius = (CGPoint){ point.x + deltaX, 252 | point.y + deltaY }; 253 | _controlPoints[kRadiusHandle] = newRadius; 254 | } 255 | 256 | _controlPoints[self.trackingIndex] = point; 257 | 258 | [self setNeedsDisplay: YES]; 259 | } // dragToNewPoint 260 | 261 | 262 | - (void) stopDrag { 263 | self.trackingIndex = kNotTrackingIndex; 264 | [self setNeedsDisplay: YES]; 265 | } // stopDrag 266 | 267 | 268 | - (void) mouseDown: (NSEvent *) event { 269 | self.trackingIndex = kNotTrackingIndex; 270 | 271 | CGPoint localPoint = [self convertPoint: event.locationInWindow fromView: nil]; 272 | 273 | for (NSInteger i = 0; i < kControlPointCount; i++) { 274 | CGRect box = [self boxAtPoint: _controlPoints[i]]; 275 | box = CGRectInset(box, -10.0, -10.0); 276 | if (CGRectContainsPoint(box, localPoint)) { 277 | [self startDragWithControlPointIndex: (ControlPointIndex)i]; 278 | break; 279 | } 280 | } 281 | 282 | } // mouseDown 283 | 284 | 285 | - (void) mouseDragged: (NSEvent *) event { 286 | if (self.trackingIndex == kNotTrackingIndex) return; 287 | 288 | CGPoint localPoint = [self convertPoint: event.locationInWindow fromView: nil]; 289 | [self dragToNewPoint: localPoint]; 290 | 291 | } // mouseDragged 292 | 293 | 294 | - (void) mouseUp: (NSEvent *) event { 295 | if (self.trackingIndex == kNotTrackingIndex) return; 296 | 297 | [self stopDrag]; 298 | } // mouseUp 299 | 300 | 301 | @end // BNRRelativeArcEditingView 302 | -------------------------------------------------------------------------------- /GrafDemo/BNRSimpleView.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface BNRSimpleView : NSView 4 | 5 | // Should drawing be sloppy with graphic saves and restores? 6 | @property (assign, nonatomic) BOOL beSloppy; 7 | 8 | @end // BNRSimpleView 9 | -------------------------------------------------------------------------------- /GrafDemo/BNRSimpleView.m: -------------------------------------------------------------------------------- 1 | #import "BNRSimpleView.h" 2 | 3 | @implementation BNRSimpleView 4 | 5 | - (instancetype) initWithFrame: (CGRect) frame { 6 | if ((self = [super initWithFrame: frame])) { 7 | } 8 | 9 | return self; 10 | 11 | } // initWithFrame 12 | 13 | 14 | - (instancetype) initWithCoder: (NSCoder *) coder { 15 | if ((self = [super initWithCoder: coder])) { 16 | } 17 | 18 | return self; 19 | 20 | } // initWithCoder 21 | 22 | 23 | - (void) setBeSloppy: (BOOL) beSloppy { 24 | _beSloppy = beSloppy; 25 | [self setNeedsDisplay: YES]; 26 | } // setBeSloppy 27 | 28 | 29 | - (CGContextRef) currentContext { 30 | return [NSGraphicsContext.currentContext graphicsPort]; 31 | } // currentContext 32 | 33 | 34 | // -------------------------------------------------- 35 | 36 | 37 | - (void) drawSloppyBackground { 38 | CGContextFillRect (self.currentContext, self.bounds); 39 | } // drawSloppyBackground 40 | 41 | 42 | - (void) drawSloppyContents { 43 | CGContextRef context = self.currentContext; 44 | 45 | CGRect innerRect = CGRectInset (self.bounds, 20, 20); 46 | if (CGRectIsEmpty(innerRect)) return; 47 | 48 | CGContextSetRGBFillColor (context, 0.0, 1.0, 0.0, 1.0); // Green 49 | CGContextFillEllipseInRect (context, innerRect); 50 | 51 | CGContextSetRGBStrokeColor (context, 0.0, 0.0, 1.0, 1.0); // Blue 52 | CGContextSetLineWidth (context, 6.0); 53 | CGContextStrokeEllipseInRect (context, innerRect); 54 | 55 | } // drawSloppyContents 56 | 57 | 58 | - (void) drawSloppyBorder { 59 | CGContextStrokeRect (self.currentContext, self.bounds); 60 | } // drawSloppyBorder 61 | 62 | 63 | - (void) drawSloppily { 64 | CGContextRef context = self.currentContext; 65 | 66 | CGContextSetRGBStrokeColor (context, 0.0, 0.0, 0.0, 1.0); // Black 67 | CGContextSetRGBFillColor (context, 1.0, 1.0, 1.0, 1.0); // White 68 | 69 | [self drawSloppyBackground]; 70 | [self drawSloppyContents]; 71 | [self drawSloppyBorder]; 72 | 73 | } // drawSloppily 74 | 75 | 76 | // -------------------------------------------------- 77 | 78 | - (void) drawNiceBackground { 79 | CGContextRef context = self.currentContext; 80 | 81 | CGContextSaveGState (context); { 82 | CGContextFillRect (context, self.bounds); 83 | } CGContextRestoreGState (context); 84 | } // drawNiceBackground 85 | 86 | 87 | - (void) drawNiceContents { 88 | CGContextRef context = self.currentContext; 89 | CGRect innerRect = CGRectInset (self.bounds, 20, 20); 90 | 91 | if (CGRectIsEmpty(innerRect)) return; 92 | 93 | CGContextSaveGState (context); { 94 | 95 | CGContextSetLineWidth (context, 6.0); 96 | 97 | CGContextSetRGBFillColor (context, 0.0, 1.0, 0.0, 1.0); // Green 98 | CGContextFillEllipseInRect (context, innerRect); 99 | 100 | CGContextSetRGBStrokeColor (context, 0.0, 0.0, 1.0, 1.0); // Blue 101 | CGContextStrokeEllipseInRect (context, innerRect); 102 | 103 | } CGContextRestoreGState (context); 104 | 105 | } // drawNiceContents 106 | 107 | 108 | - (void) drawNiceBorder { 109 | CGContextRef context = self.currentContext; 110 | 111 | CGContextSaveGState (context); { 112 | CGContextStrokeRect (context, self.bounds); 113 | } CGContextRestoreGState (context); 114 | } // drawNiceBorder 115 | 116 | 117 | - (void) drawNicely { 118 | CGContextRef context = self.currentContext; 119 | 120 | // Set the background and border size attributes. 121 | CGContextSetRGBStrokeColor (context, 0.0, 0.0, 0.0, 1.0); // Black 122 | CGContextSetRGBFillColor (context, 1.0, 1.0, 1.0, 1.0); // White 123 | CGContextSetLineWidth (self.currentContext, 3.0); 124 | 125 | [self drawNiceBackground]; 126 | [self drawNiceContents]; 127 | [self drawNiceBorder]; 128 | 129 | } // drawNicely 130 | 131 | 132 | - (void) drawRect: (NSRect) dirtyRect 133 | { 134 | [super drawRect: dirtyRect]; 135 | 136 | if (self.beSloppy) { 137 | [self drawSloppily]; 138 | } else { 139 | [self drawNicely]; 140 | } 141 | 142 | } // drawRect 143 | 144 | @end // BNRSimpleView 145 | 146 | -------------------------------------------------------------------------------- /GrafDemo/BNRSimpleWindowController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface BNRSimpleWindowController : NSWindowController 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /GrafDemo/BNRSimpleWindowController.m: -------------------------------------------------------------------------------- 1 | #import "BNRSimpleWindowController.h" 2 | #import "GrafDemo-Swift.h" 3 | 4 | 5 | #import "BNRSimpleView.h" 6 | 7 | @interface BNRSimpleWindowController () 8 | @property (weak) IBOutlet BNRSimpleView *simpleView; 9 | @property (weak) IBOutlet SimpleView *swSimpleView; 10 | 11 | @end // extension 12 | 13 | 14 | 15 | @implementation BNRSimpleWindowController 16 | 17 | 18 | - (void) loadWindow { 19 | [super loadWindow]; 20 | } 21 | 22 | - (void) windowDidLoad { 23 | [super windowDidLoad]; 24 | } // windowDidLoad 25 | 26 | 27 | 28 | - (IBAction) toggleSloppy: (NSButton *) toggle { 29 | self.simpleView.beSloppy = (toggle.state == NSControlStateValueOn); 30 | self.swSimpleView.beSloppy = (toggle.state == NSControlStateValueOn); 31 | 32 | } // toggleSloppy 33 | 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /GrafDemo/BNRSimpleWindowController.xib: -------------------------------------------------------------------------------- 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 | 31 | 32 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /GrafDemo/BNRTransformsWindowController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface BNRTransformsWindowController : NSWindowController 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /GrafDemo/BNRTransformsWindowController.m: -------------------------------------------------------------------------------- 1 | #import "BNRTransformsWindowController.h" 2 | #import "GrafDemo-Swift.h" 3 | 4 | 5 | @interface BNRTransformsWindowController () 6 | @property (strong) IBOutlet TransformView *transformView; 7 | @end // extension 8 | 9 | 10 | @implementation BNRTransformsWindowController 11 | 12 | - (void)windowDidLoad { 13 | [super windowDidLoad]; 14 | } // windowDidLoad 15 | 16 | 17 | - (IBAction) animate: (NSButton *) sender { 18 | [self.transformView startAnimation]; 19 | } // animate 20 | 21 | 22 | - (IBAction) reset: (NSButton *) sender { 23 | [self.transformView reset]; 24 | } // reset 25 | 26 | 27 | - (IBAction) toggleTranslate: (NSButton *) sender { 28 | self.transformView.shouldTranslate = sender.state == NSControlStateValueOn; 29 | } // toggleTranslate 30 | 31 | 32 | - (IBAction) toggleRotate: (NSButton *) sender { 33 | self.transformView.shouldRotate = sender.state == NSControlStateValueOn; 34 | } // toggleRotate 35 | 36 | 37 | - (IBAction) toggleScale: (NSButton *) sender { 38 | self.transformView.shouldScale = sender.state == NSControlStateValueOn; 39 | } // toggleScale 40 | 41 | 42 | @end // BNRTransformsWindowController 43 | -------------------------------------------------------------------------------- /GrafDemo/BNRTransformsWindowController.xib: -------------------------------------------------------------------------------- 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 | 40 | 51 | 62 | 73 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /GrafDemo/BNRUtilities.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | CGContextRef CurrentContext(); 4 | 5 | NSBezierPath *RanchLogoPath(); 6 | -------------------------------------------------------------------------------- /GrafDemo/CGContext+Protection.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | extension CGContext{ 4 | 5 | func protectGState(_ drawStuff: () -> Void) { 6 | saveGState() 7 | drawStuff() 8 | restoreGState() 9 | } 10 | 11 | } 12 | 13 | -------------------------------------------------------------------------------- /GrafDemo/CGPath+Utilities.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | private typealias CGPathDumpUtility = CGPath 4 | extension CGPathDumpUtility { 5 | 6 | func dump() { 7 | self.apply(info: nil) { info, unsafeElement in 8 | let element = unsafeElement.pointee 9 | let points = element.points.pointee 10 | 11 | switch element.type { 12 | case .moveToPoint: 13 | print("moveto - \(points)") 14 | case .addLineToPoint: 15 | print("lineto - \(points)") 16 | case .addQuadCurveToPoint: 17 | print("quadCurveTo - \(points)") 18 | case .addCurveToPoint: 19 | print("curveTo - \(points)") 20 | case .closeSubpath: 21 | print("close - \(points)") 22 | } 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /GrafDemo/ConvenienceView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | enum ConvenienceType { 4 | case rect 5 | case oval 6 | case roundedRect 7 | } 8 | 9 | 10 | class ConvenienceView: NSView { 11 | 12 | var controlPoints: [CGPoint] = [] 13 | 14 | var draggingIndex: Int? 15 | var type = ConvenienceType.rect 16 | 17 | 18 | func auxiliaryPoints() -> [CGPoint] { 19 | // given the set of CGPoints, return array of points of non-interactive 20 | // control handles 21 | 22 | let topLeft = controlPoints[0] 23 | let bottomRight = controlPoints[1] 24 | 25 | let bottomLeft = CGPoint(x: topLeft.x, y: bottomRight.y) 26 | let topRight = CGPoint(x: bottomRight.x, y: topLeft.y) 27 | 28 | return [bottomLeft, topRight] 29 | } 30 | 31 | func controlPointsCount() -> Int { 32 | // How many control points there are 33 | return type == .roundedRect ? 3 : 2 34 | } 35 | 36 | func initialControlPoints() -> [CGPoint] { 37 | let insetBounds = bounds.insetBy(dx: 15.0, dy: 15.0) 38 | 39 | let topLeft = CGPoint(x: insetBounds.minX, y: insetBounds.minY) 40 | let bottomRight = CGPoint(x: insetBounds.maxX, y: insetBounds.maxY) 41 | 42 | if type == .roundedRect { 43 | let radius = CGPoint(x: topLeft.x + 15, y: topLeft.y + 15) 44 | return [topLeft, bottomRight, radius] 45 | } else { 46 | return [topLeft, bottomRight] 47 | } 48 | } 49 | 50 | 51 | var controlRect: CGRect { 52 | let topLeft = controlPoints[0] 53 | let bottomRight = controlPoints[1] 54 | 55 | let rect = CGRect(x: topLeft.x, y: topLeft.y, 56 | width: bottomRight.x - topLeft.x, 57 | height: bottomRight.y - topLeft.y) 58 | return rect 59 | } 60 | 61 | var controlDistance: CGFloat { 62 | guard type == .roundedRect else { return 0 } 63 | 64 | let topLeft = controlPoints[0] 65 | let radiusPoint = controlPoints[2] 66 | 67 | let xdist = topLeft.x - radiusPoint.x 68 | let ydist = topLeft.y - radiusPoint.y 69 | var controlDistance = sqrt(xdist * xdist + ydist * ydist) 70 | 71 | let rect = controlRect 72 | 73 | controlDistance = min(controlDistance, rect.height / 2) 74 | controlDistance = min(controlDistance, rect.width / 2) 75 | 76 | return controlDistance 77 | } 78 | 79 | 80 | func drawShape() { 81 | 82 | // draw the influence lines 83 | currentContext.protectGState { 84 | NSColor.gray.set() 85 | let pattern: [CGFloat] = [ 1.0, 1.0 ] 86 | currentContext.setLineDash(phase: 0.0, lengths: pattern) 87 | 88 | if type == .roundedRect { 89 | let topLeft = controlPoints[0] 90 | 91 | currentContext.move(to: topLeft) 92 | currentContext.addLine(to: controlPoints[2]) 93 | 94 | currentContext.strokePath() 95 | } 96 | 97 | if type == .roundedRect || type == .oval { 98 | currentContext.stroke(controlRect) 99 | } 100 | } 101 | 102 | 103 | // draw the shape 104 | 105 | let path: CGPath 106 | 107 | switch type { 108 | case .rect: 109 | path = CGPath(rect: controlRect, transform: nil) 110 | case .oval: 111 | path = CGPath(ellipseIn: controlRect, transform: nil) 112 | case .roundedRect: 113 | path = CGPath(roundedRect: controlRect, 114 | cornerWidth: controlDistance, 115 | cornerHeight: controlDistance, 116 | transform: nil) 117 | } 118 | 119 | currentContext.protectGState { 120 | NSColor.black.set() 121 | currentContext.addPath(path) 122 | currentContext.strokePath() 123 | } 124 | } 125 | 126 | fileprivate func boxForPoint(_ point: CGPoint) -> CGRect { 127 | let BoxSize: CGFloat = 4.0 128 | let boxxy = CGRect(x: point.x - BoxSize / 2.0, 129 | y: point.y - BoxSize / 2.0, 130 | width: BoxSize, height: BoxSize) 131 | return boxxy 132 | } 133 | 134 | 135 | fileprivate func drawBoxAt(_ point: CGPoint, color: NSColor, filled: Bool = true) { 136 | let rect = boxForPoint(point); 137 | 138 | currentContext.protectGState { 139 | color.set() 140 | 141 | if filled { 142 | currentContext.addEllipse(in: rect) 143 | currentContext.fillPath() 144 | } else { 145 | currentContext.addRect(rect) 146 | currentContext.strokePath() 147 | } 148 | } 149 | } 150 | 151 | 152 | func drawControlPoints() { 153 | for point in controlPoints { 154 | drawBoxAt(point, color: NSColor.blue) 155 | } 156 | 157 | for point in auxiliaryPoints() { 158 | drawBoxAt(point, color: NSColor.black, filled: false) 159 | } 160 | } 161 | 162 | 163 | override func draw(_ dirtyRect: NSRect) { 164 | if controlPoints.count == 0 { 165 | controlPoints = initialControlPoints() 166 | } 167 | super.draw(dirtyRect) 168 | 169 | NSColor.white.set() 170 | bounds.fill() 171 | 172 | drawShape() 173 | drawControlPoints() 174 | 175 | NSColor.black.set() 176 | bounds.frame() 177 | } 178 | 179 | // Behave more like iOS, or most sane toolkits. 180 | override var isFlipped: Bool { 181 | return true 182 | } 183 | } 184 | 185 | 186 | extension ConvenienceView { 187 | override func mouseDown(with event: NSEvent) { 188 | let localPoint = convert(event.locationInWindow, from: nil) 189 | 190 | for (index, point) in controlPoints.enumerated() { 191 | let box = boxForPoint(point).insetBy(dx: -10.0, dy: -10.0) 192 | if box.contains(localPoint) { 193 | draggingIndex = index 194 | break 195 | } 196 | } 197 | } 198 | 199 | override func mouseDragged(with event: NSEvent) { 200 | guard let index = draggingIndex else { return } 201 | 202 | let localPoint = convert(event.locationInWindow, from: nil) 203 | 204 | controlPoints[index] = localPoint 205 | needsDisplay = true 206 | } 207 | 208 | 209 | override func mouseUp(with event: NSEvent) { 210 | draggingIndex = nil 211 | } 212 | } 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /GrafDemo/GrafDemo-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "BNRUtilities.h" 6 | #import "BNRCheckboxBox.h" 7 | #import "BNRLinesView.h" 8 | #import "BNRArcEditingView.h" 9 | #import "BNRRelativeArcEditingView.h" 10 | -------------------------------------------------------------------------------- /GrafDemo/GrafDemo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /GrafDemo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /GrafDemo/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /GrafDemo/Images.xcassets/PathKey.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "PathKey.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /GrafDemo/Images.xcassets/PathKey.imageset/PathKey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markd2/GrafDemo/a42d5b1800a71e1cec21dcb7b1dcdec44e676627/GrafDemo/Images.xcassets/PathKey.imageset/PathKey.png -------------------------------------------------------------------------------- /GrafDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2014 Big Nerd Ranch. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /GrafDemo/LinesView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class LinesView : NSView { 4 | 5 | enum RenderMode: Int { 6 | case singlePath // make one path manually and stroke it 7 | case addLines // make one path via CGPathAddLines 8 | case multiplePaths // one stroke per line segment 9 | case segments // use CGContextStrokeLineSegments 10 | } 11 | 12 | var preRenderHook: ((LinesView, CGContext) -> ())? { 13 | didSet { 14 | needsDisplay = true 15 | } 16 | } 17 | 18 | var showLogicalPath: Bool = true { 19 | didSet { 20 | needsDisplay = true 21 | } 22 | } 23 | 24 | var renderMode: RenderMode = .singlePath { 25 | didSet { 26 | needsDisplay = true 27 | } 28 | } 29 | 30 | fileprivate var points: [CGPoint] = [ 31 | CGPoint(x: 17, y: 400), 32 | CGPoint(x: 175, y: 20), 33 | CGPoint(x: 330, y: 275), 34 | CGPoint(x: 150, y: 371), 35 | ] 36 | 37 | fileprivate var draggedPointIndex: Int? 38 | 39 | 40 | fileprivate func drawNiceBackground() { 41 | let context = currentContext 42 | 43 | context.protectGState { 44 | context.setFillColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) // White 45 | context.fill(bounds) 46 | } 47 | } 48 | 49 | fileprivate func drawNiceBorder() { 50 | let context = currentContext 51 | 52 | context.protectGState { 53 | context.setStrokeColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) // Black 54 | context.stroke(bounds) 55 | } 56 | } 57 | 58 | 59 | fileprivate func renderAsSinglePath() { 60 | let context = currentContext 61 | let path = CGMutablePath() 62 | 63 | path.move(to: points[0]) 64 | 65 | for i in 1 ..< points.count { 66 | path.addLine(to: points[i]) 67 | } 68 | 69 | context.addPath(path) 70 | context.strokePath() 71 | } 72 | 73 | fileprivate func renderAsSinglePathByAddingLines() { 74 | let context = currentContext 75 | let path = CGMutablePath() 76 | 77 | path.addLines(between: points) 78 | context.addPath(path) 79 | context.strokePath() 80 | } 81 | 82 | fileprivate func renderAsMultiplePaths() { 83 | let context = currentContext 84 | 85 | for i in 0 ..< points.count - 1 { 86 | let path = CGMutablePath() 87 | path.move(to: points[i]) 88 | path.addLine(to: points[i + 1]) 89 | 90 | context.addPath(path) 91 | context.strokePath() 92 | } 93 | } 94 | 95 | fileprivate func renderAsSegments() { 96 | let context = currentContext 97 | 98 | var segments: [CGPoint] = [] 99 | 100 | for i in 0 ..< points.count - 1 { 101 | segments += [points[i]] 102 | segments += [points[i + 1]] 103 | } 104 | 105 | // Strokes points 0->1 2->3 4->5 106 | context.strokeLineSegments(between: segments) 107 | } 108 | 109 | fileprivate func renderPath() { 110 | switch renderMode { 111 | case .singlePath: 112 | renderAsSinglePath() 113 | case .addLines: 114 | renderAsSinglePathByAddingLines() 115 | case .multiplePaths: 116 | renderAsMultiplePaths() 117 | case .segments: 118 | renderAsSegments() 119 | } 120 | } 121 | 122 | 123 | // -------------------------------------------------- 124 | 125 | override func draw(_ dirtyRect: NSRect) { 126 | super.draw(dirtyRect) 127 | 128 | let context = currentContext; 129 | 130 | drawNiceBackground() 131 | 132 | context.protectGState() { 133 | NSColor.green.set() 134 | 135 | if let hook = preRenderHook { 136 | hook(self, context) 137 | } 138 | renderPath() 139 | } 140 | 141 | if (showLogicalPath) { 142 | context.setStrokeColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) // White 143 | renderPath() 144 | } 145 | 146 | drawNiceBorder() 147 | } 148 | 149 | // Behave more like iOS, or most sane toolkits. 150 | override var isFlipped: Bool { 151 | return true 152 | } 153 | 154 | // Which point of the multi-segment line is close to the mouse point? 155 | fileprivate func pointIndexForMouse(_ mousePoint: CGPoint) -> Int? { 156 | let kClickTolerance: Float = 10.0 157 | var pointIndex: Int? = nil 158 | 159 | for (index, point) in points.enumerated() { 160 | let distance = hypotf(Float(mousePoint.x - point.x), 161 | Float(mousePoint.y - point.y)) 162 | if distance < kClickTolerance { 163 | pointIndex = index 164 | break 165 | } 166 | } 167 | 168 | return pointIndex 169 | } 170 | 171 | override func mouseDown(with event: NSEvent) { 172 | let localPoint = convert(event.locationInWindow, from: nil) 173 | 174 | draggedPointIndex = pointIndexForMouse(localPoint) 175 | needsDisplay = true 176 | } 177 | 178 | override func mouseDragged(with event: NSEvent) { 179 | if let pointIndex = draggedPointIndex { 180 | let localPoint = convert(event.locationInWindow, from: nil) 181 | points[pointIndex] = localPoint 182 | needsDisplay = true 183 | } 184 | } 185 | 186 | override func mouseUp(with event: NSEvent) { 187 | draggedPointIndex = nil 188 | } 189 | } 190 | 191 | -------------------------------------------------------------------------------- /GrafDemo/NSView+CGStuff.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | extension NSView { 4 | var currentContext: CGContext { 5 | let context = NSGraphicsContext.current 6 | return context!.cgContext 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /GrafDemo/PDFView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | 4 | class PDFView: NSView { 5 | 6 | var pdfDocument : CGPDFDocument? { 7 | willSet { 8 | needsDisplay = true 9 | } 10 | } 11 | 12 | override func draw(_ dirtyRect: NSRect) { 13 | super.draw(dirtyRect) 14 | 15 | NSColor.white.set() 16 | bounds.fill() 17 | 18 | if let pdf = pdfDocument { 19 | let page1 = pdf.page(at: 1) 20 | 21 | currentContext.drawPDFPage(page1!) 22 | } 23 | 24 | NSColor.black.set() 25 | bounds.frame() 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /GrafDemo/PathChunksView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | // TODO(markd, 1/31/2017) - combine this and ConvenienceView (and maybe Arc views), since 4 | // they have handle-dragging similarities 5 | 6 | 7 | enum ChunkViewType { // Still love ya, Brian 8 | case lineTo 9 | case quadCurve 10 | case bezierCurve 11 | } 12 | 13 | 14 | 15 | class PathChunksView: NSView { 16 | 17 | var controlPoints: [CGPoint] = [] 18 | 19 | var draggingIndex: Int? 20 | var type = ChunkViewType.bezierCurve 21 | 22 | 23 | func controlPointsCount() -> Int { 24 | // How many control points there are 25 | 26 | switch type { 27 | case .lineTo: return 3 28 | case .quadCurve: return 3 29 | case .bezierCurve: return 4 30 | } 31 | } 32 | 33 | func initialControlPoints() -> [CGPoint] { 34 | let insetBounds = bounds.insetBy(dx: 15.0, dy: 15.0) 35 | 36 | let topLeft = CGPoint(x: insetBounds.minX, y: insetBounds.minY) 37 | let bottomRight = CGPoint(x: insetBounds.maxX, y: insetBounds.maxY) 38 | let topRight = CGPoint(x: insetBounds.maxX, y: insetBounds.minY) 39 | let bottomLeft = CGPoint(x: insetBounds.minX, y: insetBounds.maxY) 40 | 41 | let midLeft = CGPoint(x: insetBounds.minX, y: insetBounds.midY) 42 | let midRight = CGPoint(x: insetBounds.maxX, y: insetBounds.midY) 43 | 44 | let midTop = CGPoint(x: insetBounds.midX, y: insetBounds.minY) 45 | let midBottom = CGPoint(x: insetBounds.midX, y: insetBounds.maxY) 46 | 47 | switch type { 48 | case .lineTo: return [ bottomLeft, midTop, bottomRight ] 49 | case .quadCurve: return [ topLeft, midBottom, topRight ] 50 | case .bezierCurve: return [ midBottom, midTop, midLeft, midRight ] 51 | } 52 | } 53 | 54 | 55 | fileprivate let BoxSize: CGFloat = 4.0 56 | 57 | fileprivate func boxForPoint(_ point: CGPoint) -> CGRect { 58 | let boxxy = CGRect(x: point.x - BoxSize / 2.0, 59 | y: point.y - BoxSize / 2.0, 60 | width: BoxSize, height: BoxSize) 61 | return boxxy 62 | } 63 | 64 | 65 | fileprivate func drawBoxAt(_ point: CGPoint, color: NSColor, filled: Bool = true) { 66 | let rect = boxForPoint(point); 67 | let context = currentContext 68 | 69 | context.protectGState { 70 | context.addEllipse(in: rect) 71 | color.set() 72 | 73 | if filled { 74 | context.fillPath() 75 | } else { 76 | context.strokePath() 77 | } 78 | } 79 | } 80 | 81 | 82 | func drawControlPoints() { 83 | for point in controlPoints { 84 | drawBoxAt(point, color: NSColor.blue) 85 | } 86 | } 87 | 88 | 89 | func drawShape() { 90 | // draw the influence lines 91 | let context = currentContext 92 | 93 | context.protectGState { 94 | NSColor.gray.set() 95 | let pattern: [CGFloat] = [ 1.0, 1.0 ] 96 | context.setLineDash(phase: 0.0, lengths: pattern) 97 | 98 | if type == .quadCurve { 99 | context.move(to: controlPoints[0]) 100 | context.addLine(to: controlPoints[2]) 101 | context.addLine(to: controlPoints[1]) 102 | 103 | context.strokePath() 104 | } else if type == .bezierCurve { 105 | context.move(to: controlPoints[0]) 106 | context.addLine(to: controlPoints[2]) 107 | context.addLine(to: controlPoints[3]) 108 | context.addLine(to: controlPoints[1]) 109 | 110 | context.strokePath() 111 | } 112 | } 113 | 114 | 115 | // draw the shape 116 | 117 | context.protectGState { 118 | switch type { 119 | case .lineTo: 120 | context.move(to: controlPoints[0]) 121 | context.addLine(to: controlPoints[1]) 122 | context.addLine(to: controlPoints[2]) 123 | 124 | case .quadCurve: 125 | context.move(to: controlPoints[0]) 126 | context.addQuadCurve(to: controlPoints[1], control: controlPoints[2]) 127 | 128 | case .bezierCurve: 129 | context.move(to: controlPoints[0]) 130 | context.addCurve(to: controlPoints[1], 131 | control1: controlPoints[2], control2: controlPoints[3]) 132 | } 133 | 134 | NSColor.black.set() 135 | context.strokePath() 136 | } 137 | } 138 | 139 | 140 | override func draw(_ dirtyRect: NSRect) { 141 | if controlPoints.count == 0 { 142 | controlPoints = initialControlPoints() 143 | } 144 | super.draw(dirtyRect) 145 | 146 | NSColor.white.set() 147 | bounds.fill() 148 | 149 | drawShape() 150 | drawControlPoints() 151 | 152 | NSColor.black.set() 153 | bounds.frame() 154 | } 155 | 156 | 157 | // Behave more like iOS, or most sane toolkits. 158 | override var isFlipped: Bool { 159 | return true 160 | } 161 | } 162 | 163 | 164 | // Cargo-culted from ConvenienceView 165 | extension PathChunksView { 166 | override func mouseDown(with event: NSEvent) { 167 | let localPoint = convert(event.locationInWindow, from: nil) 168 | 169 | for (index, point) in controlPoints.enumerated() { 170 | let box = boxForPoint(point).insetBy(dx: -10.0, dy: -10.0) 171 | 172 | if box.contains(localPoint) { 173 | draggingIndex = index 174 | break 175 | } 176 | } 177 | } 178 | 179 | override func mouseDragged(with event: NSEvent) { 180 | guard let index = draggingIndex else { return } 181 | 182 | let localPoint = convert(event.locationInWindow, from: nil) 183 | 184 | controlPoints[index] = localPoint 185 | needsDisplay = true 186 | } 187 | 188 | 189 | override func mouseUp(with event: NSEvent) { 190 | draggingIndex = nil 191 | } 192 | } 193 | 194 | -------------------------------------------------------------------------------- /GrafDemo/PathSamplerView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | let π = CGFloat(Double.pi) 4 | 5 | enum ChunkType { 6 | case moveTo(point: CGPoint) 7 | case lineTo(point: CGPoint) 8 | case curveTo(point: CGPoint, control1: CGPoint, control2: CGPoint) 9 | case quadCurveTo(point: CGPoint, control: CGPoint) 10 | case close 11 | case arc(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool) 12 | case arcToPoint(control1: CGPoint, control2: CGPoint, radius: CGFloat) 13 | case relativeArc(center: CGPoint, radius: CGFloat, startAngle: CGFloat, deltaAngle: CGFloat) 14 | 15 | func prettyName() -> String { 16 | switch self { 17 | case .moveTo: return "Move To" 18 | case .lineTo: return "Line To" 19 | case .curveTo: return "Curve To" 20 | case .quadCurveTo: return "Quad Curve To" 21 | case .close: return "Close" 22 | case .arc: return "Arc" 23 | case .arcToPoint: return "Arc To Point" 24 | case .relativeArc: return "Relative Arc" 25 | } 26 | } 27 | 28 | func controlPoints() -> [CGPoint] { 29 | switch self { 30 | case .moveTo(let point): return [point] 31 | case .lineTo(let point): return [point] 32 | case .curveTo(let point, let control1, let control2): return [point, control1, control2] 33 | case .quadCurveTo(let point, let control): return [point, control] 34 | case .close: return [] 35 | case .arc(let center, _, _, _, _): return [center] 36 | case .arcToPoint(let control1, let control2, _): return [control1, control2] 37 | case .relativeArc(let center, _, _, _): return [center] 38 | } 39 | } 40 | 41 | func controlColor() -> NSColor { 42 | switch self { 43 | case .moveTo: return NSColor.yellow 44 | case .lineTo: return NSColor.green 45 | case .curveTo: return NSColor.blue 46 | case .quadCurveTo: return NSColor.orange 47 | case .close: return NSColor.purple 48 | case .arc: return NSColor.red 49 | case .arcToPoint: return NSColor.lightGray 50 | case .relativeArc: return NSColor.magenta 51 | } 52 | } 53 | 54 | func appendToPath(_ path: CGMutablePath) { 55 | switch self { 56 | case .moveTo(let point): 57 | path.move(to: point) 58 | 59 | case .lineTo(let point): 60 | path.addLine(to: point) 61 | 62 | case .curveTo(let point, let control1, let control2): 63 | path.addCurve(to: point, control1: control1, control2: control2) 64 | 65 | case .quadCurveTo(let point, let control): 66 | path.addQuadCurve(to: point, control: control) 67 | 68 | case .close: 69 | path.closeSubpath() 70 | 71 | case .arc(let center, let radius, let startAngle, let endAngle, let clockwise): 72 | path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise) 73 | 74 | case .arcToPoint(let control1, let control2, let radius): 75 | path.addArc(tangent1End: control1, tangent2End: control2, radius: radius) 76 | 77 | case .relativeArc(let center, let radius, let startAngle, let deltaAngle): 78 | path.addRelativeArc(center: center, radius: radius, startAngle: startAngle, delta: deltaAngle) 79 | } 80 | } 81 | 82 | func chunkLikeMeButWithDifferentPoint(_ newPoint: CGPoint, atElementIndex: Int) -> ChunkType { 83 | switch self { 84 | case .moveTo: 85 | precondition(atElementIndex == 0) 86 | return .moveTo(point: newPoint) 87 | 88 | case .lineTo: 89 | precondition(atElementIndex == 0) 90 | return .lineTo(point: newPoint) 91 | 92 | case .curveTo(var point, var control1, var control2): 93 | precondition(atElementIndex >= 0 && atElementIndex <= 2) 94 | if (atElementIndex == 0) { point = newPoint } 95 | if (atElementIndex == 1) { control1 = newPoint } 96 | if (atElementIndex == 2) { control2 = newPoint } 97 | return .curveTo(point: point, control1: control1, control2: control2) 98 | 99 | case .quadCurveTo(var point, var control): 100 | precondition(atElementIndex == 0 || atElementIndex == 1) 101 | if (atElementIndex == 0) { point = newPoint } 102 | if (atElementIndex == 1) { control = newPoint } 103 | return .quadCurveTo(point: point, control: control) 104 | 105 | case .close: 106 | return .close 107 | 108 | case .arc(_, let radius, let startAngle, let endAngle, let clockwise): 109 | precondition(atElementIndex == 0) 110 | return .arc(center: newPoint, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise) 111 | 112 | case .arcToPoint(var control1, var control2, let radius): 113 | precondition(atElementIndex == 0 || atElementIndex == 1) 114 | if (atElementIndex == 0) { control1 = newPoint } 115 | if (atElementIndex == 1) { control2 = newPoint } 116 | return .arcToPoint(control1: control1, control2: control2, radius: radius) 117 | 118 | case .relativeArc(_, let radius, let startAngle, let deltaAngle): 119 | precondition(atElementIndex == 0) 120 | return .relativeArc(center: newPoint, radius: radius, 121 | startAngle: startAngle, deltaAngle: deltaAngle) 122 | } 123 | } 124 | } 125 | 126 | 127 | 128 | class PathSamplerView: NSView { 129 | var chunks : [ChunkType] = [] // Love ya, Brian 130 | fileprivate let BoxSize: CGFloat = 6.0 131 | 132 | fileprivate var trackingChunk: ChunkType? 133 | fileprivate var trackingChunkIndex: Int? 134 | fileprivate var trackingChunkElementIndex: Int? 135 | 136 | 137 | @IBAction func dumpChunks(_: AnyObject?) { 138 | for chunk in chunks { 139 | Swift.print("\(chunk.prettyName()) - \(chunk.controlPoints())") 140 | } 141 | } 142 | 143 | 144 | fileprivate func boxForPoint(_ point: CGPoint) -> CGRect { 145 | let boxxy = CGRect(x: point.x - BoxSize / 2.0, 146 | y: point.y - BoxSize / 2.0, 147 | width: BoxSize, height: BoxSize) 148 | return boxxy 149 | } 150 | 151 | fileprivate func drawBoxAt(_ point: CGPoint, color: NSColor) { 152 | let rect = boxForPoint(point); 153 | 154 | currentContext.protectGState { 155 | currentContext.addRect(rect) 156 | color.set() 157 | currentContext.fillPath() 158 | } 159 | } 160 | 161 | 162 | fileprivate func drawBackground() { 163 | let rect = bounds 164 | 165 | currentContext.protectGState { 166 | currentContext.addRect(rect) 167 | NSColor.white.set() 168 | currentContext.fillPath() 169 | } 170 | } 171 | 172 | 173 | fileprivate func drawBorder() { 174 | let context = currentContext 175 | 176 | context.protectGState { 177 | NSColor.black.set() 178 | context.stroke(bounds) 179 | } 180 | } 181 | 182 | 183 | func addChunk(_ chunk: ChunkType) { 184 | chunks.append(chunk) 185 | needsDisplay = true 186 | } 187 | 188 | 189 | func buildPath() -> CGMutablePath { 190 | let path = CGMutablePath() 191 | 192 | _ = chunks.map { $0.appendToPath(path) } 193 | 194 | return path 195 | } 196 | 197 | 198 | func drawPath() { 199 | let path = buildPath() 200 | 201 | currentContext.protectGState { 202 | currentContext.addPath(path) 203 | currentContext.strokePath() 204 | } 205 | } 206 | 207 | func drawControlPoints() { 208 | for chunk in chunks { 209 | for controlPoint in chunk.controlPoints() { 210 | let color = chunk.controlColor() 211 | drawBoxAt(controlPoint, color: color) 212 | } 213 | } 214 | } 215 | 216 | 217 | override func draw(_ dirtyRect: NSRect) { 218 | super.draw(dirtyRect) 219 | 220 | drawBackground() 221 | currentContext.protectGState { 222 | drawControlPoints() 223 | } 224 | drawPath() 225 | drawBorder() 226 | } 227 | 228 | 229 | fileprivate func startDrag(_ chunk: ChunkType, _ chunkIndex: Int, _ controlPointIndex: Int) { 230 | trackingChunk = chunk 231 | trackingChunkIndex = chunkIndex 232 | trackingChunkElementIndex = controlPointIndex 233 | } 234 | 235 | 236 | fileprivate func updateDragWithPoint(_ point: CGPoint) { 237 | guard let trackingChunk = trackingChunk, 238 | let trackingChunkElementIndex = trackingChunkElementIndex, 239 | let trackingChunkIndex = trackingChunkIndex else { return } 240 | 241 | let newChunk = trackingChunk.chunkLikeMeButWithDifferentPoint(point, 242 | atElementIndex: trackingChunkElementIndex) 243 | chunks[trackingChunkIndex] = newChunk 244 | needsDisplay = true 245 | } 246 | 247 | 248 | override func mouseDown(with event: NSEvent) { 249 | let localPoint = convert(event.locationInWindow, from: nil) 250 | 251 | for (chunkIndex, chunk) in chunks.enumerated() { 252 | for (controlPointIndex, controlPoint) in chunk.controlPoints().enumerated() { 253 | let box = boxForPoint(controlPoint).insetBy(dx: -10.0, dy: -10.0) 254 | if (box.contains(localPoint)) { 255 | startDrag(chunk, chunkIndex, controlPointIndex) 256 | } 257 | } 258 | } 259 | } 260 | 261 | override func mouseDragged(with event: NSEvent) { 262 | let localPoint = convert(event.locationInWindow, from: nil) 263 | updateDragWithPoint(localPoint) 264 | } 265 | 266 | 267 | override func mouseUp(with event: NSEvent) { 268 | trackingChunk = nil 269 | trackingChunkIndex = nil 270 | trackingChunkElementIndex = nil 271 | } 272 | 273 | 274 | func addSamplePath() { 275 | 276 | addChunk(.moveTo(point: CGPoint(x: 184, y: 363))) 277 | addChunk(.lineTo(point: CGPoint(x: 175, y: 311))) 278 | 279 | addChunk(.relativeArc(center: CGPoint(x: 105, y: 280.0), radius: 25, 280 | startAngle: π / 2, deltaAngle: π)) 281 | 282 | addChunk(.curveTo(point: CGPoint(x: 109, y: 100), 283 | control1: CGPoint(x: 121, y: 207), 284 | control2: CGPoint(x: 63, y: 157))) 285 | addChunk(.quadCurveTo(point: CGPoint(x: 219, y: 73), 286 | control: CGPoint(x: 157, y: 141))) 287 | 288 | addChunk(.lineTo(point: CGPoint(x: 273, y: 145))) 289 | 290 | addChunk(.arcToPoint(control1: CGPoint(x: 318, y: 131), 291 | control2: CGPoint(x:260, y: 190), radius: 40)) 292 | 293 | addChunk(.lineTo(point: CGPoint(x: 282, y: 320))) 294 | 295 | addChunk(.arc(center: CGPoint(x: 230, y: 351), radius: 30.0, startAngle: 0.0, 296 | endAngle: π, clockwise: false)) 297 | addChunk(.close) 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /GrafDemo/PathWindowController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class PathWindowController: NSWindowController { 4 | @IBOutlet var pathSamplerView: PathSamplerView! 5 | 6 | override func windowDidLoad() { 7 | pathSamplerView.addSamplePath() 8 | } 9 | 10 | @IBAction func dumpPath(_ sender: NSButton) { 11 | let path = pathSamplerView.buildPath() 12 | path.dump() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GrafDemo/PathWindowController.xib: -------------------------------------------------------------------------------- 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 | 31 | 32 | 33 | 34 | 35 | 36 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /GrafDemo/RelativeArcEditingView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class RelativeArcEditingView: NSView { 4 | enum ControlPoint: Int { 5 | case pathStart 6 | case firstSegment 7 | case secondSegment 8 | case pathDelta 9 | 10 | case arcCenter 11 | case radiusHandle 12 | 13 | case count 14 | } 15 | 16 | @objc var radius: CGFloat { 17 | let center = controlPoints[.arcCenter]! 18 | let radiusHandle = controlPoints[.radiusHandle]! 19 | return CGFloat(hypot(Double(center.x - radiusHandle.x), 20 | Double(center.y - radiusHandle.y))) 21 | } 22 | 23 | @objc var center: CGPoint { 24 | return controlPoints[.arcCenter]! 25 | } 26 | 27 | 28 | @objc var startAngle: CGFloat = 0 { 29 | didSet { 30 | needsDisplay = true 31 | } 32 | } 33 | @objc var deltaAngle: CGFloat = 0 { 34 | didSet { 35 | needsDisplay = true 36 | } 37 | } 38 | 39 | let boxSize = 4 40 | var trackingPoint: ControlPoint? 41 | var controlPoints = [ControlPoint: CGPoint]() 42 | 43 | private func commonInit(withSize size: CGSize) { 44 | let defaultRadius: CGFloat = 25.0 45 | let margin: CGFloat = 5.0 46 | let lineLength = size.width / 3.0 47 | 48 | startAngle = 3 * (π / 4.0) 49 | deltaAngle = -π / 2.0 50 | 51 | let midX = size.width / 2.0 52 | let midY = size.height / 2.0 53 | 54 | let leftX = margin 55 | let rightX = size.width - margin 56 | 57 | controlPoints[.pathStart] = CGPoint(x: leftX, y: midY) 58 | controlPoints[.firstSegment] = CGPoint(x: leftX + lineLength, y: midY) 59 | controlPoints[.secondSegment] = CGPoint(x: rightX - lineLength, y: midY) 60 | controlPoints[.pathDelta] = CGPoint(x: rightX, y: midY) 61 | controlPoints[.arcCenter] = CGPoint(x: midX, y: midY) 62 | controlPoints[.radiusHandle] = CGPoint(x: midX, y: midY - defaultRadius) 63 | } 64 | 65 | override init(frame: NSRect) { 66 | super.init(frame: frame) 67 | commonInit(withSize: frame.size) 68 | } 69 | 70 | required init?(coder: NSCoder) { 71 | super.init(coder: coder) 72 | commonInit(withSize: frame.size) 73 | } 74 | 75 | func drawPath() { 76 | let context = currentContext 77 | 78 | let path = CGMutablePath() 79 | 80 | path.move(to: controlPoints[.pathStart]!) 81 | path.addLine(to: controlPoints[.firstSegment]!) 82 | path.addRelativeArc(center: controlPoints[.arcCenter]!, 83 | radius: radius, 84 | startAngle: startAngle, 85 | delta: deltaAngle) 86 | path.addLine(to: controlPoints[.secondSegment]!) 87 | path.addLine(to: controlPoints[.pathDelta]!) 88 | 89 | context.addPath(path) 90 | context.strokePath() 91 | } 92 | 93 | fileprivate func boxForPoint(_ point: CGPoint) -> CGRect { 94 | let BoxSize: CGFloat = 4.0 95 | let boxxy = CGRect(x: point.x - BoxSize / 2.0, 96 | y: point.y - BoxSize / 2.0, 97 | width: BoxSize, height: BoxSize) 98 | return boxxy 99 | } 100 | 101 | func drawControlPoints() { 102 | let context = currentContext 103 | 104 | context.protectGState { 105 | for (type, point) in controlPoints { 106 | let color: NSColor 107 | switch type { 108 | case .pathStart, .firstSegment, .secondSegment, .pathDelta: 109 | color = NSColor.blue 110 | case .arcCenter: 111 | color = NSColor.red 112 | case .radiusHandle: 113 | color = NSColor.orange 114 | default: 115 | color = NSColor.magenta // it's a Magenta Alert 116 | } 117 | color.set() 118 | context.fill(boxForPoint(point)) 119 | } 120 | } 121 | } 122 | 123 | // Need to dust off the trig book and figure out the proper places to draw 124 | // gray influence lines to beginning/ending angle 125 | func drawInfluenceLines() { 126 | let context = currentContext 127 | 128 | let influenceOverspill: CGFloat = 20.0 // how many points beyond the circle 129 | 130 | context.protectGState { 131 | NSColor.lightGray.set() 132 | let pattern: [CGFloat] = [2.0, 2.0] 133 | context.setLineDash(phase: 0.0, lengths: pattern) 134 | 135 | let radius = Double(self.radius + influenceOverspill) 136 | let deltaAngle = startAngle + self.deltaAngle 137 | 138 | 139 | let startAngleDouble = Double(startAngle) // I love you Swift. 140 | let deltaAngleDouble = Double(deltaAngle) 141 | 142 | let startAnglePoint = CGPoint(x: center.x + CGFloat(radius * cos(startAngleDouble)), 143 | y: center.y + CGFloat(radius * sin(startAngleDouble))) 144 | let deltaAnglePoint = CGPoint(x: center.x + CGFloat(radius * cos(deltaAngleDouble)), 145 | y: center.y + CGFloat(radius * sin(deltaAngleDouble))) 146 | 147 | let startAngleSegments = [center, startAnglePoint] 148 | let deltaAngleSegments = [center, deltaAnglePoint] 149 | 150 | context.strokeLineSegments(between: startAngleSegments) 151 | context.strokeLineSegments(between: deltaAngleSegments) 152 | } 153 | } 154 | 155 | override func draw(_ rect: NSRect) { 156 | NSColor.white.set() 157 | bounds.fill() 158 | NSColor.black.set() 159 | bounds.frame() 160 | 161 | drawInfluenceLines() 162 | drawPath() 163 | drawControlPoints() 164 | } 165 | 166 | override func mouseDown(with event: NSEvent) { 167 | trackingPoint = nil 168 | 169 | let localPoint = convert(event.locationInWindow, from: nil) 170 | 171 | for (type, point) in controlPoints { 172 | let box = boxForPoint(point).insetBy(dx: -10, dy: -10) 173 | 174 | if box.contains(localPoint) { 175 | trackingPoint = type 176 | break 177 | } 178 | } 179 | } 180 | 181 | private func dragTo(point: CGPoint) { 182 | guard let trackingIndex = trackingPoint else { 183 | return 184 | } 185 | 186 | controlPoints[trackingIndex] = point 187 | 188 | needsDisplay = true 189 | } 190 | 191 | override func mouseDragged(with event: NSEvent) { 192 | let localPoint = convert(event.locationInWindow, from: nil) 193 | dragTo(point: localPoint) 194 | } 195 | 196 | override func mouseUp(with event: NSEvent) { 197 | trackingPoint = nil 198 | } 199 | 200 | } 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | -------------------------------------------------------------------------------- /GrafDemo/SimpleView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class SimpleView: NSView { 4 | 5 | // Should drawing be sloppy with graphic saves and restores? 6 | @objc var beSloppy : Bool = false { 7 | willSet { 8 | needsDisplay = true 9 | } 10 | } 11 | 12 | // -------------------------------------------------- 13 | 14 | func drawSloppily() { 15 | currentContext.setStrokeColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) // Black 16 | currentContext.setFillColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) // White 17 | 18 | currentContext.setLineWidth(3.0) 19 | 20 | drawSloppyBackground() 21 | drawSloppyContents() 22 | drawSloppyBorder() 23 | } 24 | 25 | func drawSloppyBackground() { 26 | currentContext.fill(bounds) 27 | } 28 | 29 | func drawSloppyContents() { 30 | let innerRect = bounds.insetBy(dx: 20.0, dy: 20.0) 31 | if innerRect.isEmpty { 32 | return 33 | } 34 | 35 | currentContext.setFillColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0) // Green 36 | currentContext.fillEllipse(in: innerRect) 37 | 38 | currentContext.setStrokeColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0) // Blue 39 | currentContext.setLineWidth(6.0) 40 | currentContext.strokeEllipse(in: innerRect) 41 | } 42 | 43 | func drawSloppyBorder() { 44 | currentContext.stroke(bounds) 45 | } 46 | 47 | // -------------------------------------------------- 48 | 49 | func drawNicely() { 50 | currentContext.setStrokeColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) // Black 51 | currentContext.setFillColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) // White 52 | currentContext.setLineWidth(3.0) 53 | 54 | drawNiceBackground() 55 | drawNiceContents() 56 | drawNiceBorder() 57 | } 58 | 59 | func drawNiceBackground() { 60 | currentContext.protectGState { 61 | currentContext.fill(bounds) 62 | } 63 | } 64 | 65 | func drawNiceContents() { 66 | let innerRect = bounds.insetBy(dx: 20.0, dy: 20.0) 67 | 68 | if innerRect.isEmpty { 69 | return 70 | } 71 | 72 | currentContext.protectGState { 73 | currentContext.setFillColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0) // Green 74 | currentContext.fillEllipse(in: innerRect) 75 | 76 | currentContext.setStrokeColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0) // Blue 77 | currentContext.setLineWidth(6.0) 78 | currentContext.strokeEllipse(in: innerRect) 79 | } 80 | } 81 | 82 | func drawNiceBorder() { 83 | currentContext.protectGState { 84 | currentContext.stroke(bounds) 85 | } 86 | } 87 | 88 | 89 | // -------------------------------------------------- 90 | 91 | override func draw(_ dirtyRect: NSRect) { 92 | super.draw(dirtyRect) 93 | 94 | if beSloppy { 95 | drawSloppily() 96 | } else { 97 | drawNicely() 98 | } 99 | 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /GrafDemo/TransformView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class TransformView: NSView { 4 | 5 | fileprivate let kBig: CGFloat = 10000 6 | fileprivate let animationSteps: CGFloat = 15 7 | fileprivate var useContextTransforms = false 8 | 9 | fileprivate var animationTimer: Timer! 10 | 11 | fileprivate var translation = CGPoint() { 12 | didSet { 13 | needsDisplay = true 14 | } 15 | } 16 | 17 | fileprivate var rotation: CGFloat = 0 { 18 | didSet { 19 | needsDisplay = true 20 | } 21 | } 22 | 23 | fileprivate var scale = CGSize(width: 1.0, height: 1.0) { 24 | didSet { 25 | needsDisplay = true 26 | } 27 | } 28 | 29 | fileprivate var animationFunction: (() -> Bool)? // returns true when finished 30 | 31 | // until get a fancy UI 32 | @objc var shouldTranslate = true 33 | @objc var shouldRotate = true 34 | @objc var shouldScale = true 35 | 36 | @objc func reset() { 37 | translation = CGPoint() 38 | rotation = 0 39 | scale = CGSize(width: 1.0, height: 1.0) 40 | } 41 | 42 | // TODO(markd 2015-07-07) this common stuff could use a nice refactoring. 43 | fileprivate func drawBackground() { 44 | let rect = bounds 45 | 46 | currentContext.protectGState { 47 | currentContext.addRect(rect) 48 | NSColor.white.set() 49 | currentContext.fillPath() 50 | } 51 | } 52 | 53 | 54 | fileprivate func drawBorder() { 55 | let context = currentContext 56 | 57 | context.protectGState { 58 | NSColor.black.set() 59 | context.stroke(bounds) 60 | } 61 | } 62 | 63 | fileprivate func drawGridLinesWithStride(_ strideLength: CGFloat, withLabels: Bool, context: CGContext) { 64 | let font = NSFont.systemFont(ofSize: 10.0) 65 | 66 | let darkGray = NSColor.darkGray.withAlphaComponent(0.5) 67 | 68 | let textAttributes: [String : AnyObject] = [ convertFromNSAttributedStringKey(NSAttributedString.Key.font) : font, 69 | convertFromNSAttributedStringKey(NSAttributedString.Key.foregroundColor): darkGray] 70 | 71 | // draw vertical lines 72 | for x in stride(from: bounds.minX - kBig, to: kBig, by: strideLength) { 73 | let start = CGPoint(x: x + 0.25, y: -kBig) 74 | let end = CGPoint(x: x + 0.25, y: kBig ) 75 | context.move(to: start) 76 | context.addLine(to: end) 77 | context.strokePath() 78 | 79 | if (withLabels) { 80 | var textOrigin = CGPoint(x: x + 0.25, y: bounds.minY + 0.25) 81 | textOrigin.x += 2.0 82 | let label = NSString(format: "%d", Int(x)) 83 | label.draw(at: textOrigin, withAttributes: convertToOptionalNSAttributedStringKeyDictionary(textAttributes)) 84 | } 85 | } 86 | 87 | // draw horizontal lines 88 | for y in stride(from: bounds.minY - kBig, to: kBig, by: strideLength) { 89 | let start = CGPoint(x: -kBig, y: y + 0.25) 90 | let end = CGPoint(x: kBig, y: y + 0.25) 91 | context.move(to: start) 92 | context.addLine(to: end) 93 | context.strokePath() 94 | 95 | if (withLabels) { 96 | var textOrigin = CGPoint(x: bounds.minX + 0.25, y: y + 0.25) 97 | textOrigin.x += 3.0 98 | 99 | let label = NSString(format: "%d", Int(y)) 100 | label.draw(at: textOrigin, withAttributes: convertToOptionalNSAttributedStringKeyDictionary(textAttributes)) 101 | } 102 | } 103 | } 104 | 105 | fileprivate func drawGrid() { 106 | let context = currentContext 107 | 108 | context.protectGState { 109 | context.setLineWidth(0.5) 110 | 111 | let lightGray = NSColor.lightGray.withAlphaComponent(0.3) 112 | let darkGray = NSColor.darkGray.withAlphaComponent(0.3) 113 | 114 | 115 | // Light grid lines every 10 points 116 | 117 | // Performance hack - if the transform has a rotation, speed of drawing 118 | // plummets, so hide the inner lines when animating. 119 | if animationFunction == nil { 120 | lightGray.setStroke() 121 | drawGridLinesWithStride(10, withLabels: false, context: context) 122 | } 123 | 124 | // darker gray lines every 100 points 125 | darkGray.setStroke() 126 | drawGridLinesWithStride(100, withLabels: true, context: context) 127 | 128 | // black lines on cartesian axes 129 | // P.S. "AND MY AXE" -- Gimli 130 | let bounds = self.bounds 131 | 132 | let start = CGPoint(x: bounds.minX + 0.25, y: bounds.minY) 133 | let horizontalEnd = CGPoint(x: bounds.maxX + 0.25, y: bounds.minY) 134 | let verticalEnd = CGPoint(x: bounds.minX + 0.25, y: bounds.maxY) 135 | 136 | context.setLineWidth(2.0) 137 | NSColor.black.setStroke() 138 | context.move(to: CGPoint(x: -kBig, y: start.y)) 139 | context.addLine(to: CGPoint(x: kBig, y: horizontalEnd.y)) 140 | 141 | context.move(to: CGPoint(x: start.x, y: -kBig)) 142 | context.addLine(to: CGPoint(x: verticalEnd.x, y: kBig)) 143 | 144 | context.strokePath() 145 | } 146 | } 147 | 148 | fileprivate func applyTransforms() { 149 | 150 | if useContextTransforms { 151 | currentContext.translateBy(x: translation.x, y: translation.y) 152 | currentContext.rotate(by: rotation) 153 | currentContext.scaleBy(x: scale.width, y: scale.height) 154 | 155 | } else { // use matrix transforms 156 | let identity = CGAffineTransform.identity 157 | let shiftingCenter = identity.translatedBy(x: translation.x, y: translation.y) 158 | let rotating = shiftingCenter.rotated(by: rotation) 159 | let scaling = rotating.scaledBy(x: scale.width, y: scale.height) 160 | 161 | // makes experimentation a little easier - just set to the transform you want to apply 162 | // to see how it looks 163 | let lastTransform = scaling 164 | 165 | currentContext.concatenate(lastTransform) 166 | } 167 | 168 | } 169 | 170 | fileprivate func drawPath() { 171 | guard let hat = RanchLogoPath() else { return } 172 | 173 | var flipTransform = AffineTransform.identity 174 | let bounds = hat.bounds 175 | flipTransform.translate(x: 0.0, y: bounds.height * 4) 176 | flipTransform.scale(x: 2.0, y: -2.0) 177 | hat.transform(using: flipTransform as AffineTransform) 178 | 179 | NSColor.orange.set() 180 | hat.fill() 181 | 182 | NSColor.black.set() 183 | hat.stroke() 184 | } 185 | 186 | override func draw(_ dirtyRect: NSRect) { 187 | super.draw(dirtyRect) 188 | 189 | drawBackground() 190 | currentContext.protectGState() { 191 | applyTransforms() 192 | drawGrid() 193 | drawPath() 194 | } 195 | drawBorder() 196 | } 197 | 198 | override var isFlipped : Bool{ 199 | return true 200 | } 201 | 202 | @objc func tick(_ timer: Timer) { 203 | guard let animator = animationFunction else { 204 | return 205 | } 206 | if animator() { 207 | animationTimer.invalidate() 208 | animationTimer = nil 209 | animationFunction = nil 210 | needsDisplay = true 211 | } 212 | } 213 | 214 | 215 | @objc func translationAnimator(_ from: CGPoint, to: CGPoint) -> () -> Bool { 216 | translation = from 217 | 218 | let delta = CGPoint(x: (to.x - from.x) / animationSteps, 219 | y: (to.y - from.y) / animationSteps) 220 | 221 | return { 222 | self.translation.x += delta.x 223 | self.translation.y += delta.y 224 | 225 | self.needsDisplay = true 226 | 227 | // this is insufficient, if from.x == to.x 228 | if self.translation.x > to.x { 229 | return true 230 | } else { 231 | return false 232 | } 233 | } 234 | } 235 | 236 | 237 | @objc func rotationAnimator(_ from: CGFloat, to: CGFloat) -> () -> Bool { 238 | rotation = from 239 | 240 | let delta = (to - from) / animationSteps 241 | 242 | return { 243 | self.rotation += delta 244 | 245 | if self.rotation > to { 246 | return true 247 | } else { 248 | return false 249 | } 250 | } 251 | } 252 | 253 | 254 | @objc func scaleAnimator(_ from: CGSize, to: CGSize) -> () -> Bool { 255 | scale = from 256 | 257 | let delta = CGSize(width: (to.width - from.width) / animationSteps, 258 | height: (to.height - from.height) / animationSteps) 259 | 260 | return { 261 | self.scale.width += delta.width 262 | self.scale.height += delta.height 263 | 264 | self.needsDisplay = true 265 | 266 | // this is insufficient, if from.height == to.height 267 | if self.scale.width > to.width { 268 | return true 269 | } else { 270 | return false 271 | } 272 | } 273 | } 274 | 275 | func compositeAnimator(_ animations: [ () -> Bool ]) -> () -> Bool { 276 | guard var currentAnimation = animations.first else { 277 | return { 278 | return true // no animations, so we're done 279 | } 280 | } 281 | 282 | var animatorIndex = 0 283 | 284 | return { 285 | if currentAnimation() { 286 | // move to the next one 287 | animatorIndex += 1 288 | 289 | // run out? 290 | if animatorIndex >= animations.count { 291 | return true 292 | } 293 | 294 | // otherwise, tick over 295 | currentAnimation = animations[animatorIndex] 296 | return false // not done 297 | } else { 298 | return false // not done 299 | } 300 | } 301 | } 302 | 303 | 304 | @objc func startAnimation() { 305 | // The worst possible way to animate, but I'm in a hurry right now prior 306 | // to cocoaconf/columbus. ++md 2015-07-07 307 | 308 | let translateFrom = CGPoint() 309 | let translateTo = CGPoint(x: 200, y: 100) 310 | let translator = translationAnimator(translateFrom, to: translateTo) 311 | 312 | let rotator = rotationAnimator(0.0, to: rotation + π / 12) 313 | 314 | let scaleFrom = CGSize(width: 1.0, height: 1.0) 315 | let scaleTo = CGSize(width: 3.0, height: 1.5) 316 | let scaler = scaleAnimator(scaleFrom, to: scaleTo) 317 | 318 | var things: [(() -> Bool)] = [] 319 | 320 | if shouldScale { 321 | things += [scaler] 322 | } 323 | if shouldRotate { 324 | things += [rotator] 325 | } 326 | if shouldTranslate { 327 | things += [translator] 328 | } 329 | 330 | animationFunction = compositeAnimator(things) 331 | 332 | animationTimer = Timer.scheduledTimer(timeInterval: 1 / 15, target: self, selector: #selector(TransformView.tick(_:)), userInfo: nil, repeats: true) 333 | } 334 | 335 | 336 | /* 337 | For now giving up on core animation since I don't have a layer subclass. 338 | 339 | func startAnimation() { 340 | let anim = CABasicAnimation() 341 | anim.keyPath = "translateX" 342 | anim.fromValue = 0 343 | anim.toValue = 100 344 | anim.repeatCount = 1 345 | anim.duration = 3 346 | layer!.style = [ "translateX" : 0 ] 347 | layer!.addAnimation(anim, forKey: "translateX") 348 | 349 | Swift.print ("blah \(layer!.style)") 350 | 351 | /* 352 | let translateAnimation = CAKeyframeAnimation(keyPath: "translateX") 353 | translateAnimation.values = [ 200 ] 354 | translateAnimation.keyTimes = [ 100 ] 355 | translateAnimation.duration = 2.0 356 | translateAnimation.additive = true 357 | layer?.addAnimation(translateAnimation, forKey: "translate X") 358 | */ 359 | } 360 | 361 | override func actionForLayer(layer: CALayer, forKey event: String) -> CAAction? { 362 | Swift.print("flonk \(event)") 363 | return super.actionForLayer(layer, forKey: event) 364 | } 365 | 366 | */ 367 | } 368 | 369 | // Helper function inserted by Swift 4.2 migrator. 370 | fileprivate func convertFromNSAttributedStringKey(_ input: NSAttributedString.Key) -> String { 371 | return input.rawValue 372 | } 373 | 374 | // Helper function inserted by Swift 4.2 migrator. 375 | fileprivate func convertToOptionalNSAttributedStringKeyDictionary(_ input: [String: Any]?) -> [NSAttributedString.Key: Any]? { 376 | guard let input = input else { return nil } 377 | return Dictionary(uniqueKeysWithValues: input.map { key, value in (NSAttributedString.Key(rawValue: key), value)}) 378 | } 379 | -------------------------------------------------------------------------------- /GrafDemo/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | int main(int argc, const char * argv[]) { 4 | return NSApplicationMain(argc, argv); 5 | } 6 | -------------------------------------------------------------------------------- /GrafDemoTests/GrafDemoTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // GrafDemoTests.m 3 | // GrafDemoTests 4 | // 5 | // Created by Mark Dalrymple on 8/27/14. 6 | // Copyright (c) 2014 Big Nerd Ranch. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface GrafDemoTests : XCTestCase 13 | 14 | @end 15 | 16 | @implementation GrafDemoTests 17 | 18 | - (void)setUp { 19 | [super setUp]; 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | } 22 | 23 | - (void)tearDown { 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | [super tearDown]; 26 | } 27 | 28 | - (void)testExample { 29 | // This is an example of a functional test case. 30 | XCTAssert(YES, @"Pass"); 31 | } 32 | 33 | - (void)testPerformanceExample { 34 | // This is an example of a performance test case. 35 | [self measureBlock:^{ 36 | // Put the code you want to measure the time of here. 37 | }]; 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /GrafDemoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GrafDemo 2 | ======== 3 | 4 | A little demo program showing off various Core Graphics features. It's a sample program 5 | for a set of posts over at the Big Nerd Ranch blog: 6 | 7 | * [In the Beginning](https://www.bignerdranch.com/blog/core-graphics-part-1-in-the-beginning/) 8 | * [Contextually Speaking](https://www.bignerdranch.com/blog/core-graphics-part-2-contextually-speaking/) 9 | * [Lines](https://www.bignerdranch.com/blog/core-graphics-part-three-lines/) 10 | * Paths (forthcoming) 11 | 12 | Here are some other, possibly interesting, CG-related blog posts: 13 | 14 | * [Rectangles, Part 1](https://www.bignerdranch.com/blog/rectangles-part-1/) - an 15 | introduction to CGRect and basic manipulations. 16 | * [Rectangles, Part 2](https://www.bignerdranch.com/blog/rectangles-part-2/) - more 17 | CGRect and more fun API (union, intersection, insets, dividing) 18 | 19 | 20 | Digital Dashboard 21 | ----------------- 22 | 23 | Behold the main menu it all of its glory: 24 | 25 | ![](assets/main-menu.png) 26 | 27 | For the demo windows, the left-hand view is implemented in Objective-C, the 28 | right-hand view is implemented in Swift, and the window controllers 29 | alternate ObjC and Swift implementations. 30 | 31 | 32 | Simple 33 | ------ 34 | 35 | **Simple** is a view that shows basic drawing, as well as [GState hygiene](https://www.bignerdranch.com/blog/core-graphics-part-2-contextually-speaking/). The "Sloppy" 36 | toggle turns off some GState management, showing attribute settings leak out in other 37 | method's drawing. 38 | 39 | ![](assets/simple-window.png) 40 | 41 | 42 | Lines 43 | ----- 44 | Here are things related to lines. End caps, line joins, drawing mechanisms, and 45 | line phases. 46 | 47 | ![](assets/lines-window.png) 48 | 49 | 50 | Paths 51 | ----- 52 | A sampler of the different path component calls. Click and drag the control points to 53 | see how they behave. 54 | 55 | ![](assets/paths-window.png) 56 | 57 | 58 | Arcs 59 | ---- 60 | The plethora of "arc" calls are confusing. Here they are with influence lines 61 | and tweakable settings. 62 | 63 | ![](assets/arcs-window.png) 64 | 65 | 66 | Transforms 67 | ---------- 68 | Transformations are a-fine thing. 69 | 70 | ![](assets/transforms-window.png) 71 | 72 | 73 | 74 | PostScript 75 | ---------- 76 | A bit of history - Core Graphics is based on the PostScript drawing model. CG also 77 | includes a full postscript interpreter. This window lets you enter code and run it. 78 | Worst IDE Evar. 79 | 80 | ![](assets/postscript-window.png) 81 | 82 | 83 | Interesting Docs 84 | ---------------- 85 | 86 | * [Quartz 2D Programming Guide (Apple docs)](https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/Introduction/Introduction.html) 87 | * [Programming with Quartz: 2D and PDF Graphics in Mac OS X (book)](http://www.amazon.com/Programming-Quartz-Graphics-Kaufmann-Computer/dp/0123694736/) -------------------------------------------------------------------------------- /assets/arcs-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markd2/GrafDemo/a42d5b1800a71e1cec21dcb7b1dcdec44e676627/assets/arcs-window.png -------------------------------------------------------------------------------- /assets/lines-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markd2/GrafDemo/a42d5b1800a71e1cec21dcb7b1dcdec44e676627/assets/lines-window.png -------------------------------------------------------------------------------- /assets/main-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markd2/GrafDemo/a42d5b1800a71e1cec21dcb7b1dcdec44e676627/assets/main-menu.png -------------------------------------------------------------------------------- /assets/paths-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markd2/GrafDemo/a42d5b1800a71e1cec21dcb7b1dcdec44e676627/assets/paths-window.png -------------------------------------------------------------------------------- /assets/postscript-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markd2/GrafDemo/a42d5b1800a71e1cec21dcb7b1dcdec44e676627/assets/postscript-window.png -------------------------------------------------------------------------------- /assets/simple-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markd2/GrafDemo/a42d5b1800a71e1cec21dcb7b1dcdec44e676627/assets/simple-window.png -------------------------------------------------------------------------------- /assets/transforms-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markd2/GrafDemo/a42d5b1800a71e1cec21dcb7b1dcdec44e676627/assets/transforms-window.png --------------------------------------------------------------------------------