- Kavsoftのメタボールのコードをベースに、別途シャドウ用のCanvasを追加することで表現してみた
- ボールの数*2つのTimerを使うのでパフォーマンスは怪しいか…?
- animationのmodifierで置き換えるとか改善はできそう
import SwiftUI
struct FormglowView: View {
// MARK: - Properties
static let ballShadowColors: [Color] = [.blue, .green, .indigo, .mint, .orange, .pink, .purple, .red, .yellow]
static let ballDiameter: CGFloat = 200
static let numberOfBalls = 5
static let animationDuration: CGFloat = 1.0
@State private var ballsOffset: [CGPoint] = Array(repeating: .zero, count: Self.numberOfBalls)
@State private var ballsShadowColor: [Color] = Array(repeating: .clear, count: Self.numberOfBalls)
@State private var ballAnimationTimers: [Timer] = []
@State private var ballShadowAnimationTimers: [Timer] = []
// MARK: - Views
var body: some View {
metaBalls()
.background {
metaBallsShadow()
}
}
@ViewBuilder
private func metaBalls() -> some View {
Canvas { context, size in
// メタボール表現
context.addFilter(.alphaThreshold(min: 0.5, color: .black))
context.addFilter(.blur(radius: 30))
context.drawLayer { ctx in
for index in 0...Self.numberOfBalls {
guard let resolvedView = context.resolveSymbol(id: index) else {
return
}
let center = CGPoint(x: size.width / 2, y: size.height / 2)
ctx.draw(resolvedView, at: center)
}
}
} symbols: {
ForEach(0..<Self.numberOfBalls, id: \.self) { ballIndex in
Circle()
.aspectRatio(contentMode: .fit)
.frame(width: Self.ballDiameter)
.offset(x: ballsOffset[ballIndex].x, y: ballsOffset[ballIndex].y)
.animation(.easeInOut(duration: Self.animationDuration), value: ballsOffset[ballIndex])
.onAppear {
let timer = Timer.scheduledTimer(withTimeInterval: Self.animationDuration, repeats: true) { _ in
let offsetValueMax = Self.ballDiameter / 4.0
self.ballsOffset[ballIndex] = .init(
x: CGFloat.random(in: -offsetValueMax...offsetValueMax),
y: CGFloat.random(in: -offsetValueMax...offsetValueMax)
)
}
ballAnimationTimers.append(timer)
}
.tag(ballIndex)
}
}
}
@ViewBuilder
private func metaBallsShadow() -> some View {
Canvas { context, size in
context.drawLayer { ctx in
for index in 0...Self.numberOfBalls {
guard let resolvedView = context.resolveSymbol(id: index) else {
return
}
let center = CGPoint(x: size.width / 2, y: size.height / 2)
ctx.draw(resolvedView, at: center)
}
}
} symbols: {
ForEach(0..<Self.numberOfBalls, id: \.self) { ballIndex in
Circle()
.aspectRatio(contentMode: .fit)
.frame(width: Self.ballDiameter + 20)
.foregroundStyle(ballsShadowColor[ballIndex])
.blur(radius: 20)
.shadow(color: ballsShadowColor[ballIndex].opacity(0.8), radius: 100, x: 0.0, y: 0.0)
.blendMode(.hardLight)
.offset(x: ballsOffset[ballIndex].x, y: ballsOffset[ballIndex].y)
.animation(.easeInOut(duration: Self.animationDuration), value: ballsOffset[ballIndex])
.onAppear {
let timer = Timer.scheduledTimer(withTimeInterval: Self.animationDuration, repeats: true) { _ in
self.ballsShadowColor[ballIndex] = Self.ballShadowColors.randomElement()!
}
ballShadowAnimationTimers.append(timer)
}
.tag(ballIndex)
}
}
}
}
#Preview {
FormglowView()
}