How to rotate a CGRect about its centre? - ios

Background:
I want to show a label that is rotated 90 degrees clockwise. The rotated label should be in a certain frame that I specify. Let's say (10, 100, 50, 100).
I know that I can rotate a view by using CGAffineTransform. I also know that I should not set the frame property after a transformation is done, according to the docs.
When the value of this property is anything other than the identity transform, the value in the frame property is undefined and should be ignored.
I tried to set the frame after the transform and although it worked, I don't want to do something that the docs told me not to.
label2.transform = CGAffineTransform(rotationAngle: -.pi / 2)
label2.frame = CGRect(x: 10, y: 100, width: 50, height: 100)
Then I thought, I could do this:
create a CGRect of the frame that I want
rotate that rect 90 degrees anticlockwise
set that rect as the frame of the label
rotate the label 90 degrees clockwise
And then the label would be in the desired frame.
So I tried something like this:
let desiredFrame = CGRect(x: 10, y: 100, width: 50, height: 100) // step 1
// I am using UIViews here because I am just looking at their frames
// whether it is a UIView or UILabel does not matter
// label1 is a view that is in the desired frame but not rotated
// If label2 has the correct frame, it should completely cover label1
let label1 = UIView(frame: desiredFrame)
let label2Frame = rotateRect(label1.frame) // step 2
let label2 = UIView(frame: label2Frame) // step 3
view.addSubview(label1)
view.addSubview(label2)
label1.backgroundColor = .red
label2.backgroundColor = .blue
label2.transform = CGAffineTransform(rotationAngle: -.pi / 2) // step 4
Where rotateRect is declared like this:
func rotateRect(_ rect: CGRect) -> CGRect {
return rect.applying(CGAffineTransform(rotationAngle: .pi / 2))
}
This didn't work. label2 does not overlap label1 at all. I can't even see label2 at all on the screen.
I suspect that this is because the applying method in CGRect rotates the rect about the origin, instead of the centre of the rect. So I tried to first transform the rect to the origin, rotate it, then transform it back, as this post on math.SE said:
func rotateRect(_ rect: CGRect) -> CGRect {
let x = rect.x
let y = rect.y
let transform = CGAffineTransform(translationX: -x, y: -y)
.rotated(by: .pi / 2)
.translatedBy(x: x, y: y)
return rect.applying(transform)
}
However, I still cannot see label2 on the screen.

I think that the order of your transformations are incorrect. If you do it so, that,
Translate(x, y) * Rotate(θ) * Translate(-x, -y)
And using that with your rotateRect seem to work correctly. Since, you are rotating the view by 90 degrees, the blue view completely block red view. Try it out with some other angle and you shall see effect more prominently.
func rotateRect(_ rect: CGRect) -> CGRect {
let x = rect.midX
let y = rect.midY
let transform = CGAffineTransform(translationX: x, y: y)
.rotated(by: .pi / 2)
.translatedBy(x: -x, y: -y)
return rect.applying(transform)
}

Since I only want to rotate the view by 90 degrees, the width and height of the initial frame will be the reverse of the width and height of the rotated frame.
The centers of the initial frame and rotated are the same, so we can set the center of the label to the centre of the desired frame before the transformation.
let desiredFrame = CGRect(x: 10, y: 100, width: 50, height: 100)
let label1 = UIView(frame: desiredFrame)
// get the centre of the desired frame
// this is also equal to label1.center
let center = CGPoint(x: desiredFrame.midX, y: desiredFrame.midY)
let label2Frame = CGRect(x: 0, y: 0, width: desiredFrame.height, height: desiredFrame.width)
let label2 = UIView(frame: label2Frame)
view.addSubview(label1)
view.addSubview(label2)
label1.backgroundColor = .red
label2.backgroundColor = .blue
label2.center = center // set the centre of label2 before the transform
label2.transform = CGAffineTransform(rotationAngle: -.pi / 2)

Related

calculate new size and location on a CGRect

I have a CGRect (let's call it A1) with know size and location that has a parent A of know size. A1 has all sort of CGAffineTransform applied to it.
I need to find the location and size of a second rectangle relative to a parent B of known (but different dimensions) that would look proportional to A1 in relation to A
Basically a "zoom" effect. What would A1 dimensions and position be to look similar when placed in B . Kind of a "scale the CGAffineTransform" ?
visual guide:
From:
To:
Assuming you are transforming the rect like this:
scale it
move it
rotate it
First, you want to calculate the "scaleFactor" between the two sizes. So...
if rect A was 100 x 100
and rect B was 200 x 200
The scaleFactor would be 200 / 100 == 2 ... that is, B is 2x bigger.
For your values:
rect A is 375 x 375
rect B is 1080 x 1080
the scaleFactor is 1080 / 375 == 2.88
So, let's go with values that make the math easy (and obvious):
rect A is 200 x 200
rect A1 origin is x: 30 y: 20
rect A1 size is 50 x 25
rect B is 600 x 600
scaleFactor is 600 / 200 == 3, so
rect B1 origin is x: (30 * 3) y: (20 * 3)
rect B1 size is (50 * 3) x (25 * 3)
If that's all we've done so far, we would get this result:
Next, we need to move (translate) it, and scale the translation. If the original translation is:
CGAffineTransform(translationX: 30, y: 100)
the new translation is:
CGAffineTransform(translationX: 30 * scaleFactor, y: 100 * scaleFactor)
We don't need to scale the rotation, because, well, it's the same rotation.
So, after setting the original frame based on our origin and size calculations, then applying the same scale transform, plus the scaled translation transform, plus the same rotation transform, we get:
Here's example code I used to produce that:
class ViewController: UIViewController {
let viewA: UIView = {
let v = UIView()
v.backgroundColor = .systemYellow
return v
}()
let viewARedView: UIView = {
let v = UIView()
v.backgroundColor = .systemRed
return v
}()
let viewABlueView: UIView = {
let v = UIView()
v.backgroundColor = .systemBlue
return v
}()
let viewB: UIView = {
let v = UIView()
v.backgroundColor = .systemYellow
return v
}()
let viewBRedView: UIView = {
let v = UIView()
v.backgroundColor = .systemRed
return v
}()
let viewBBlueView: UIView = {
let v = UIView()
v.backgroundColor = .systemBlue
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.setNavigationBarHidden(true, animated: false)
view.addSubview(viewA)
view.addSubview(viewB)
viewA.frame = CGRect(x: 20, y: 60, width: 200, height: 200)
viewB.frame = CGRect(x: 20, y: viewA.frame.maxY + 20, width: 600, height: 600)
viewA.addSubview(viewARedView)
viewA.addSubview(viewABlueView)
let origRect: CGRect = CGRect(x: 30, y: 20, width: 50, height: 25)
// set frames of red and blue rect to the same rectangles
// so blue is on top of red
// we're going to transform the blue rect
viewARedView.frame = origRect
viewABlueView.frame = origRect
// scale values
let scX: CGFloat = 2.25
let scY: CGFloat = 1.75
// translate values
let trX: CGFloat = 30.0
let trY: CGFloat = 100.0
// rotation value
let rot: CGFloat = .pi * -0.15
var tx: CGAffineTransform = .identity
// scale it
tx = tx.concatenating(CGAffineTransform(scaleX: scX, y: scY))
// move it
tx = tx.concatenating(CGAffineTransform(translationX: trX, y: trY))
// rotate it
tx = tx.concatenating(CGAffineTransform(rotationAngle: rot))
// apply the transform
viewABlueView.transform = tx
viewB.addSubview(viewBRedView)
viewB.addSubview(viewBBlueView)
// get the scale factor...
// in this case,
// viewA width is 200
// viewB width is 700
// so viewB is 3.5 times bigger (700 / 200)
let scaleFactor: CGFloat = viewB.frame.width / viewA.frame.width
// make the original rect origin and size
// "scaleFactor bigger"
let newRect: CGRect = CGRect(x: origRect.origin.x * scaleFactor,
y: origRect.origin.y * scaleFactor,
width: origRect.width * scaleFactor,
height: origRect.height * scaleFactor)
// set frames of red and blue rect to the same rectangles
// so blue is on top of red
// we're going to transform the blue rect
viewBRedView.frame = newRect
viewBBlueView.frame = newRect
// scale the translation values
let trXb: CGFloat = trX * scaleFactor
let trYb: CGFloat = trY * scaleFactor
var txb: CGAffineTransform = .identity
// we've already changed the initial size of the rectangle,
// so use same scaling values
txb = txb.concatenating(CGAffineTransform(scaleX: scX, y: scY))
// we need to use scaled translation values
txb = txb.concatenating(CGAffineTransform(translationX: trXb, y: trYb))
// rotation doesn't change based on scale
txb = txb.concatenating(CGAffineTransform(rotationAngle: rot))
// apply the transform
viewBBlueView.transform = txb
}
}

How do I constrain an SKShapeNode to the device edges in SpriteKit?

Just to test things out I have created a blue square and placed it at the center of the screen like this:
let mySquare = SKShapeNode(rectOf: CGSize(width: 50, height: 50))
mySquare.fillColor = SKColor.blue
mySquare.lineWidth = 1
let myPoint = CGPoint(x: UIScreen.main.nativeBounds.midX, y: UIScreen.main.nativeBounds.midY)
mySquare.position.x = 0
mySquare.position.y = 0
self.addChild(mySquare)
Works great. Now, I would like to use constraints and set up the square constraints to the edges of the device screen. I have tried this, but the blue square doesn't appear, so I think I have the wrong idea on how to capture the CGPoint of the screen edges.
let mySquare = SKShapeNode(rectOf: CGSize(width: 50, height: 50))
mySquare.fillColor = SKColor.blue
mySquare.lineWidth = 1
let myPoint = CGPoint(x: UIScreen.main.nativeBounds.maxX, y: UIScreen.main.nativeBounds.maxY)
let range = SKRange(lowerLimit: 10.0, upperLimit: 10.0)
let myConstraints = SKConstraint.distance(range, to: myPoint)
mySquare.constraints = [myConstraints]
self.addChild(mySquare)
How do I capture the screen edges and constrain the square to those?
SKConstraint doesn't work equally as UIKit Constraints.
SKConstraint functionality is really specific:
Please take a look here: https://developer.apple.com/documentation/spritekit/skconstraint
Anyway, can give you some recommendations:
Transform screen position to scene position:
self.view?.convert(myPoint, to: self)
You can start with this example and get node on a corner
let mySquare = SKShapeNode(rectOf: CGSize(width: 50, height: 50))
mySquare.fillColor = SKColor.blue
mySquare.lineWidth = 1
let myScreenPoint = CGPoint(x: UIScreen.main.bounds.maxX, y: UIScreen.main.bounds.maxY)
if let myScenePoint = self.view?.convert(myScreenPoint, to: self) {
mySquare.position = myScenePoint
}
self.addChild(mySquare)
With this logic, you can get each side of the screen and decrease or increase margin and make each 4 sides; or 1 constraint for the center.

How to Bend or distortion a UILabel Text from one side

I have used attributed String and Libraries but unable to find any proper solution for Label Text effect.
Can Anyone please suggest me how i can achieve this functionality.?
Thanks
Adapting the code from How do I apply a perspective transform to a UIView?, here is some sample Swift 4 code that applies perspective to a label giving a result similar to your image.
let pview = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 300))
pview.backgroundColor = .black
let plabel = UILabel(frame: CGRect(x: 0, y: 110, width: 250, height: 75))
pview.addSubview(plabel)
plabel.text = " ND420 "
plabel.textAlignment = .center
plabel.font = UIFont.boldSystemFont(ofSize: 72)
plabel.textColor = .orange
plabel.backgroundColor = .red
plabel.sizeToFit()
let layer = plabel.layer
var transform = CATransform3DIdentity
transform.m34 = 1.0 / -200
transform = CATransform3DRotate(transform, -45 * CGFloat.pi / 180, 0, 1, 0)
layer.transform = transform
Run this in the playground and view pview.
Play with the value being assigned to transform.m34 and the rotation angle in CATransform3DRotate. A negative rotation angle (as shown above) makes the left side smaller and the right side larger. A positive angle does the opposite.

Drawing rectangles using the CGPoint coordinate system - SWIFT

I am able to draw a rectangle using the code below (works). However, I am using the Vison framework to detect rectangles but it is giving me back CGPoint values that is less than 1.0. When I enter these coordinates to draw a rectangle I get nothing back. Please can some advise?
Works - i get a rectangle
let rectangle = UIBezierPath.init()
rectangle.move(to: CGPoint.init(x: 100, y: 100))
rectangle.addLine(to: CGPoint.init(x: 200, y: 130))
rectangle.addLine(to: CGPoint.init(x: 300, y: 400))
rectangle.addLine(to: CGPoint.init(x: 100, y: 500))
rectangle.close()
let rec = CAShapeLayer.init()
rec.path = rectangle.cgPath
rec.fillColor = UIColor.red.cgColor
self.view.layer.addSublayer(rec)
Does not work (no rectangle):
let rectangle = UIBezierPath.init()
rectangle.move(to: CGPoint.init(x: 0.154599294066429, y: 0.904223263263702))
rectangle.addLine(to: CGPoint.init(x: 0.8810795545578, y: 0.970198452472687))
rectangle.addLine(to: CGPoint.init(x: 0.16680309176445, y: 0.0157230049371719))
rectangle.addLine(to: CGPoint.init(x: 0.878569722175598, y: 0.128135353326797))
rectangle.close()
let rec = CAShapeLayer.init()
rec.path = rectangle.cgPath
rec.fillColor = UIColor.red.cgColor
self.view.layer.addSublayer(rec)
The points returned by the Vision framework are coordinates represented by a percentage from the full size of the viewport. If your viewport is 640 x 480 (for example) then your first point is CGPoint(x: 0.154599294066429 * 640, y: 0.904223263263702 * 480). Taking that into account change your code to something like this and it should be fine:
let rectangle = UIBezierPath.init()
let width = // your width - Maybe UIScreen.main.bounds.size.width ?
let height = // your height - Maybe UIScreen.main.bounds.size.height ?
rectangle.move(to: CGPoint.init(x: width * 0.154599294066429, y: height * 0.904223263263702))
rectangle.addLine(to: CGPoint.init(x: width * 0.8810795545578, y: height * 0.970198452472687))
rectangle.addLine(to: CGPoint.init(x: width * 0.16680309176445, y: height * 0.0157230049371719))
rectangle.addLine(to: CGPoint.init(x: width * 0.878569722175598, y: height * 0.128135353326797))
rectangle.close()
let rec = CAShapeLayer.init()
rec.path = rectangle.cgPath
rec.fillColor = UIColor.red.cgColor
self.view.layer.addSublayer(rec)
Let's just analyse what it is you've got here. In the first example, you're drawing a rectangle of size of roughly 200px by 400px, ish... Of course, this will display exactly as you expect it to.
In the second example, you're drawing a rectangle of roughly 0.7px by 0.8px, ish. Now think about this logically, how is the screen supposed to represent 0.7 of a pixel? It can't! A pixel is the smallest representation you can have, that is, a single coloured square.
The code doesn't work because of physical constraints of the screen/system. You need to be using size (and position) values greater than 1 to see the rectangle. The Vision framework is no different, it will only see what you can see, if that makes sense.

How do I use this circle drawing code in a UIView?

I am making an app including some breathing techniques for a client. What he wants is to have a circle in the middle. For breathing in it becomes bigger, for breathing out tinier. The thing is, that he would like to have a cool animated circle in the middle, not just a standard one. I showed him this picture from YouTube:
The code used in the video looks like this:
func drawRotatedSquares() {
UIGraphicsBeginImageContextWithOptions(CGSize(width: 512, height: 512), false, 0)
let context = UIGraphicsGetCurrentContext()
context!.translateBy(x: 256, y: 256)
let rotations = 16
let amount = M_PI_2 / Double(rotations)
for i in 0 ..< rotations {
context!.rotate(by: CGFloat(amount))
//context!.addRect(context, CGRect(x: -128, y: -128, width: 256, height: 256))
context!.addRect(CGRect(x: -128, y: -128, width: 256, height: 256))
}
context!.setStrokeColor(UIColor.black as! CGColor)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
imageView.image = img
}
But if I run it, my simulator shows just a white screen. How do I get this circle into my Swift 3 app and how would the code look like? And is it possible not to show it in an ImageView but simply in a view?
Thank you very much!
Here is an implementation as a UIView subclass.
To set up:
Add this class to your Swift project.
Add a UIView to your Storyboard and change the class to Circle.
Add an outlet to your viewController
#IBOutlet var circle: Circle!
Change the value of multiplier to change the size of the circle.
circle.multiplier = 0.5 // 50% of size of view
class Circle: UIView {
var multiplier: CGFloat = 1.0 {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
// Calculate size of square edge that fits into the view
let size = min(bounds.width, bounds.height) * multiplier / CGFloat(sqrt(2)) / 2
// Move origin to center of the view
context.translateBy(x: center.x, y: center.y)
// Create a path to draw a square
let path = UIBezierPath()
path.move(to: CGPoint(x: -size, y: -size))
path.addLine(to: CGPoint(x: -size, y: size))
path.addLine(to: CGPoint(x: size, y: size))
path.addLine(to: CGPoint(x: size, y: -size))
path.close()
UIColor.black.setStroke()
let rotations = 16
let amount = .pi / 2 / Double(rotations)
for _ in 0 ..< rotations {
// Rotate the context
context.rotate(by: CGFloat(amount))
// Draw a square
path.stroke()
}
}
}
Here it is running in a Playground:
You posted a singe method that generates a UIImage and installs it in an image view. If you don't have the image view on-screen then it won't show up.
If you create an image view in your view controller and connect an outlet to the image view then the above code should install the image view into your image and draw it on-screen.
You could rewrite the code you posted as the draw(_:) method of a custom subclass of UIView, in which case you'd get rid of the context setup and UIImage stuff, and simply draw in the current context. I suggest you search on UIView custom draw(_:) methods for more guidance.

Resources