-1.8 C
New York
Wednesday, February 5, 2025

ios – SwiftUI liquid animation


I am making an attempt to create a type of liquid animation as seen right here (static picture). A video of the impact will be seen on this youtube video from round 35s mark. Dots spawn on the outermost circle and transfer inwards. As they strategy the innermost circle displaying charging info, the purpose of contact of the dot with the circle type of animates upwards regularly till it makes contact with the transferring dot after which flatlines again to the circumference of the circle. This is my code however the animation shouldn’t be fairly there, the circumference type of abruptly scales up and again down and isn’t fluid.

struct MovingDot: Identifiable {
    let id = UUID()
    var startAngle: Double
    var progress: CGFloat
    var scale: CGFloat = 1.0
}

struct BulgeEffect: Form {
    var targetAngle: Double
    var bulgeHeight: CGFloat
    var bulgeWidth: Double
    
    var animatableData: AnimatablePair {
        get { AnimatablePair(targetAngle, bulgeHeight) }
        set {
            targetAngle = newValue.first
            bulgeHeight = newValue.second
        }
    }
    
    func path(in rect: CGRect) -> Path {
        let radius = rect.width / 2
        var path = Path()
        
        path.transfer(to: CGPoint(x: rect.midX + radius, y: rect.midY))
        stride(from: 0, to: 2 * .pi, by: 0.01).forEach { angle in
            let distanceFromTarget = abs(angle - targetAngle)
            let bulgeEffect = distanceFromTarget < bulgeWidth
                ? bulgeHeight * (cos(distanceFromTarget / bulgeWidth * .pi) + 1) / 2
                : 0
            let x = rect.midX + (radius + bulgeEffect) * cos(angle)
            let y = rect.midY + (radius + bulgeEffect) * sin(angle)
            path.addLine(to: CGPoint(x: x, y: y))
        }
        
        path.closeSubpath()
        return path
    }
}

struct LiquidAnimation: View {
    let outerDiameter: CGFloat
    let innerDiameter: CGFloat
    let dotSize: CGFloat
    
    @State non-public var movingDots: [MovingDot] = []
    @State non-public var bulgeHeight: CGFloat = 0
    @State non-public var targetAngle: Double = 0
    
    var physique: some View {
        ZStack {
            ForEach(movingDots) { dot in
                Circle()
                    .body(width: dotSize * 2, top: dotSize * 2)
                    .scaleEffect(dot.scale)
                    .place(
                        x: outerDiameter/2 + cos(dot.startAngle) * (outerDiameter/2 - dot.progress * (outerDiameter/2 - innerDiameter/2)),
                        y: outerDiameter/2 + sin(dot.startAngle) * (outerDiameter/2 - dot.progress * (outerDiameter/2 - innerDiameter/2))
                    )
            }
            
            BulgeEffect(targetAngle: targetAngle, bulgeHeight: bulgeHeight, bulgeWidth: 0.8)
                .fill()
                .body(width: innerDiameter, top: innerDiameter)
                .animation(.spring(response: 0.3, dampingFraction: 0.7), worth: bulgeHeight)
        }
        .body(width: outerDiameter, top: outerDiameter)
        .onAppear(carry out: startSpawningDots)
    }
    
    non-public func startSpawningDots() {
        Timer.scheduledTimer(withTimeInterval: Double.random(in: 2...5), repeats: true) { _ in
            let startAngle = Double.random(in: 0...(2 * .pi))
            let newDot = MovingDot(startAngle: startAngle, progress: 0)
            
            movingDots.append(newDot)
            
            withAnimation(.easeIn(period: 1.5)) {
                movingDots[movingDots.count - 1].progress = 0.8
            }
            
            // Begin bulge animation when dot is shut
            DispatchQueue.essential.asyncAfter(deadline: .now() + 1.2) {
                targetAngle = startAngle
                withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
                    bulgeHeight = dotSize * 3
                }
                
                // Scale up the dot barely
                withAnimation(.easeOut(period: 0.3)) {
                    movingDots[movingDots.count - 1].scale = 1.2
                }
            }
            
            // Full dot motion and absorption
            DispatchQueue.essential.asyncAfter(deadline: .now() + 1.5) {
                withAnimation(.easeOut(period: 0.3)) {
                    movingDots[movingDots.count - 1].progress = 1
                    movingDots[movingDots.count - 1].scale = 0.1
                }
                
                // Collapse bulge
                withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
                    bulgeHeight = 0
                }
            }
            
            // Take away dot
            DispatchQueue.essential.asyncAfter(deadline: .now() + 2.0) {
                movingDots.removeAll { $0.id == newDot.id }
            }
        }
    }
}

struct ContentView: View {
    var physique: some View {
        ZStack {
            LiquidAnimation(
                outerDiameter: 350,
                innerDiameter: 150,
                dotSize: 4
            )
        }
    }
}

How can I obtain the identical impact as within the video ?

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles