In my application, I am calculating BMI of a user based on its height and weight. So based on the BMI value I am animating a needle. The issue is that the needle moves off the center and rotates.
CODE:
ivPointer.layer.anchorPoint = CGPoint(x: 0, y: 0.5) // i have changed the values still its not working properly
let angle = Double((value * Double.pi)/180)
var transform = CGAffineTransform.identity.rotated(by: CGFloat(angle))
Related
I am expanding/reducing an image perfectly using CGAffineTransform, BUT it changes around the centre points of the image. I want to keep it fixed/anchored to the baseline? Is this possible? The image is displayed in a UIImageView.
UIView.animate(withDuration: 0.0, animations: {
// Sunrise
let multiplierSunrise = CGFloat(self.sunriseTime/self.sunriseMax)
var transformSunrise = CGAffineTransform.identity
transformSunrise = transformSunrise.translatedBy(x: 0, y: (self.imageHeight*(1-(multiplierSunrise/2))-(self.imageHeight/2)))
transformSunrise = transformSunrise.scaledBy(x: 1, y: 1*multiplierSunrise )
self.sunriseView.transform = transformSunrise
})
Transforms are applied around the view's layer's anchor point. This is the layer's bounds center by default, but you can move it, describing the desired anchor point in percentage terms. So if you want the transform to be applied around the bottom of the view, you could say:
self.sunriseView.layer.anchorPoint = CGPoint(x:0.5, y:1)
However, when you say that, the view itself will move! To prevent that, also move the view's center to compensate:
self.sunriseView.center.y += self.sunriseView.bounds.height/2
self.sunriseView.layer.anchorPoint = CGPoint(x:0.5, y:1)
Here's an example where, having done that, I proceed to apply a y-axis scale transform. As you can see, we are holding the bottom steady:
Im currently developing an iOS Application where you can process an image. (rotating, zooming, translating). Im using an uiimageview where i added gestures. This works fine but i also have some masking rectangle of a fixed size. Initial State
After i processed my image i want the content which is inside my masking rectangle.
I also want the four edge points of the masking rectangle of the processed image.
I know i have to apply the imageview transform to the points somehow, but its not working.
let points = maskView.edgePoints()
let translateTransform = CGAffineTransform(translationX: translationPoint.x, y: translationPoint.y)
let rotateTransform = CGAffineTransform(rotationAngle: CGFloat(rotationAngle))
let scaleTransform = CGAffineTransform(scaleX: xScale, y: yScale)
let finalTransform = rotateTransform.concatenating(scaleTransform).concatenating(translateTransform)
let topleftPoint = points[0].applying(finalTransform)
let toprightPoint = points[1].applying(finalTransform)
let bottomleftPoint = points[2].applying(finalTransform)
let bottomrightPoint = points[3].applying(finalTransform)
Edge point results: Sample
Topleft: (50.75, -8.75)
Topright: (63.6072332330383, -365.252863911729)
Bottomleft: (-172.064289944831, -16.7857707706489)
Bottomright: (-159.207056711792, -373.288634682378)
But the Topleft should be something like (0,0)
and the Bottomleft something like (40,200)?
Maybe you can give me some hints or useful links!
Thx in advance!
The problem lies in your transformation order. Right now your transformation order is Rotate Scale Translate, it should be Scale Rotate Translate instead.
let finalTransform = scaleTransform.concatenating(rotateTransform).concatenating(translateTransform)
I have created a big circle with a UIBezierPath and turned it into a Sprite using this,
let path = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: CGFloat(226), startAngle: 0.0, endAngle: CGFloat(M_PI * 2), clockwise: false)
// create a shape from the path and customize it
let shape = SKShapeNode(path: path.cgPath)
shape.lineWidth = 20
shape.position = center
shape.strokeColor = UIColor(red:0.98, green:0.99, blue:0.99, alpha:1.00)
let trackViewTexture = self.view!.texture(from: shape, crop: outerPath.bounds)
let trackViewSprite = SKSpriteNode(texture: trackViewTexture)
trackViewSprite.physicsBody = SKPhysicsBody(edgeChainFrom: innerPath.cgPath)
self.addChild(trackViewSprite)
It works fine. It creates the circle perfectly. But I need to resize it using
SKAction.resize(byWidth: -43, height: -43, duration: 0.3)
Which will make it a bit smaller. But, when it resizes the 20 line width I set now is very small because of the aspect fill. So when I shink it looks something like this:
But I need it to shrink like this-- keeping the 20 line width:
How would I do this?
Don't know if this would affect anything, but the sprites are rotating with an SKAction forever
-- EDIT --
Now, how do I use this method to scale to a specific size? Like turn 226x226 to 183x183?
Since by scaling down the circle, not only its radius gets scaled but its line's width too, you need to set a new lineWidth proportional with the scale. For example, when scaling the circle down by 2, you will need to double the lineWidth.
This can be done in two ways:
Setting the lineWidth in the completion block of the run(SKAction, completion: #escaping () -> Void) method. However this will result in seeing the line shrinking while the animation is running, then jumping to its new width once the animation finishes. If your animation is short, this may not be easy to observe tough.
Running a parallel animation together with the scaling one, which constantly adjusts the lineWidth. For this, you can use SKAction's customAction method.
Here is an example for your case:
let scale = CGFloat(0.5)
let finalLineWidth = initialLineWidth / scale
let animationDuration = 1.0
let scaleAction = SKAction.scale(by: scale, duration: animationDuration)
let lineWidthAction = SKAction.customAction(withDuration: animationDuration) { (shapeNode, time) in
if let shape = shapeNode as? SKShapeNode {
let progress = time / CGFloat(animationDuration)
shape.lineWidth = initialLineWidth + progress * (finalLineWidth - initialLineWidth)
}
}
let group = SKAction.group([scaleAction, lineWidthAction])
shape.run(group)
In this example, your shape will be scaled by 0.5, therefore in case of an initial line width of 10, the final width will be 20. First we create a scaleAction with a specified duration, then a custom action which will update the line's width every time its actionBlock is called, by using the progress of the animation to make the line's width look like it's not changing. At the end we group the two actions so they will run in parallel once you call run.
As a hint, you don't need to use Bezier paths to create circles, there is a init(circleOfRadius: CGFloat) initializer for SKShapeNode which creates a circle for you.
Before writing this question, I've
had experience with Affine transforms for views
read the Transforms documentation in the Quartz 2D Programming Guide
seen this detailed CALayer tutorial
downloaded and run the LayerPlayer project from Github
However, I'm still having trouble understanding how to do basic transforms on a layer. Finding explanations and simple examples for translate, rotate and scale has been difficult.
Today I finally decided to sit down, make a test project, and figure them out. My answer is below.
Notes:
I only do Swift, but if someone else wants to add the Objective-C code, be my guest.
At this point I am only concerned with understanding 2D transforms.
Basics
There are a number of different transforms you can do on a layer, but the basic ones are
translate (move)
scale
rotate
To do transforms on a CALayer, you set the layer's transform property to a CATransform3D type. For example, to translate a layer, you would do something like this:
myLayer.transform = CATransform3DMakeTranslation(20, 30, 0)
The word Make is used in the name for creating the initial transform: CATransform3DMakeTranslation. Subsequent transforms that are applied omit the Make. See, for example, this rotation followed by a translation:
let rotation = CATransform3DMakeRotation(CGFloat.pi * 30.0 / 180.0, 20, 20, 0)
myLayer.transform = CATransform3DTranslate(rotation, 20, 30, 0)
Now that we have the basis of how to make a transform, let's look at some examples of how to do each one. First, though, I'll show how I set up the project in case you want to play around with it, too.
Setup
For the following examples I set up a Single View Application and added a UIView with a light blue background to the storyboard. I hooked up the view to the view controller with the following code:
import UIKit
class ViewController: UIViewController {
var myLayer = CATextLayer()
#IBOutlet weak var myView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// setup the sublayer
addSubLayer()
// do the transform
transformExample()
}
func addSubLayer() {
myLayer.frame = CGRect(x: 0, y: 0, width: 100, height: 40)
myLayer.backgroundColor = UIColor.blue.cgColor
myLayer.string = "Hello"
myView.layer.addSublayer(myLayer)
}
//******** Replace this function with the examples below ********
func transformExample() {
// add transform code here ...
}
}
There are many different kinds of CALayer, but I chose to use CATextLayer so that the transforms will be more clear visually.
Translate
The translation transform moves the layer. The basic syntax is
CATransform3DMakeTranslation(_ tx: CGFloat, _ ty: CGFloat, _ tz: CGFloat)
where tx is the change in the x coordinates, ty is the change in y, and tz is the change in z.
Example
In iOS the origin of the coordinate system is in the top left, so if we wanted to move the layer 90 points to the right and 50 points down, we would do the following:
myLayer.transform = CATransform3DMakeTranslation(90, 50, 0)
Notes
Remember that you can paste this into the transformExample() method in the project code above.
Since we are just going to deal with two dimensions here, tz is set to 0.
The red line in the image above goes from the center of the original location to the center of the new location. That's because transforms are done in relation to the anchor point and the anchor point by default is in the center of the layer.
Scale
The scale transform stretches or squishes the layer. The basic syntax is
CATransform3DMakeScale(_ sx: CGFloat, _ sy: CGFloat, _ sz: CGFloat)
where sx, sy, and sz are the numbers by which to scale (multiply) the x, y, and z coordinates respectively.
Example
If we wanted to half the width and triple the height, we would do the following
myLayer.transform = CATransform3DMakeScale(0.5, 3.0, 1.0)
Notes
Since we are only working in two dimensions, we just multiply the z coordinates by 1.0 to leave them unaffected.
The red dot in the image above represents the anchor point. Notice how the scaling is done in relation to the anchor point. That is, everything is either stretched toward or away from the anchor point.
Rotate
The rotation transform rotates the layer around the anchor point (the center of the layer by default). The basic syntax is
CATransform3DMakeRotation(_ angle: CGFloat, _ x: CGFloat, _ y: CGFloat, _ z: CGFloat)
where angle is the angle in radians that the layer should be rotated and x, y, and z are the axes about which to rotate. Setting an axis to 0 cancels a rotation around that particular axis.
Example
If we wanted to rotate a layer clockwise 30 degrees, we would do the following:
let degrees = 30.0
let radians = CGFloat(degrees * Double.pi / 180)
myLayer.transform = CATransform3DMakeRotation(radians, 0.0, 0.0, 1.0)
Notes
Since we are working in two dimentions, we only want the xy plane to be rotated around the z axis. Thus we set x and y to 0.0 and set z to 1.0.
This rotated the layer in a clockwise direction. We could have rotated counterclockwise by setting z to -1.0.
The red dot shows where the anchor point is. The rotation is done around the anchor point.
Multiple transforms
In order to combine multiple transforms we could use concatination like this
CATransform3DConcat(_ a: CATransform3D, _ b: CATransform3D)
However, we will just do one after another. The first transform will use the Make in its name. The following transforms will not use Make, but they will take the previous transform as a parameter.
Example
This time we combine all three of the previous transforms.
let degrees = 30.0
let radians = CGFloat(degrees * Double.pi / 180)
// translate
var transform = CATransform3DMakeTranslation(90, 50, 0)
// rotate
transform = CATransform3DRotate(transform, radians, 0.0, 0.0, 1.0)
// scale
transform = CATransform3DScale(transform, 0.5, 3.0, 1.0)
// apply the transforms
myLayer.transform = transform
Notes
The order that the transforms are done in matters.
Everything was done in relation to the anchor point (red dot).
A Note about Anchor Point and Position
We did all our transforms above without changing the anchor point. Sometimes it is necessary to change it, though, like if you want to rotate around some other point besides the center. However, this can be a little tricky.
The anchor point and position are both at the same place. The anchor point is expressed as a unit of the layer's coordinate system (default is 0.5, 0.5) and the position is expressed in the superlayer's coordinate system. They can be set like this
myLayer.anchorPoint = CGPoint(x: 0.0, y: 1.0)
myLayer.position = CGPoint(x: 50, y: 50)
If you only set the anchor point without changing the position, then the frame changes so that the position will be in the right spot. Or more precisely, the frame is recalculated based on the new anchor point and old position. This usually gives unexpected results. The following two articles have an excellent discussion of this.
About the anchorPoint
Translate rotate translate?
See also
Border, rounded corners, and shadow on a CALayer
Using a border with a Bezier path for a layer
I'm learning SpriteKit and Swift, and I'm trying to make 5 small images "spawn" at the top of the screen, and slide down to the bottom. This is what I got so far, added in the didMoveToView method.
var myArray = NSMutableArray()
myArray.addObject(NSNumber(int: 40))
myArray.addObject(NSNumber(int: 80))
myArray.addObject(NSNumber(int: 120))
myArray.addObject(NSNumber(int: 160))
myArray.addObject(NSNumber(int: 200))
for item in myArray
{
let location = CGPoint(x: CGFloat(item.floatValue), y: 1)
let sprite = SKSpriteNode(imageNamed: "myImage")
sprite.xScale = 0.5
sprite.yScale = 0.5
sprite.position = location
let action = SKAction.moveToY(-4, duration: 4.5)
sprite.runAction(action)
addChild(sprite)
}
But all the objects appear at the bottom. I've tried to change the y-position, with the same result..
Thanks!
You need to increase the y coordinate.
Think about the axis system as the left bottom corner is (0,0) and the top right is (self.frame.size.width,self.frame.size.height)
You may also want to read about anchor points - the default anchor point for an object is 0.5,0.5 so if you locate it on 1 half of it (+ 1 pixel) will appear from the top.
If you want it to pop in from out of the screen you need either to change the anchor point or the poisition
Try changing your location variable to this:
let location = CGPoint(x: CGFloat(item.floatValue) , y:self.size.height)
This will set the y-value to the top of the screen and move the nodes there.