Код: Выделить всё
struct MovingDot: Identifiable {
let id = UUID()
var startAngle: Double
var progress: CGFloat
var scale: CGFloat = 1.0
}
struct BulgeEffect: Shape {
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()
stride(from: 0, to: 2 * .pi, by: 0.01).forEach { angle in
let normalizedAngle = (angle - targetAngle + .pi * 2).truncatingRemainder(dividingBy: 2 * .pi)
let distanceFromCenter = min(normalizedAngle, 2 * .pi - normalizedAngle)
let bulgeEffect = distanceFromCenter < bulgeWidth
? bulgeHeight * pow(cos(distanceFromCenter / bulgeWidth * .pi / 2), 2)
: 0
let x = rect.midX + (radius + bulgeEffect) * cos(angle)
let y = rect.midY + (radius + bulgeEffect) * sin(angle)
if angle == 0 {
path.move(to: CGPoint(x: x, y: y))
} else {
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 private var movingDots: [MovingDot] = []
@State private var bulgeHeight: CGFloat = 0
@State private var targetAngle: Double = 0
var body: some View {
ZStack {
ForEach(movingDots) { dot in
Circle()
.frame(width: dotSize * 2, height: dotSize * 2)
.scaleEffect(dot.scale)
.position(
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.6)
.fill()
.frame(width: innerDiameter, height: innerDiameter)
.animation(.spring(response: 0.3, dampingFraction: 0.6), value: bulgeHeight)
}
.frame(width: outerDiameter, height: outerDiameter)
.onAppear(perform: startSpawningDots)
}
private 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(duration: 1.5)) {
movingDots[movingDots.count - 1].progress = 0.8
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
targetAngle = startAngle
withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
bulgeHeight = dotSize * 8
}
withAnimation(.easeOut(duration: 0.3)) {
movingDots[movingDots.count - 1].scale = 1.2
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
withAnimation(.easeOut(duration: 0.3)) {
movingDots[movingDots.count - 1].progress = 1
movingDots[movingDots.count - 1].scale = 0.1
}
withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
bulgeHeight = 0
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
movingDots.removeAll { $0.id == newDot.id }
}
}
}
}
struct ContentView: View {
var body: some View {
ZStack {
LiquidAnimation(
outerDiameter: 350,
innerDiameter: 150,
dotSize: 4
)
}
}
}
Подробнее здесь: https://stackoverflow.com/questions/794 ... -animation