Last active
October 24, 2024 07:11
-
-
Save Coder-ACJHP/9db9e8ab598047c72cdb7a06166bd4e9 to your computer and use it in GitHub Desktop.
Applying Ripple Transition effect for 2 images using CIFilter and DisplayLink for transition timing, transition starts from user touch location (tested on iOS 12, UIKit)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ImageRippleTransitionViewController: UIViewController, UIViewControllerTransitioningDelegate { | |
public let backgroundImageView: UIImageView = { | |
let imageView = UIImageView() | |
imageView.contentMode = .scaleAspectFill | |
imageView.isUserInteractionEnabled = true | |
imageView.clipsToBounds = true | |
imageView.translatesAutoresizingMaskIntoConstraints = false | |
return imageView | |
}() | |
private var duration: TimeInterval = 1.0 // Customize transition duration | |
private var currentTime: CGFloat = .zero | |
private var displayLink: CADisplayLink? | |
private var rippleFilter: CIFilter? | |
private var ciContext: CIContext? | |
private var isAnimationStarted = false | |
private var isImageNeedsToSwitch = false | |
private var transitionImages: Array<ImageResource> = [ | |
.background, .backgroundImage2 | |
] | |
private var resizedTransitionImages: Array<UIImage?> = [] | |
init() { | |
super.init(nibName: nil, bundle: nil) | |
if let mtlDevice = MTLCreateSystemDefaultDevice() { | |
ciContext = CIContext(mtlDevice: mtlDevice) | |
} | |
let targetSize = UIScreen.main.bounds.size | |
for image in transitionImages { | |
let fullSizeImage = UIImage(resource: image) | |
let resizedImage = resizeImage(fullSizeImage, to: targetSize) | |
resizedTransitionImages.append(resizedImage) | |
} | |
} | |
required init?(coder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
view.backgroundColor = .black | |
configureBackgroundView() | |
addTapGestureRecognizer() | |
} | |
private func configureBackgroundView() { | |
view.addSubview(backgroundImageView) | |
NSLayoutConstraint.activate([ | |
backgroundImageView.topAnchor.constraint(equalTo: view.topAnchor), | |
backgroundImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor), | |
backgroundImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor), | |
backgroundImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor) | |
]) | |
let image: ImageResource = isImageNeedsToSwitch ? transitionImages.first! : transitionImages.last! | |
backgroundImageView.image = UIImage(resource: image) | |
} | |
private func addTapGestureRecognizer() { | |
let action = #selector(handleTapAction(_:)) | |
let tapGesture = UITapGestureRecognizer(target: self, action: action) | |
view.addGestureRecognizer(tapGesture) | |
} | |
@objc | |
private func handleTapAction(_ gesture: UITapGestureRecognizer) { | |
guard isAnimationStarted == false else { return } | |
// Reset animation | |
currentTime = .zero | |
let location = gesture.location(in: view) | |
applyRippleEffect(centered: location) | |
} | |
private func applyRippleEffect(centered atLocation: CGPoint) { | |
// Create CIImages from UIImages | |
guard let inputImage1 = isImageNeedsToSwitch ? resizedTransitionImages.first! : resizedTransitionImages.last!, | |
let fromCIImage = CIImage(image: inputImage1), | |
let inputImage2 = isImageNeedsToSwitch ? resizedTransitionImages.last! : resizedTransitionImages.first!, | |
let toCIImage = CIImage(image: inputImage2) | |
else { return print("Cannot create ciImages") } | |
// Setup ripple filter | |
// Switch the flag | |
isImageNeedsToSwitch.toggle() | |
// Calculate the correct center for Core Image coordinates (bottom-left origin) | |
let ciCenter = CIVector(x: atLocation.x, y: toCIImage.extent.height - atLocation.y) | |
// Define the extent to cover the entire view | |
let ciExtent = CIVector(x: 0, y: 0, z: view.bounds.width, w: view.bounds.height) | |
// Setup the ripple filter | |
rippleFilter = createRippleTransitionFilter( | |
inputImage: fromCIImage, | |
inputTargetImage: toCIImage, | |
inputShadingImage: CIImage(), | |
inputCenter: ciCenter, // Center from the middle of the view | |
inputExtent: ciExtent, // Bounds of the entire view | |
inputWidth: NSNumber(value: 100), | |
inputScale: 50.0 | |
) | |
displayLink = CADisplayLink(target: self, selector: #selector(updateTransition)) | |
displayLink?.add(to: .main, forMode: .common) | |
isAnimationStarted = true | |
} | |
func createRippleTransitionFilter( | |
inputImage: CIImage, | |
inputTargetImage: CIImage, | |
inputShadingImage: CIImage, | |
inputCenter: CIVector, | |
inputExtent: CIVector, | |
inputTime: NSNumber = 0, | |
inputWidth: NSNumber = 100, | |
inputScale: NSNumber = 50 | |
) -> CIFilter? { | |
guard let filter = CIFilter(name: "CIRippleTransition") else { | |
return nil | |
} | |
filter.setDefaults() | |
filter.setValue(inputImage, forKey: kCIInputImageKey) | |
filter.setValue(inputTargetImage, forKey: kCIInputTargetImageKey) | |
filter.setValue(inputShadingImage, forKey: kCIInputShadingImageKey) | |
filter.setValue(inputCenter, forKey: kCIInputCenterKey) | |
filter.setValue(inputExtent, forKey: kCIInputExtentKey) | |
filter.setValue(inputTime, forKey: kCIInputTimeKey) | |
filter.setValue(inputWidth, forKey: kCIInputWidthKey) | |
filter.setValue(inputScale, forKey: kCIInputScaleKey) | |
return filter | |
} | |
func resizeImage(_ image: UIImage, to size: CGSize) -> UIImage? { | |
UIGraphicsBeginImageContextWithOptions(size, false, image.scale) | |
image.draw(in: CGRect(origin: .zero, size: size)) | |
let resizedImage = UIGraphicsGetImageFromCurrentImageContext() | |
UIGraphicsEndImageContext() | |
return resizedImage | |
} | |
@objc | |
func updateTransition() { | |
guard let displayLink, let ciContext, let rippleFilter else { | |
return print("Missing required objects like displayLink or ciContext etc.") | |
} | |
currentTime += CGFloat(displayLink.duration) / CGFloat(duration) | |
if currentTime >= duration { | |
displayLink.invalidate() | |
displayLink.remove(from: .main, forMode: .common) | |
isAnimationStarted = false | |
} else { | |
rippleFilter.setValue(currentTime, forKey: kCIInputTimeKey) | |
if let outputImage = rippleFilter.outputImage, | |
let cgImage = ciContext.createCGImage(outputImage, from: outputImage.extent) { | |
let resultImage = UIImage(cgImage: cgImage) | |
backgroundImageView.image = resultImage | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
RPReplay_Final1729713781.mov
Short video clip