Skip to content

Instantly share code, notes, and snippets.

@beyoung
Forked from pommdau/FormglowView.md
Created September 27, 2024 02:25
Show Gist options
  • Save beyoung/700d219c65b13206fbe57ed70581be9f to your computer and use it in GitHub Desktop.
Save beyoung/700d219c65b13206fbe57ed70581be9f to your computer and use it in GitHub Desktop.

General

image

Refs

Code

  • 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()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment