- Kavsoftのメタボールのコードをベースに、別途シャドウ用のCanvasを追加することで表現してみた
- ボールの数*2つのTimerを使うのでパフォーマンスは怪しいか…?
- animationのmodifierで置き換えるとか改善はできそう
//
// FormglowView.swift
// MetaBallDemos
//
// Created by HIROKI IKEUCHI on 2024/09/28.
//
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 ballOffsets: [CGPoint] = Array(repeating: .zero, count: Self.numberOfBalls)
@State private var ballShadowColors: [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: ballOffsets[ballIndex].x, y: ballOffsets[ballIndex].y)
.animation(.easeInOut(duration: Self.animationDuration), value: ballOffsets[ballIndex])
.onAppear {
let timer = Timer.scheduledTimer(withTimeInterval: Self.animationDuration, repeats: true) { _ in
let offsetValueMax = Self.ballDiameter / 4.0
self.ballOffsets[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(ballShadowColors[ballIndex])
.blur(radius: 20)
.shadow(color: ballShadowColors[ballIndex].opacity(0.8), radius: 100, x: 0.0, y: 0.0)
.blendMode(.hardLight)
.offset(x: ballOffsets[ballIndex].x, y: ballOffsets[ballIndex].y)
.animation(.easeInOut(duration: Self.animationDuration), value: ballOffsets[ballIndex])
.animation(.easeInOut(duration: Self.animationDuration), value: ballShadowColors[ballIndex])
.onAppear {
let timer = Timer.scheduledTimer(withTimeInterval: Self.animationDuration, repeats: true) { _ in
self.ballShadowColors[ballIndex] = Self.ballShadowColors.randomElement()!
}
ballShadowAnimationTimers.append(timer)
}
.tag(ballIndex)
}
}
}
}
#Preview {
FormglowView()
}