How to make two lines in UIBezierPath to animate at different speed - xcode

Say the following scenario:
I have drawn a quadrilateral shape, which is a mask for a UIView. I denote the shape layer as maskLayer. maskLayer crops the bottom of the UIView asymmetrically.
But then I want to fully reveal my UIView in an animation. The animation should be left side of maskLayer drops down to the bottom of UIView, and .2 sec later my right side of maskLayer also drops down to the bottom of UIView, thus fully reveal the entity of UIView.
My approach is to drop down left line first, then right one as the following code:
//this quadrilateral will put down left corner to the bottom of screen
var path2 = UIBezierPath()
path2.moveToPoint(CGPointZero)
path2.addLineToPoint(CGPoint(x: 0, y: frame.height))
path2.addLineToPoint(CGPoint(x: frame.width, y: frame.height / goldRatio / goldRatio))
path2.addLineToPoint(CGPoint(x: frame.width, y: 0))
path2.closePath()
//this rectangle path will put down both corner to the bottom of screen
//thus fix the view to its original shape
var path3 = UIBezierPath()
path3.moveToPoint(CGPointZero)
path3.addLineToPoint(CGPoint(x: 0, y: frame.height))
path3.addLineToPoint(CGPoint(x: frame.width, y: frame.height))
path3.addLineToPoint(CGPoint(x: frame.width, y: 0))
path3.closePath()
I have spent 2 hours trying to figure it out to no avail. May you please give me some instructions about how to achieve just that.
The initial state is like the following:
The end state is like the following:
I truly appreciate your help!

Easiest way to do this... Cheat.
Don't try to animate the path of the shape it really doesn't need it.
What you should do is something like this.
Create your final state view. Rectangular, at the bottom of the screen with the UI on it etc...
This final state view will not move. It will always be here.
Now create another view and insert it as a sub view underneath the final state view. On this you can add a shape layer with the angular corner cut off.
Now all you need to do is animate the position of this angular view downward until it is completely below the final state view.
If they are the same colour the this will give the effect of animating the path of the shape.
To get the different speeds you could have a rectangluar shape layer rotated to 45 degrees. Then animate it to 0 degrees as the view slides down?
In fact, you could do this with a single shape layer that is rotated and moved.

To do this sort of animation, you would generally use a CADisplayLink (sort of like a timer, but linked to updates of the display rather than some arbitrary interval) to repeatedly change the path associated with desired shape. I think this is easiest if you use a CAShapeLayer and just change the path property of this shape.
To make this work, you need a function that represents the path at a given point of time (or easier, a path at an instant a certain percentageComplete along the animation duration). Since you have a regular shape (constantly the same number of points), you can simply interpolate between some array of startPoints and endPoints.
So, create the shape layer, capture the start time, start the display link, and then for every "tick" of the display link, calculate what percentage of the total animationDuration has passed, and update the shape layer's path accordingly:
class ViewController: UIViewController {
let animationDuration = 2.0
var displayLink: CADisplayLink!
var startTime: CFAbsoluteTime!
var shapeLayer: CAShapeLayer!
let goldRatio: CGFloat = 1.6180339887
var startPoints:[CGPoint]!
var endPoints:[CGPoint]!
override func viewDidLoad() {
super.viewDidLoad()
self.startAnimation()
}
func startAnimation() {
startPoints = [
CGPoint(x: 0, y: view.bounds.size.height),
CGPoint(x: 0, y: view.bounds.size.height - view.bounds.size.height / goldRatio),
CGPoint(x: view.bounds.size.width, y: 0),
CGPoint(x: view.bounds.size.width, y: view.bounds.size.height)
]
endPoints = [
CGPoint(x: 0, y: view.bounds.size.height),
CGPoint(x: 0, y: view.bounds.size.height * 0.75),
CGPoint(x: view.bounds.size.width, y: view.bounds.size.height * 0.75),
CGPoint(x: view.bounds.size.width, y: view.bounds.size.height)
]
assert(startPoints.count == endPoints.count, "Point counts don't match")
createShape()
startDisplayLink()
}
func startDisplayLink() {
displayLink = CADisplayLink(target: self, selector: "handleDisplayLink:")
startTime = CFAbsoluteTimeGetCurrent()
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
func stopDisplayLink() {
displayLink.invalidate()
displayLink = nil
}
func handleDisplayLink(displayLink: CADisplayLink) {
var percent = CGFloat((CFAbsoluteTimeGetCurrent() - startTime) / animationDuration)
if percent >= 1.0 {
percent = 1.0
stopDisplayLink()
}
updatePathBasedUponPercentComplete(percent)
}
func createShape() {
shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.blueColor().CGColor
shapeLayer.strokeColor = UIColor.clearColor().CGColor
updatePathBasedUponPercentComplete(0.0)
view.layer.addSublayer(shapeLayer)
}
func updatePathBasedUponPercentComplete(percentComplete: CGFloat) {
shapeLayer.path = pathBasedUponPercentComplete(percentComplete, frame: view.frame).CGPath
}
func pathBasedUponPercentComplete(percentComplete: CGFloat, frame: CGRect) -> UIBezierPath {
var path = UIBezierPath()
for i in 0 ..< startPoints.count {
let point = CGPoint(
x: startPoints[i].x + (endPoints[i].x - startPoints[i].x) * percentComplete,
y: startPoints[i].y + (endPoints[i].y - startPoints[i].y) * percentComplete
)
if i == 0 {
path.moveToPoint(point)
} else {
path.addLineToPoint(point)
}
}
path.closePath()
return path
}
}

Related

Animating a view in a real time

I am trying to create animation that is connected to real time.
I have two points on my screen and an image moves between them. So for example I have an image that is at the Strat point at 12:00 and I want it to be at the end point at 12:10.
func animate_this_image(start: Date, end: Date, imageView: UIImageView, start_point: CGPoint, end_point: CGPoint) {
let duration = end.timeIntervalSince(start)
let frame = CGRect(x: end_point.x, y: end_point.y, width: 60, height: 60).offsetBy(dx: -30, dy: -30)
UIView.animate(withDuration: duration) {
imageView.frame = frame
}
}
My problem is that I want it to be connected to the actual clock. So for example if I close the view at 12:02 and open it at 12:09 I want the image to be at the correct place between the two point (roughly at the end). Instead at the moment the image starts again from the start point. That is happening because I just take the two dates and use the difference as the animation duration.
Anyone that has had similar problem or a suggestion on how I can solve this?
This is connected to a map. I have two points and a poly line between the points. I wanted an image to glide between the points. I could not figure anything easy with the map so I created a view on top of the map that has the image view on. I transfer the points from the map to the other view to get the start and stop points. Maybe there is a better way to do this by using the map?
Cheers,
Jonas
This was easier than I thought... this is my solution
func animate_this_image(start: Date, end: Date, imageView: UIImageView, start_point: CGPoint, end_point: CGPoint) {
// find the total duration and the time left until reaching next point
let duration = end.timeIntervalSince(start)
let time_left = end.timeIntervalSince(Date())
// the percentage is used to calculate how far is left on the line
let percentage = time_left / duration
//calculate the correct point at this time
let x_dist = (end_point.x - start_point.x) * CGFloat(percentage)
let y_dist = (end_point.y - start_point.y) * CGFloat(percentage)
imageView.frame = CGRect(x: end_point.x - x_dist, y: end_point.y - y_dist, width: 60, height: 60).offsetBy(dx: -30, dy: -30)
// define end point
let frame = CGRect(x: end_point.x, y: end_point.y, width: 60, height: 60).offsetBy(dx: -30, dy: -30)
// Then animate it
UIView.animate(withDuration: time_left) {
imageView.frame = frame
}
}

How to animate more than two points on a trapezoid?

Part of the UI that I'm working on has large elements changing size in response to user input. Animating the views changing size is straightforward; what I'm working on now is building trapezoids that provide a visual transition between similarly colored rectangles (Views) of different sizes, and I want the length of the trapezoids' tops & bottoms to change size smoothly as the rectangles above & below change size.
So far, I have the following code which can animate the two bottom points changing in response to user input, and it works fine. What I need to do is extend this so that not just TWO x-coordinates can change with animation, but all FOUR x-coordinates. (The y-coordinates don't vary, so all that is being passed that needs to animate are the x values.)
struct NewTrapezoid: Shape {
var xTopLeft : Double
var xTopRight : Double
var xBottomLeft : Double
var xBottomRight : Double
var height : Double
var animatableData: AnimatablePair<Double, Double> {
// top two points WERE always fixed -- the base of the top rectangle
// what needs to happen NEXT is that these two top points can animate. Present code, they don't.
// the y-coord of the bottom two points are fixed -- height of the transition zone
// these two doubles are the X coords of the two bottom points. They animate fine.
get { AnimatablePair(xBottomLeft, xBottomRight) }
set {
xBottomLeft = newValue.first
xBottomRight = newValue.second
}
}
}
func path(in rect: CGRect) -> Path {
let topLeft: CGPoint = CGPoint(x: xTopLeft, y: 0)
let bottomLeft: CGPoint = CGPoint(x: xBottomLeft, y: height)
let topRight: CGPoint = CGPoint(x: xTopRight, y: 0)
let bottomRight: CGPoint = CGPoint(x: xBottomRight, y: height)
var path = Path()
path.move(to: topLeft)
path.addLine(to: topRight)
path.addLine(to: bottomRight)
path.addLine(to: bottomLeft)
path.addLine(to: topLeft)
return path
}
}
What I suspect would make things easy is if, instead of AnimatablePair<Double, Double> I could have something like AnimatableMany<Double, Double, Double, Double> but I haven't found a way to do that.
Any help would be most appreciated!
For multiple values, you can cascade/nest AnimatablePairs like this:
AnimatablePair< Double, AnimatablePair< Double, AnimatablePair< Double, Double>>>
But as you can see, this is not very flexible and usable for more than 4 or 5 values
A better solution is to create a custom type that conforms to VectorArithmetics, like this AnimatableVector

How to Animate a UIImage out of the Screen in Swift

i have searched around the web but nothing could answer my question.
I am trying to animate an UIImageview out of the Screen, set the alpha of it to 0, reset its position and reset the alpha to 1 again.
I can't seem to get the right position where to animate the Image to.
I have set up a gesturerecognizer to drag the image around and when the center of the image is above a certain point, i want to animate it out of screen.
This is the Regocnizercode :
#objc func imageDragged(gestureRecognizer: UIPanGestureRecognizer){
//Current Point where the label is dragged
let draggedLabelPoint = gestureRecognizer.translation(in: view)
//Updating the center of the label : viewcenter +/- currentpoint
matchImageView.center = CGPoint(x: view.bounds.width/2 + draggedLabelPoint.x, y: view.bounds.height/2 + draggedLabelPoint.y)
let xFromCenter = view.bounds.width/2 - matchImageView.center.x
var rotation = CGAffineTransform(rotationAngle: xFromCenter / -500)
let scale = min(100/abs(xFromCenter),1)
var scaledAndRotated = rotation.scaledBy(x: scale, y: scale)
//Transforming the Item respective to the distance from center
matchImageView.transform = scaledAndRotated
if gestureRecognizer.state == .ended {
if matchImageView.center.x < (view.bounds.width/2 - 100) {
animateImageToSide(target: CGPoint(x: view.bounds.width - 200, y: matchImageView.center.y))
matchImageView.alpha = 0
}
if matchImageView.center.x > (view.bounds.width/2 + 100) {
animateImageToSide(target: CGPoint(x: view.bounds.width + 200, y: matchImageView.center.y))
matchImageView.alpha = 0
}
//Reset the scaling variables and recentering the Item after swipe
rotation = CGAffineTransform(rotationAngle: 0)
scaledAndRotated = rotation.scaledBy(x: 1, y: 1)
matchImageView.transform = scaledAndRotated
matchImageView.center = CGPoint(x: view.bounds.width/2, y: view.bounds.height/2)
}
}
I am sorry if the resetting part is irrelevant for you, but im not sure if this could cause this behavior.
And this is my current Animationmethod :
func animateImageToSide(target:CGPoint){
UIImageView.animate(withDuration: 0.5, delay: 0, options: .curveLinear, animations: {self.matchImageView.center = target}) { (success: Bool) in
self.updateImage()
self.fadeInImage()//This is where i set the alpha back to 1
}
}
I am not sure if a CGPoint is what i need to use.
The current behavoir of the image is very weird. Most of the time it snaps to a specific place, regardless of the targetposition. Maybe my attempt on this is entirely wrong.
Thanks for your help !

What is the correct way to determine the bounds of NSBezierPath to ensure correct repainting on macOS

I am having some trouble figuring out the correct way to determine the bounds of NZBezierPath for redrawing correctly.
As you can see from the picture below the thin border line seems to be falling outside the NSBezierPath.bounds rectangle.
The code for handling the object dragging is as follows:
func offsetItemLocationBy(item: DItem, x: CGFloat, y:CGFloat)
{
// tell the display to redraw the old rect
let oldBounds = item.bounds // NSBezierPath.bounds
// since the offset can be generated by both mouse moves
// and moveUp:, moveDown:, etc.. actions, we'll invert
// the deltaY amount based on if the view is flipped or
// not.
let invertDeltaY: CGFloat = self.isFlipped ? -1.0: 1.0
let y1=y*invertDeltaY
item.offsetLocationBy(x: x, y: y1) // Creates a new NSBezierPath
self.setNeedsDisplay(oldBounds)
// invalidate the new rect location so that it'll
// be redrawn
self.setNeedsDisplay(item.bounds)
}
Should I be using some other method to calculate the bounds of NSBezierPath?
OK, I just figured it out - NSBezierPath.bounds does not include the LINE WIDTH.
So to calculate the correct bounds subtract lineWidth/2.0 from x and y and add lineWidth to width and height.
var bounds: NSRect {
let b = path.bounds
let sw = path.lineWidth/2.0
let boundsRect = NSRect(x: b.minX-sw, y: b.minY-sw, width: b.width+2*sw, height: b.height+2*sw)
return boundsRect
}

How to rotate UIBezierPath around center of its own bounds?

Lets say we have a UIBezierPath... the bounds of which are perfectly square... like this:
func getExponentPath(rotate180: Bool) -> UIBezierPath {
// establish unit of measure (grid) based on this containing view's bounds... (not to be confused with this bezierpath's bounds)
let G = bounds.width / 5
let exponentPath = UIBezierPath()
let sstartPoint = CGPoint(x:(3.8)*G,y:(1.2)*G)
exponentPath.move(to: sstartPoint)
exponentPath.addLine(to: CGPoint(x:(5)*G,y:(1.2)*G))
exponentPath.addLine(to: CGPoint(x:(4.4)*G,y:(0.2)*G))
exponentPath.addLine(to: CGPoint(x:(5)*G,y:(0.2)*G))
exponentPath.addLine(to: CGPoint(x:(5)*G,y:(0)*G))
exponentPath.addLine(to: CGPoint(x:(3.8)*G,y:(0)*G))
exponentPath.addLine(to: CGPoint(x:(3.8)*G,y:(0.2)*G))
exponentPath.addLine(to: CGPoint(x:(4.4)*G,y:(0.2)*G))
exponentPath.addLine(to: sstartPoint)
exponentPath.close()
// this does not work:
// if rotate180 { exponentPath.apply(CGAffineTransform(rotationAngle: CGFloat.pi)) }
return exponentPath
}
If rotated, this bezierpath still needs to occupy the exact same area within its containing view.
I can only presume this does not work because there's some problem with the center of rotation not being what I intend... although I get the same (wrong) result even when saying "rotate by 0."
So how can the path be rotated around it's own center point?
It seems like there should be a simple linear algebra matrix multiplication type thingy that could be applied to the set of points. =T
extension UIBezierPath
{
func rotateAroundCenter(angle: CGFloat)
{
let center = self.bounds.getCenter()
var transform = CGAffineTransform.identity
transform = transform.translatedBy(x: center.x, y: center.y)
transform = transform.rotated(by: angle)
transform = transform.translatedBy(x: -center.x, y: -center.y)
self.apply(transform)
}
}
I don't think you need the rotation. To draw the same shape upside down, just flip it:
exponentPath.apply(CGAffineTransform(scaleX: 1, y: -1))
exponentPath.apply(CGAffineTransform(translationX: 0, y: G))
So in case anyone else is trying to rotate a UIBezierPath on the center of it's own bounding rectangle... this is the actual working solution arrived at with help from previous answers/comments:
func getExponentPath(rotationAngle: CGFloat) -> UIBezierPath {
// ...
let x_translation = -( (bounds.width) - ( exponentPath.bounds.width/2) )
let y_translation = -exponentPath.bounds.height/2
exponentPath.apply(CGAffineTransform(translationX: x_translation, y: y_translation))
exponentPath.apply(CGAffineTransform(rotationAngle: rotationAngle))
exponentPath.apply(CGAffineTransform(translationX: -x_translation, y: -y_translation))
// ...
}

Resources