I'm not sure how to create this animation. Would you somehow split the 1 jpg file evenly in 3 pieces and animate that? Or would you have to make multiple copies of the jpg and do something with them?
Any help would be awesome!
UPDATE
Since you want a crossfade, it's probably easiest to do this by splitting the image into separate cel images:
import UIKit
import PlaygroundSupport
extension UIImage {
func subImage(inUnitRect unitRect: CGRect) -> UIImage? {
guard imageOrientation == .up, let cgImage = self.cgImage else { return nil }
let cgImageWidth = CGFloat(cgImage.width)
let cgImageHeight = CGFloat(cgImage.height)
let scaledRect = CGRect(x: unitRect.origin.x * cgImageWidth, y: unitRect.origin.y * cgImageHeight, width: unitRect.size.width * cgImageWidth, height: unitRect.size.height * cgImageHeight)
guard let croppedCgImage = cgImage.cropping(to: scaledRect) else { return nil }
return UIImage(cgImage: croppedCgImage, scale: scale, orientation: .up)
}
}
let image = #imageLiteral(resourceName: "image.png")
let celCount: CGFloat = 3
let cels = stride(from: 0, to: celCount, by: 1).map({ (i) -> UIImage in
image.subImage(inUnitRect: CGRect(x: i / celCount, y: 0, width: 1/3, height: 1))!
})
Then we can use a keyframe animation to crossfade the layer contents:
let imageView = UIImageView(image: cels[0])
PlaygroundPage.current.liveView = imageView
let animation = CAKeyframeAnimation(keyPath: "contents")
var values = [CGImage]()
var keyTimes = [Double]()
for (i, cel) in cels.enumerated() {
keyTimes.append(Double(i) / Double(cels.count))
values.append(cel.cgImage!)
// The 0.9 means 90% of the time will be spent *outside* of crossfade.
keyTimes.append((Double(i) + 0.9) / Double(cels.count))
values.append(cel.cgImage!)
}
values.append(cels[0].cgImage!)
keyTimes.append(1.0)
animation.keyTimes = keyTimes.map({ NSNumber(value: $0) })
animation.values = values
animation.repeatCount = .infinity
animation.duration = 5
imageView.layer.add(animation, forKey: animation.keyPath)
Result:
ORIGINAL
There are multiple ways you can do this. One is by setting or animating the contentsRect property of the image view's layer.
In your image, there are three cels, and each occupies exactly 1/3 of the image. The contentsRect is in the unit coordinate space, which makes computation easy. The contentsRect for cel i is CGRect(x: i/3, y: 0, width: 1/3, height: 0).
You want discrete jumps between cels, instead of smooth sliding transitions, so you need to use a keyframe animation with a kCAAnimationDiscrete calculationMode.
import UIKit
import PlaygroundSupport
let image = #imageLiteral(resourceName: "image.png")
let celSize = CGSize(width: image.size.width / 3, height: image.size.height)
let imageView = UIImageView()
imageView.frame = CGRect(origin: .zero, size: celSize)
imageView.image = image
PlaygroundPage.current.liveView = imageView
let animation = CAKeyframeAnimation(keyPath: "contentsRect")
animation.duration = 1.5
animation.calculationMode = kCAAnimationDiscrete
animation.repeatCount = .infinity
animation.values = [
CGRect(x: 0, y: 0, width: 1/3.0, height: 1),
CGRect(x: 1/3.0, y: 0, width: 1/3.0, height: 1),
CGRect(x: 2/3.0, y: 0, width: 1/3.0, height: 1)
] as [CGRect]
imageView.layer.add(animation, forKey: animation.keyPath!)
Result:
Related
I have attached a gif of what the animation looks like when setup as described below. How can I ensure that all 6 instances of the replicatorLayer finish animating and then remove the leftover coin image (i feel using dispatchQueue.asyncAfter is insufficient for animationPurposes, but am unaware of another solution).
I have a CAReplicationLayer as the first and only subLayer of my views primary CALayer object.
let replicatorLayer = CAReplicatorLayer()
replicatorLayer.instanceDelay = TimeInterval(0.1)
replicatorLayer.instanceCount = 6
replicatorLayer.frame.size = frame.size
layer.addSublayer(replicatorLayer)
I create another CALayer for an image and some text and add it as a sublayer replicatorLayer
let coinWithTextLayer = CALayer()
coinWithTextLayer.frame = CGRect(x: circleCenter.x, y: circleCenter.y, width: 61, height: 67)
coinWithTextLayer.masksToBounds = false
let imageLayer = CALayer()
imageLayer.contents = image.cgImage
imageLayer.frame = CGRect(x: 0, y: 20, width: 49, height: 49)
imageLayer.contentsGravity = .resizeAspect
let textLayer = CATextLayer()
textLayer.frame = CGRect(x: 34, y: 0, width: 40, height: 19)
textLayer.fontSize = 20
textLayer.string = "+ \(5)"
coinWithTextLayer.addSublayer(imageLayer)
coinWithTextLayer.addSublayer(textLayer)
replicatorLayer.addSublayer(coinWithTextLayer)
I then apply an animation group to the coinWithTextLayer that animates it up and to the right, while also scaling from 0.1 to 1
let positionAnim1 = CABasicAnimation(keyPath: "position")
positionAnim1.fromValue = circleCenter
positionAnim1.toValue = CGPoint(x: circleCenter.x + 24, y: circleCenter.y - 24)
let scaleAnim1 = CABasicAnimation(keyPath: "transform.scale")
scaleAnim1.fromValue = 0.1
scaleAnim1.toValue = 1
scaleAnim1.delegate = self
let firstAnimationGroup = CAAnimationGroup()
firstAnimationGroup.beginTime = 0
firstAnimationGroup.duration = 2
firstAnimationGroup.autoreverses = false
firstAnimationGroup.animations = [positionAnim1, scaleAnim1]
firstAnimationGroup.delegate = self
firstAnimationGroup.fillMode = .backwards
coinWithTextLayer.add(firstAnimationGroup, forKey: "tapCoinAnim1")
/*DispatchQueue.main.asyncAfter(seconds: 0.5 + (0.1 * 6)) { // 0.1 * 6 to account for the delay between instances of the replicatorLayer
coinWithTextLayer.removeFromSuperlayer()
coinWithTextLayer.removeAllAnimations()
}*/ // Commented out because it does successfuly remove the coin after the animation, but feels like an incorrect/inefficient way of doing so
The code is added to Github to let you understand the real problem.
This is the hierarchy:
-- ViewController.View P [width: 375, height: 667]
---- UIImageView A [width: 375, height: 667] Name: imgBackground
[A is holding an image of size(1287,1662)]
---- UIImageView B [width: 100, height: 100] Name: imgForeground
[B is holding an image of size(2400,982)]
I am trying to merge A with B but the result is stretched.
This is the merge code:
func mixImagesWith(frontImage:UIImage?, backgroundImage: UIImage?, atPoint point:CGPoint, ofSize signatureSize:CGSize) -> UIImage {
let size = self.imgBackground.frame.size
UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
backgroundImage?.draw(in: CGRect.init(x: 0, y: 0, width: size.width, height: size.height))
frontImage?.draw(in: CGRect.init(x: point.x, y: point.y, width: signatureSize.width, height: signatureSize.height))
let newImage:UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return newImage
}
Note:
.contentMode = .scaleAspectFit
Code works but the result is stretched.
See this line in code, let size = self.imgBackground.frame.size – I need to change this to fix the problem. Find the origin of subview with respect to UIImage size
Here's the screenshot to understand the problem:
What should I do to get the proper output of merge function?
You have two bugs in your code:
You should also calculate aspect for document image to fit it into UIImageView. In mergeImages() replace:
img.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
with:
img.draw(in: getAspectFitFrame(sizeImgView: size, sizeImage: img.size))
When calculating aspect you center image horizontally/vertically if its width/height less then UIImageView width/height. But instead of comparing newWidth and newHeight you should compare factors:
if hfactor > vfactor {
y = (sizeImgView.height - newHeight) / 2
} else {
x = (sizeImgView.width - newWidth) / 2
}
Try bellow code it works for me, hope it works for you too,
func addWaterMarkToImage(img:UIImage, sizeWaterMark:CGRect, waterMarkImage:UIImage, completion : ((UIImage)->())?){
handler = completion
let img2:UIImage = waterMarkImage
let rect = CGRect(x: 0, y: 0, width: img.size.width, height: img.size.height)
UIGraphicsBeginImageContext(img.size)
img.draw(in: rect)
let frameAspect:CGRect = getAspectFitFrame(sizeImgView: sizeWaterMark.size, sizeImage: waterMarkImage.size)
let frameOrig:CGRect = CGRect(x: sizeWaterMark.origin.x+frameAspect.origin.x, y: sizeWaterMark.origin.y+frameAspect.origin.y, width: frameAspect.size.width, height: frameAspect.size.height)
img2.draw(in: frameOrig, blendMode: .normal, alpha: 1)
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
if handler != nil {
handler!(result!)
}
}
//MARK - Get Aspect Fit frame of UIImage
func getAspectFitFrame(sizeImgView:CGSize, sizeImage:CGSize) -> CGRect{
let imageSize:CGSize = sizeImage
let viewSize:CGSize = sizeImgView
let hfactor : CGFloat = imageSize.width/viewSize.width
let vfactor : CGFloat = imageSize.height/viewSize.height
let factor : CGFloat = max(hfactor, vfactor)
// Divide the size by the greater of the vertical or horizontal shrinkage factor
let newWidth : CGFloat = imageSize.width / factor
let newHeight : CGFloat = imageSize.height / factor
var x:CGFloat = 0.0
var y:CGFloat = 0.0
if newWidth > newHeight{
y = (sizeImgView.height - newHeight)/2
}
if newHeight > newWidth{
x = (sizeImgView.width - newWidth)/2
}
let newRect:CGRect = CGRect(x: x, y: y, width: newWidth, height: newHeight)
return newRect
}
I have been exploring this questions through playgrounds for days now without finding any solution.
I am been managing to create a hole in a blurred UIView. Now I am wondering how to animate the radius of this circle, ergo animating the blurred view's mask's path.
Here is my code so far:
import Foundation
import UIKit
import PlaygroundSupport
let contentRect = CGRect(x: 0, y: 0, width: 400, height: 400)
let contentView = UIView(frame: contentRect)
contentView.backgroundColor = .white
PlaygroundPage.current.liveView = contentView
let image = UIImage(named: "landscape.jpg")
let imageView = UIImageView(frame: contentRect, image: image)
imageView.contentMode = .scaleAspectFill
let blur = UIBlurEffect(style: .light)
let blurView = UIVisualEffectView(effect: blur)
blurView.frame = contentRect
let path = UIBezierPath(rect: contentRect)
let circle = UIBezierPath(ovalIn: CGRect(x: 50, y: 50, width: 300, height: 300))
path.append(circle)
path.usesEvenOddFillRule = true
let toPath = UIBezierPath(rect: contentRect)
let toCircle = UIBezierPath(ovalIn: CGRect(x: 150, y: 150, width: 100, height: 100))
toPath.append(toCircle)
toPath.usesEvenOddFillRule = true
let hole = CAShapeLayer()
hole.path = path.cgPath
hole.fillColor = UIColor.green.cgColor
hole.fillRule = kCAFillRuleEvenOdd
let mask = UIView(frame: contentRect)
contentView.addSubview(imageView)
contentView.addSubview(blurView)
mask.layer.addSublayer(hole)
blurView.mask = mask
I have been trying this, without success:
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = hole.path
animation.toValue = toPath.cgPath
animation.duration = 2
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
hole.add(animation, forKey: nil)
CATransaction.begin()
CATransaction.disableActions()
hole.path = toPath.cgPath
CATransaction.commit()
Also tried UIView.animate(... or extracting blurView.layer.sublayers to add them to my contentView, but it just gets messy.
Another approach would be to use a CGAffineTransform, which I tried, but the blur gets disrupted. Plus I have trouble to use a workaround when I feel so close to a solution.
Any help would be greatly appreciated! Thanks!
I want to rotate an UIImageView with CAKeyframeAnimation. The problem is that the shadow rotates with the object, making a non-real effect... The shadow must remain under the image, not rotating around it
This is my code. Very simple:
let imagen:UIImageView = UIImageView(image:UIImage(named: "material6.png"))
self.view.addSubview(imagen)
imagen.center = CGPoint(x: 100, y: 100)
imagen.layer.shadowOpacity = 0.75
imagen.layer.shadowOffset = CGSize(width: 0, height: 15)
//Animación del ángulo
let animacionAngulo = CAKeyframeAnimation(keyPath: "transform.rotation.z")
var valoresAngulo = [NSNumber]()
let degrees:Float = 360
let radians = degrees * Float.pi / Float(180.0)
valoresAngulo.append(NSNumber(value: 0))
valoresAngulo.append(NSNumber(value: radians))
animacionAngulo.values = valoresAngulo
//Permite añadir varias animaciones y gestionarlas a la vez
animacionAngulo.repeatCount = Float.infinity
animacionAngulo.duration = 2.0
imagen.layer.add(animacionAngulo, forKey: nil)
Any solution?
If the shadow is rotating along with the image. You can solve it by adding an UIView with same dimensions of your UIImageView below your UIImageView. If you add shadow to your UIView instead of UIImageView, you can achieve the desired effect in this case.
let imagen:UIImageView = UIImageView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
imagen.center = CGPoint(x: 100, y: 100)
imagen.backgroundColor = UIColor.red
let belowView: UIView = UIView(frame: imagen.frame)
belowView.backgroundColor = UIColor.white
belowView.layer.shadowOpacity = 0.75
belowView.layer.shadowOffset = CGSize(width: 0, height: 15)
self.view.addSubview(belowView)
self.view.addSubview(imagen)
//Animación del ángulo
let animacionAngulo = CAKeyframeAnimation(keyPath: "transform.rotation.z")
var valoresAngulo = [NSNumber]()
let degrees:Float = 360
let radians = degrees * Float.pi / Float(180.0)
valoresAngulo.append(NSNumber(value: 0))
valoresAngulo.append(NSNumber(value: radians))
animacionAngulo.values = valoresAngulo
//Permite añadir varias animaciones y gestionarlas a la vez
animacionAngulo.repeatCount = Float.infinity
animacionAngulo.duration = 2.0
imagen.layer.add(animacionAngulo, forKey: nil)
We were wondering how we can easily build a circular mask, that blends out the background and transitions into a new view, with new buttons? See example here (watch the red planet gets triggered):
//Swift 4
This is a simple static method for circular bubble transition from a point screen.
//as shown on this link
https://github.com/andreamazz/BubbleTransition
import UIKit
class AnimationUtility: UIViewController {
static func animateBubbleTrnsitionView( selfView: UIView, point : CGPoint) {
//let button = CGRect.init(x: 30, y: selfView.frame.size.height - 15, width: 45, height: 45)
let button = CGRect.init(x: point.x, y: point.y, width: 0, height: 0)
let circleMaskPathInitial = UIBezierPath(ovalIn: CGRect.init(x: point.x, y: point.y, width: 1, height: 1))
let extremePoint = CGPoint(x: point.x, y: 15 - selfView.frame.size.height - 200)
let radius = sqrt((extremePoint.x*extremePoint.x) + (extremePoint.y*extremePoint.y))
let circleMaskPathFinal = UIBezierPath(ovalIn: (button.insetBy(dx: -radius, dy: -radius)))
let maskLayer = CAShapeLayer()
maskLayer.path = circleMaskPathFinal.cgPath
selfView.layer.mask = maskLayer
let maskLayerAnimation = CABasicAnimation(keyPath: "path")
maskLayerAnimation.fromValue = circleMaskPathInitial.cgPath
maskLayerAnimation.toValue = circleMaskPathFinal.cgPath
maskLayerAnimation.duration = 0.9
maskLayer.add(maskLayerAnimation, forKey: "path")
}
}
Use this code in following way
Perform segue or push without animation.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
switch segue.identifier! {
case "navToHomeWithoutanimation":
self.navigationController?.view.backgroundColor = APP_ORANGE_COLOR //which ever color you want
let vc = segue.destination as! MapViewController
AnimationUtility.animateBubbleTrnsitionView(selfView: vc.view, point: self.view.center)
break
}
}
//The animation will start to animate from given point.
Swift code :
I writted all code appdelegate file. Used a screenshot of timeline rather than a full-blown UITableView.
First, let’s add the screenshot on the window:
let imageView = UIImageView(frame: self.window!.frame)
imageView.image = UIImage(named: "twitterscreen")
self.window!.addSubview(imageView)
self.mask = CALayer()
self.mask!.contents = UIImage(named: "twitter logo mask").CGImage
self.mask!.bounds = CGRect(x: 0, y: 0, width: 100, height: 100)
self.mask!.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.mask!.position = CGPoint(x: imageView.frame.size.width/2, y: imageView.frame.size.height/2)
imageView.layer.mask = mask
animating the mask. You’ll observe that the bird reduces in size for a bit, and then increases in size, so to do that using just one animation, let’s use CAKeyframeAnimation.
let keyFrameAnimation = CAKeyframeAnimation(keyPath: "bounds")
keyFrameAnimation.duration = 1
keyFrameAnimation.timingFunctions = [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut), CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)]
let initalBounds = NSValue(CGRect: mask!.bounds)
let secondBounds = NSValue(CGRect: CGRect(x: 0, y: 0, width: 90, height: 90))
let finalBounds = NSValue(CGRect: CGRect(x: 0, y: 0, width: 1500, height: 1500))
keyFrameAnimation.values = [initalBounds, secondBounds, finalBounds]
keyFrameAnimation.keyTimes = [0, 0.3, 1]
self.mask!.addAnimation(keyFrameAnimation, forKey: "bounds")
If you see result please visit the github repo. https://github.com/rounak/TwitterBirdAnimation/