Fill a polygon using Core Graphics in Swift - swift

The goal is to fill a polygon using Core Graphics in Swift.
The following code does just that if all of the endpoints are known.
func drawPolySegment() {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 512, height: 512))
let img = renderer.image { ctx in
let endpoints = [
CGPoint(x: 200, y: 175),
CGPoint(x: 270, y: 170),
CGPoint(x: 300, y: 100)
]
ctx.cgContext.addLines(between: endpoints)
UIColor.yellow.setFill()
ctx.cgContext.drawPath(using: .fillStroke)
}
imageView.image = img
}
However, the endpoints for the desired shape are not easily obtainable and so a more generic approach was opted for using rotate(by:), move(to:), addline(to:), translate(by:) methods as follows:
func drawPolygon() {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 512, height: 512))
let img = renderer.image { ctx in
let apexes: CGFloat = 3 //5 //6
let length: CGFloat = 500 //190 //140
let angle: CGFloat = 2 * π / apexes
let zero: CGFloat = 0
UIColor.brown.setStroke()
UIColor.yellow.setFill()
ctx.cgContext.setLineWidth(10)
ctx.cgContext.setLineCap(.round)
ctx.cgContext.translateBy(x: 256, y: 5)
for segment in 0..<Int(apexes * 2) {
if segment == 0 {
ctx.cgContext.rotate(by: angle)
} else if segment % 2 == 0 {
ctx.cgContext.rotate(by: 2 * angle)
} else {
ctx.cgContext.rotate(by: -angle)
}
ctx.cgContext.move(to: CGPoint(x: zero, y: zero))
ctx.cgContext.addLine(to: CGPoint(x: length, y: zero))
ctx.cgContext.translateBy(x: length, y: zero)
}
ctx.cgContext.drawPath(using: .fillStroke)
}
imageView.image = img
}
The code above generates a beautifully outlined shape, but will not fill with colour. The values commented out for "apexes" and "length" are valid for the 512 x 512 rendering space created. Why does the shape fill for the first code sample and not the second? What is missing from the second code sample to make the shape fill?

You aren't drawing a polygon, you're drawing 3 separate unconnected lines. Each move(to:) starts a new polygon.
You can fix this by only doing the move(to:) for the first segment:
for segment in 0..<Int(apexes * 2) {
if segment == 0 {
ctx.cgContext.rotate(by: angle)
ctx.cgContext.move(to: CGPoint(x: zero, y: zero))
} else if segment % 2 == 0 {
ctx.cgContext.rotate(by: 2 * angle)
} else {
ctx.cgContext.rotate(by: -angle)
}
ctx.cgContext.addLine(to: CGPoint(x: length, y: zero))
ctx.cgContext.translateBy(x: length, y: zero)
}
Results:
apexes: 3, length: 500
apexes: 5, length: 190
apexes: 6, length: 140

Related

Creating a scnplain that is the same size as half of the screen using SceneKit

I am trying to figure out how to create a plainNode in SceneKit that takes up exactly half of the screen.
So I found this routine to projectValues that seems correct.
extension CGPoint {
func scnVector3Value(view: SCNView, depth: Float) -> SCNVector3 {
let projectedOrigin = view.projectPoint(SCNVector3(0, 0, depth))
return view.unprojectPoint(SCNVector3(Float(x), Float(y), projectedOrigin.z))
}
}
And I fed these values into it...
let native = UIScreen.main.bounds
let maxMax = CGPoint(x: native.width, y: native.height * 0.5)
let newPosition1 = maxMax.scnVector3Value(view: view, depth: Float(0))
print("newPosition \(newPosition1)")
let minMin = CGPoint(x: 0, y: 0)
let newPosition2 = minMin.scnVector3Value(view: view, depth: Float(0))
print("newPosition \(newPosition2)")
let minMax = CGPoint(x: 0, y: native.height * 0.5)
let newPosition3 = minMax.scnVector3Value(view: view, depth: Float(0))
print("newPosition \(newPosition3)")
let maxMin = CGPoint(x: native.width, y: 0)
let newPosition4 = maxMin.scnVector3Value(view: view, depth: Float(0))
print("newPosition \(newPosition4)")
// approximations that look almost correct, but they are not...
let width = (maxMax.x - minMin.x) / 100 * 2
let height = (maxMax.y - minMin.y) / 100 * 2
let plainGeo = SCNPlane(width: width, height: height)
let planeNode = SCNNode(geometry: plainGeo)
planeNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red
view.scene.rootNode?.addChildNode(planeNode)
But it isn't right? What am I doing wrong here?

Aligning four CAEmitterLayer() on each side of view, pointing inward

I'm working on adding 4 CAEmitterLayers to a subview. Each CAEmitterLayer is a line, and will be positioned on each side. Rotation will be controlled via CGAffineTransform
My problem:
I can't get the .Left and .Right emitterPosition to properly line up with the left and right side of the view
Here's my progress:
override func awakeFromNib() {
super.awakeFromNib()
self.layer.cornerRadius = GlobalConstants.smallCornerRadius
createParticles(direction: .Up)
createParticles(direction: .Down)
createParticles(direction: .Left)
createParticles(direction: .Right)
}
enum EmitTo {
case Up
case Down
case Left
case Right
}
func createParticles(direction: EmitTo) {
self.layoutSubviews()
let particleEmitter = CAEmitterLayer()
particleEmitter.position = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
particleEmitter.bounds = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height)
if direction == .Up {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: 0))
} else if (direction == .Down) {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: .pi))
} else if (direction == .Left) {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: .pi / 2))
} else if (direction == .Right) {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: -.pi / 2))
}
if direction == .Up || direction == .Down {
particleEmitter.emitterPosition = CGPoint(x: self.frame.width / 2, y: 0)
}
if direction == .Left {
particleEmitter.emitterPosition = CGPoint(x: self.frame.height / 2, y: self.frame.height)
}
if direction == .Right {
particleEmitter.emitterPosition = CGPoint(x: self.frame.height / 2, y: -50)
}
particleEmitter.emitterSize = self.bounds.size
particleEmitter.emitterShape = CAEmitterLayerEmitterShape.line
particleEmitter.zPosition = 1
let triangle = makeEmitterCell(direction: direction)
particleEmitter.emitterCells = [triangle]
self.layer.addSublayer(particleEmitter)
}
func makeEmitterCell(direction: EmitTo) -> CAEmitterCell {
let cell = CAEmitterCell()
cell.birthRate = 25
cell.lifetime = 3
cell.lifetimeRange = 0
cell.velocity = 10
cell.velocityRange = 5
cell.emissionLongitude = .pi
cell.spin = 2
cell.spinRange = 3
cell.scale = 0.4
cell.scaleRange = 0.6
cell.scaleSpeed = -0.10
cell.contents = UIImage(named: "greenTriangle")?.cgImage
return cell
}
What's happening:
My question
How can I get the .Left and .Right CAEmitterLayer to properly align with the left side and right side of the gray view?
Green Triangle Image:
Your approach to try to use CGAffineTransform is promising because a line emitter is a horizontal line that has only a width dimension. By rotating it, you can have it emit from the left or right.
However you need to use emitter bounds with the view height as width and the view width as height.
particleEmitter.bounds = CGRect(x: 0, y: 0, width: bounds.size.height, height: bounds.size.width)
Accordingly, one sets the width of the emitterSize to the height of the view:
particleEmitter.emitterSize = CGSize(width: bounds.size.height, height: 0)
The emitter position must be adjusted in the same way.
Why is this so? Take a look at the following illustration with an emission from the top:
If you rotate the layer by 90 degrees you would only cover a small area in the middle. Also the emissions would not be visible, because they would be to much outside to the left.
The difference between having the emitter on the left or the right is just a call to setAffineTransform with either -.pi / 2 or .pi / 2.
The emitters from top and bottom of course should get the normal bounds, they only need to be rotated as you do in your question.
Hints
In your createParticles method you call self.layoutSubviews(). That should be avoided.
Apple docs explicitly state:
You should not call this method directly.
see https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews.
Instead you could override layoutSubviews and call your createParticles methods from there. Make sure to remove any previously created emitter layers by removing them.
One small thing: Swift switch cases start with a lowercase letter by convention.
Code
A complete example to help you get started might then look something like this:
import UIKit
class EmitterView: UIView {
private var emitters = [CAEmitterLayer]()
enum EmitTo {
case up
case down
case left
case right
}
override func layoutSubviews() {
super.layoutSubviews()
emitters.forEach { $0.removeFromSuperlayer() }
emitters.removeAll()
createParticles(direction: .up)
createParticles(direction: .down)
createParticles(direction: .left)
createParticles(direction: .right)
}
func createParticles(direction: EmitTo) {
let particleEmitter = CAEmitterLayer()
emitters.append(particleEmitter)
particleEmitter.position = CGPoint(x: bounds.midX, y: bounds.midY)
particleEmitter.emitterShape = .line
switch direction {
case .up, .down:
particleEmitter.bounds = self.bounds
particleEmitter.emitterPosition = CGPoint(x: bounds.size.width / 2, y: 0)
particleEmitter.emitterSize = CGSize(width: bounds.size.width, height: 0)
if direction == .up {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: -.pi))
}
case .left, .right:
particleEmitter.bounds = CGRect(x: 0, y: 0, width: bounds.size.height, height: bounds.size.width)
particleEmitter.emitterPosition = CGPoint(x: bounds.height / 2, y: 0)
particleEmitter.emitterSize = CGSize(width: bounds.size.height, height: 0)
let rotationAngle: CGFloat = direction == EmitTo.left ? -.pi / 2 : .pi / 2
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: rotationAngle))
}
particleEmitter.emitterCells = [makeEmitterCell()]
layer.addSublayer( particleEmitter)
}
func makeEmitterCell() -> CAEmitterCell {
let cell = CAEmitterCell()
cell.birthRate = 25
cell.lifetime = 3
cell.lifetimeRange = 0
cell.velocity = 10
cell.velocityRange = 5
cell.emissionLongitude = .pi
cell.spin = 2
cell.spinRange = 3
cell.scale = 0.4
cell.scaleRange = 0.6
cell.scaleSpeed = -0.10
cell.contents = UIImage(named: "greenTriangle")?.cgImage
return cell
}
}
Test

bezierPath setfill with diff colours Not Working ( to create a mountain climb color coding )

My goal is to create a chart much like below. Where the diff colours represent the different slope / gradient % for each segment.
My previous code, I have already managed to get the "mountain" outline, now further improved it such that I am now making a set of "boxes" which represents each colour segment. But am facing some issue.
Here is the code I'm using right now.
let myBezier = UIBezierPath()
let nextPt = fillSlopes(at: idx)
if idx > 0 {
let prevPt = fillSlopes(at: idx - 1)
myBezier.move(to: CGPoint(x: prevPt.x, y: height))
myBezier.addLine(to: CGPoint(x: prevPt.x, y: prevPt.y))
myBezier.addLine(to: CGPoint(x: nextPt.x, y: nextPt.y))
myBezier.addLine(to: CGPoint(x: nextPt.x, y: height))
myBezier.addLine(to: CGPoint(x: prevPt.x, y: height))
myBezier.close()
// This is test code, actual code will compare current slope %
// (in an array) and then based on the slope %, will change
// the colour accordingly.
if idx % 2 == 0 {
UIColor.systemRed.setFill()
myBezier.fill()
} else {
UIColor.systemBlue.setFill()
myBezier.fill()
}
This is the results. Unfortunately, the colours is not coming out properly. What Am I missing?
In addition to this, Drawing so many little boxes is really eating up a lot of CPU cycles, if there are other suggestions to get the desired output result is also appreciated. Thanks
Update:
Many Thanks for #seaspell, I realised what was happening. I was putting my let myBezier = UIBezierPath() outside of the for loop hence my [UIBezierPath] array was getting mangled up. eg:
This got things all wrong:
func xxx {
let myBezier = UIBezierPath() // <<<<<< this is wrong
for idx in slopeBars.indices {
// do stuff
}
This is Correct and fixed some performance issue as well:
func xxx {
var slopeBars = [UIBezierPath]()
var slopeBarColorArray = [UIColor]()
for idx in slopeBars.indices {
let myBezier = UIBezierPath() // <<<<< Moved here
let nextPt = fillSlopes(at: idx)
if idx > 0 {
let prevPt = fillSlopes(at: idx - 1)
myBezier.move(to: CGPoint(x: prevPt.x, y: height))
myBezier.addLine(to: CGPoint(x: prevPt.x, y: prevPt.y))
myBezier.addLine(to: CGPoint(x: nextPt.x, y: nextPt.y))
myBezier.addLine(to: CGPoint(x: nextPt.x, y: height))
myBezier.addLine(to: CGPoint(x: prevPt.x, y: height))
myBezier.close()
slopeBars.append(myBezier)
if gradient < 0.0 {
slopeBarColorArray.append(UIColor(cgColor: Consts.elevChart.slope0_0))
} else if gradient < 4.0 {
slopeBarColorArray.append(UIColor(cgColor: Consts.elevChart.slope0_4))
}
for i in 0..<slopeBars.count {
if !slopeBarColorArray.isEmpty {
slopeBarColorArray[i].setStroke()
slopeBarColorArray[i].setFill()
}
slopeBars[i].stroke()
slopeBars[i].fill()
}
}
There's nothing really wrong with using bezier paths for this. You need to use a new one for each bar though and also you don't want to build them in draw(rect:). I suspect that is the cause of the CPU issues.
Take a look at this example I put together.
class BarChartView: UIView {
lazy var dataPoints: [CGFloat] = {
var points = [CGFloat]()
for _ in 1...barCount {
let random = arc4random_uniform(UInt32(bounds.maxY / 2))
points.append(CGFloat(random))
}
return points
}()
var bars = [UIBezierPath]()
private let barCount = 20
lazy var colors: [UIColor] = {
let colors: [UIColor] = [.red, .blue, .cyan, .brown, .darkGray, .green, .yellow, .gray, .lightGray, .magenta, .orange, .purple, .systemPink, .white]
var random = [UIColor]()
for i in 0...barCount {
random.append(colors[Int(arc4random_uniform(UInt32(colors.count)))])
}
return random
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.bars = buildBars()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.bars = buildBars()
}
func buildBars() -> [UIBezierPath] {
let lineWidth = Int(bounds.maxX / CGFloat(barCount))
var bars = [UIBezierPath]()
for i in 0..<barCount {
let line = UIBezierPath()
let leftX = lineWidth * i
let rightX = leftX + lineWidth
let centerX = leftX + (lineWidth / 2)
let nextLeftPoint = (i == 0 ? dataPoints[i] : dataPoints[i - 1])
let nextRightPoint = (i == barCount - 1 ? dataPoints[i] : dataPoints[i + 1])
let currentPoint = dataPoints[i]
//bottom left
line.move(to: CGPoint(x: leftX, y: Int(bounds.maxY)) )
//bottom right
line.addLine(to: CGPoint(x: rightX, y: Int(bounds.maxY)) )
//top right
line.addLine(to: CGPoint(x: rightX, y: Int((currentPoint + nextRightPoint) / 2)) )
//top center
line.addLine(to: CGPoint(x: centerX, y: Int(currentPoint)) )
//top left
line.addLine(to: CGPoint(x: leftX, y: Int((currentPoint + nextLeftPoint) / 2)) )
//close the path
line.close()
bars.append(line)
}
return bars
}
override func draw(_ rect: CGRect) {
super.draw(rect)
for i in 0..<bars.count {
colors[i].setStroke()
colors[i].setFill()
bars[i].stroke()
bars[i].fill()
}
}
}

Getting SpriteKit texture from SKView returning nil after 104 calls

I am having some odd behavior in SpriteKit when creating a texture. The function below shows you what I am doing. In short, I'm in SceneKit and making a SCNNode out of an Array of colors (think pixel/voxels). It works like a charm. However, after exactly 104 calls the texture returned is nil. Afterwards, it is hit or miss whether the texture will be nil or not. I am also providing the exact color information. Thoughts?
func create2dModelSK(with colors: [String]) -> SCNNode? {
let geo = SCNBox(width: 1.0, height: 1.0, length: 0.1, chamferRadius: 0.0)
let base = SCNNode(geometry: geo)
let view = SKSpriteNode(color: .white, size: CGSize(width: 160, height: 160))
view.anchorPoint = CGPoint(x: 0, y: 1)
var xOffset = 0
var yOffset = 0
var count = 0
for _ in 0...15 {
for _ in 0...15 {
guard let newColor = UIColor(hexString: "#" + colors[count] + "ff") else { return base }
let n = SKSpriteNode(color: newColor, size: CGSize(width: 10, height: 10))
n.anchorPoint = CGPoint(x: 0, y: 1)
n.position = CGPoint(x: xOffset, y: yOffset)
view.addChild(n)
xOffset += 10
count += 1
}
xOffset = 0
yOffset -= 10
}
let skView = SKView(frame: CGRect(x: 0, y: 0, width: 160, height: 160))
let texture = skView.texture(from: view)
//AFTER being called 104 times, texture is nil.
let faceMaterial = SCNMaterial()
faceMaterial.diffuse.contents = texture
let sideMaterial = SCNMaterial()
sideMaterial.diffuse.contents = UIColor.white
let materialsForBox = [faceMaterial,sideMaterial,faceMaterial,sideMaterial,sideMaterial,sideMaterial]
base.geometry?.materials = materialsForBox
let scale = SCNVector3(x: 0.1, y: 0.1, z: 0.1)
base.scale = scale
return base
}
This is where autoreleasepool comes in handy, it allows you to release the memory when the autoreleasepool is finished so that you do not run out of space before using it again.
Of course this is not going to solve your main problem, where you are creating too many textures and running out of memory space, but it will allow you to at least make some more because it will release the temporary memory that view.texture(from:node) is holding on to.
func create2dModelSK(with colors: [String]) -> SCNNode? {
let geo = SCNBox(width: 1.0, height: 1.0, length: 0.1, chamferRadius: 0.0)
let base = SCNNode(geometry: geo)
let view = SKSpriteNode(color: .white, size: CGSize(width: 160, height: 160))
view.anchorPoint = CGPoint(x: 0, y: 1)
var xOffset = 0
var yOffset = 0
var count = 0
for _ in 0...15 {
for _ in 0...15 {
guard let newColor = UIColor(hexString: "#" + colors[count] + "ff") else { return base }
let n = SKSpriteNode(color: newColor, size: CGSize(width: 10, height: 10))
n.anchorPoint = CGPoint(x: 0, y: 1)
n.position = CGPoint(x: xOffset, y: yOffset)
view.addChild(n)
xOffset += 10
count += 1
}
xOffset = 0
yOffset -= 10
}
autoreleasepool{
let skView = SKView(frame: CGRect(x: 0, y: 0, width: 160, height: 160))
let texture = skView.texture(from: view)
//AFTER being called 104 times, texture is nil.
let faceMaterial = SCNMaterial()
faceMaterial.diffuse.contents = texture
let sideMaterial = SCNMaterial()
sideMaterial.diffuse.contents = UIColor.white
let materialsForBox = [faceMaterial,sideMaterial,faceMaterial,sideMaterial,sideMaterial,sideMaterial]
base.geometry?.materials = materialsForBox
}
let scale = SCNVector3(x: 0.1, y: 0.1, z: 0.1)
base.scale = scale
return base
}

Rotate NSImage in Swift, Cocoa, Mac OSX

Is there an easy way to rotate a NSImage in a Mac OSX app? Or just set the orientation from portrait to landscape using Swift?
I am playing around with CATransform3DMakeAffineTransform but I can't get it to work.
CATransform3DMakeAffineTransform(CGAffineTransformMakeRotation(CGFloat(M_PI) * 90/180))
It's the first time for me to work with transformations. So please be patient with me :) Maybe I'm working on a wrong approach...
Can anybody help me please?
Thanks!
public extension NSImage {
public func imageRotatedByDegreess(degrees:CGFloat) -> NSImage {
var imageBounds = NSZeroRect ; imageBounds.size = self.size
let pathBounds = NSBezierPath(rect: imageBounds)
var transform = NSAffineTransform()
transform.rotateByDegrees(degrees)
pathBounds.transformUsingAffineTransform(transform)
let rotatedBounds:NSRect = NSMakeRect(NSZeroPoint.x, NSZeroPoint.y, pathBounds.bounds.size.width, pathBounds.bounds.size.height )
let rotatedImage = NSImage(size: rotatedBounds.size)
//Center the image within the rotated bounds
imageBounds.origin.x = NSMidX(rotatedBounds) - (NSWidth(imageBounds) / 2)
imageBounds.origin.y = NSMidY(rotatedBounds) - (NSHeight(imageBounds) / 2)
// Start a new transform
transform = NSAffineTransform()
// Move coordinate system to the center (since we want to rotate around the center)
transform.translateXBy(+(NSWidth(rotatedBounds) / 2 ), yBy: +(NSHeight(rotatedBounds) / 2))
transform.rotateByDegrees(degrees)
// Move the coordinate system bak to normal
transform.translateXBy(-(NSWidth(rotatedBounds) / 2 ), yBy: -(NSHeight(rotatedBounds) / 2))
// Draw the original image, rotated, into the new image
rotatedImage.lockFocus()
transform.concat()
self.drawInRect(imageBounds, fromRect: NSZeroRect, operation: NSCompositingOperation.CompositeCopy, fraction: 1.0)
rotatedImage.unlockFocus()
return rotatedImage
}
var image = NSImage(named:"test.png")!.imageRotatedByDegreess(CGFloat(90)) //use only this values 90, 180, or 270
}
Updated for Swift 3:
public extension NSImage {
public func imageRotatedByDegreess(degrees:CGFloat) -> NSImage {
var imageBounds = NSZeroRect ; imageBounds.size = self.size
let pathBounds = NSBezierPath(rect: imageBounds)
var transform = NSAffineTransform()
transform.rotate(byDegrees: degrees)
pathBounds.transform(using: transform as AffineTransform)
let rotatedBounds:NSRect = NSMakeRect(NSZeroPoint.x, NSZeroPoint.y, pathBounds.bounds.size.width, pathBounds.bounds.size.height )
let rotatedImage = NSImage(size: rotatedBounds.size)
//Center the image within the rotated bounds
imageBounds.origin.x = NSMidX(rotatedBounds) - (NSWidth(imageBounds) / 2)
imageBounds.origin.y = NSMidY(rotatedBounds) - (NSHeight(imageBounds) / 2)
// Start a new transform
transform = NSAffineTransform()
// Move coordinate system to the center (since we want to rotate around the center)
transform.translateX(by: +(NSWidth(rotatedBounds) / 2 ), yBy: +(NSHeight(rotatedBounds) / 2))
transform.rotate(byDegrees: degrees)
// Move the coordinate system bak to normal
transform.translateX(by: -(NSWidth(rotatedBounds) / 2 ), yBy: -(NSHeight(rotatedBounds) / 2))
// Draw the original image, rotated, into the new image
rotatedImage.lockFocus()
transform.concat()
self.draw(in: imageBounds, from: NSZeroRect, operation: NSCompositingOperation.copy, fraction: 1.0)
rotatedImage.unlockFocus()
return rotatedImage
}
}
class SomeClass: NSViewController {
var image = NSImage(named:"test.png")!.imageRotatedByDegreess(degrees: CGFloat(90)) //use only this values 90, 180, or 270
}
Thank for this solution, however it did not worked perfectly for me.
As you may have noticed that pathBounds is not used anywhere. In my opinion is has to be used like so:
let rotatedBounds:NSRect = NSMakeRect(NSZeroPoint.x, NSZeroPoint.y , pathBounds.bounds.size.width, pathBounds.bounds.size.height )
Otherwise the image will be rotated but cropped to a square bounds.
Letting IKImageView do the heavy lifting:
import Quartz
extension NSImage {
func imageRotated(by degrees: CGFloat) -> NSImage {
let imageRotator = IKImageView()
var imageRect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)
let cgImage = self.cgImage(forProposedRect: &imageRect, context: nil, hints: nil)
imageRotator.setImage(cgImage, imageProperties: [:])
imageRotator.rotationAngle = CGFloat(-(degrees / 180) * CGFloat(M_PI))
let rotatedCGImage = imageRotator.image().takeUnretainedValue()
return NSImage(cgImage: rotatedCGImage, size: NSSize.zero)
}
}
Here's a simple Swift (4+) solution to drawing an image that is rotated around the center:
extension NSImage {
/// Rotates the image by the specified degrees around the center.
/// Note that if the angle is not a multiple of 90°, parts of the rotated image may be drawn outside the image bounds.
func rotated(by angle: CGFloat) -> NSImage {
let img = NSImage(size: self.size, flipped: false, drawingHandler: { (rect) -> Bool in
let (width, height) = (rect.size.width, rect.size.height)
let transform = NSAffineTransform()
transform.translateX(by: width / 2, yBy: height / 2)
transform.rotate(byDegrees: angle)
transform.translateX(by: -width / 2, yBy: -height / 2)
transform.concat()
self.draw(in: rect)
return true
})
img.isTemplate = self.isTemplate // preserve the underlying image's template setting
return img
}
}
This one works also for non-square images, Swift 5.
extension NSImage {
func rotated(by degrees : CGFloat) -> NSImage {
var imageBounds = NSRect(x: 0, y: 0, width: size.width, height: size.height)
let rotatedSize = AffineTransform(rotationByDegrees: degrees).transform(size)
let newSize = CGSize(width: abs(rotatedSize.width), height: abs(rotatedSize.height))
let rotatedImage = NSImage(size: newSize)
imageBounds.origin = CGPoint(x: newSize.width / 2 - imageBounds.width / 2, y: newSize.height / 2 - imageBounds.height / 2)
let otherTransform = NSAffineTransform()
otherTransform.translateX(by: newSize.width / 2, yBy: newSize.height / 2)
otherTransform.rotate(byDegrees: degrees)
otherTransform.translateX(by: -newSize.width / 2, yBy: -newSize.height / 2)
rotatedImage.lockFocus()
otherTransform.concat()
draw(in: imageBounds, from: CGRect.zero, operation: NSCompositingOperation.copy, fraction: 1.0)
rotatedImage.unlockFocus()
return rotatedImage
}
}
Building on #FrankByte.com's code, this version should extend correctly in both x and y on any image and any rotation.
extension NSImage {
func rotated(by degrees: CGFloat) -> NSImage {
let sinDegrees = abs(sin(degrees * CGFloat.pi / 180.0))
let cosDegrees = abs(cos(degrees * CGFloat.pi / 180.0))
let newSize = CGSize(width: size.height * sinDegrees + size.width * cosDegrees,
height: size.width * sinDegrees + size.height * cosDegrees)
let imageBounds = NSRect(x: (newSize.width - size.width) / 2,
y: (newSize.height - size.height) / 2,
width: size.width, height: size.height)
let otherTransform = NSAffineTransform()
otherTransform.translateX(by: newSize.width / 2, yBy: newSize.height / 2)
otherTransform.rotate(byDegrees: degrees)
otherTransform.translateX(by: -newSize.width / 2, yBy: -newSize.height / 2)
let rotatedImage = NSImage(size: newSize)
rotatedImage.lockFocus()
otherTransform.concat()
draw(in: imageBounds, from: CGRect.zero, operation: NSCompositingOperation.copy, fraction: 1.0)
rotatedImage.unlockFocus()
return rotatedImage
}
}