How to Fill Colour with Animation in UIImage particular portion?

121 Views Asked by At

I have flood fill Image functionality in my app and it's working fine, but now I want to fill Colour on specific portion of Image with Animation.

I have used below library ho achive flood fill.

Github link

Here I am sharing reference video link so anyone can get idea that which and where animation I want.

Reference Video link:- link

1

There are 1 best solutions below

8
DonMag On

You could edit that code and update the image every n iterations... but, that might make it too slow, and might not give you the desired effect anyway.

Another option, which may or may not suit your needs, would be to animate a mask to "reveal" the newly-color-filled image.

Could look something like this:

enter image description here

If the oval animation doesn't suffice, you could play around with a shape mask such as this:

enter image description here

animating its scale from tiny to large enough to cover the view.


Edit

Here's a quick example of animating a shape mask (put an image named "sample" in your assets):

class ViewController: UIViewController {
    
    let imgView = UIImageView()

    // the image view will be completely clear to begin with
    //  so we need a view to show where to tap
    let tapView = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemYellow
        
        guard let img = UIImage(named: "sample") else {
            fatalError("Could not load image!")
        }
        
        let g = view.safeAreaLayoutGuide
        
        tapView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tapView)
        imgView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(imgView)
        
        NSLayoutConstraint.activate([
            
            // center the image view
            imgView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            imgView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            // let's give it a width of 240
            imgView.widthAnchor.constraint(equalToConstant: 240.0),
            // use original image aspect ratio
            imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor, multiplier: img.size.height / img.size.width),
            
            // "tap view" same frame as the image view
            tapView.topAnchor.constraint(equalTo: imgView.topAnchor),
            tapView.leadingAnchor.constraint(equalTo: imgView.leadingAnchor),
            tapView.trailingAnchor.constraint(equalTo: imgView.trailingAnchor),
            tapView.bottomAnchor.constraint(equalTo: imgView.bottomAnchor),

        ])

        // so we can see the tap view frame
        tapView.backgroundColor = .systemBlue
        
        imgView.image = img
        
        // Image View starts with empty mask (so it is completely clear)
        imgView.layer.mask = CALayer()

        // tap gesture recognizer
        let tg = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
        tapView.addGestureRecognizer(tg)
        
    }
    
    @objc func gotTap(_ gr: UITapGestureRecognizer) {
        // run the animation, using the tap point as the oval center
        animMask(imgView, pt: gr.location(in: tapView))
    }
    
    func animMask(_ v: UIView, pt: CGPoint) {

        // final oval mask has to cover the entire view
        let largeOvalRadius: CGFloat = sqrt((v.bounds.width * v.bounds.width) + (v.bounds.height * v.bounds.height))
        
        // small oval rect is 4x1 points, centered on the tapped point
        let smallOvalRect: CGRect = .init(x: pt.x - 2.0, y: pt.y, width: 4.0, height: 1.0)
        
        // large oval rect is small oval rect inset by the calculated radius
        let largeOvalRect: CGRect = smallOvalRect.insetBy(dx: -largeOvalRadius, dy: -largeOvalRadius)
        
        // shape layer to use as the layer mask
        let shape = CAShapeLayer()
        // can be any solid color
        shape.fillColor = UIColor.black.cgColor
        
        // set initial path to oval that covers entire view
        let pth = UIBezierPath(ovalIn: largeOvalRect).cgPath
        shape.path = pth
        shape.frame = v.bounds
        v.layer.mask = shape
        
        // animate the path on the mask layer
        let morphAnimation = CABasicAnimation(keyPath: "path")
        // from very small oval
        morphAnimation.fromValue = UIBezierPath(ovalIn: smallOvalRect).cgPath
        // to very large oval
        morphAnimation.toValue = UIBezierPath(ovalIn: largeOvalRect).cgPath
        // 3/4 second animation (for example)
        morphAnimation.duration = 0.75
        // we don't want any easing
        morphAnimation.timingFunction = CAMediaTimingFunction(name: .linear)
        
        // start the animation
        shape.add(morphAnimation, forKey: nil)
        
    }
    
}

enter image description here

If you want to use that with your "flood fill" ...

First, I would edit that flood fill code (or find another example).

Instead of encapsulating it in a subclassed UIImageView, have it:

  • accept an image, color and point
  • return the new image

Then, overlay one image view on top of another, where the "top" image view begins with an empty mask (so it's completely clear). On tap:

  • get a flood-filled image based on the "bottom" image
  • set that as the .image of the "top" image view
  • run a "reveal" mask animation on the "top" image view
  • replace the .image of the "bottom" image view with the new image
  • reset the empty mask on the "top" image view

Lather, rinse, repeat.

You could also optimize the animation by having the flood-fill code also return the changed bounds, and then make the "end" mask only as large as needed to cover the changed area.

Example project here: https://github.com/DonMag/FloodAnim