Skip to content

3D Raindrops update #29

Description

@BigSurDynamic

import SwiftUI

public struct P275_Raining: View {
public init() {}

public var body: some View {
    RealisticRain()
        .background(
            LinearGradient(
                colors: [.black.opacity(0.9), .gray.opacity(0.35)],
                startPoint: .top,
                endPoint: .bottom
            )
        )
}

}

fileprivate struct RealisticRain: View {
@State private var drops: [RainDropModel] = []
@State private var splashes: [SplashModel] = []
@State private var isRunning = false

var body: some View {
    GeometryReader { geo in
        ZStack {
            ForEach(drops) { drop in
                RealisticDrop(drop: drop, screen: geo.size)
            }

            ForEach(splashes) { splash in
                SplashView(splash: splash, screen: geo.size)
            }
        }
        .ignoresSafeArea()
        .onAppear {
            isRunning = true
            spawnRain(in: geo.size)
        }
        .onDisappear {
            isRunning = false
            drops.removeAll()
            splashes.removeAll()
        }
    }
}

private func spawnRain(in size: CGSize) {
    guard isRunning else { return }

    DispatchQueue.main.asyncAfter(deadline: .now() + .random(in: 0.015...0.07)) {
        guard isRunning else { return }

        let depth = Double.random(in: 0.35...1.25)
        let duration = Double.random(in: 0.75...1.85) / depth
        let x = Double.random(in: -0.05...1.05)
        let wind = Double.random(in: -45...45) * depth
        let length = Double.random(in: 28...90) * depth
        let width = Double.random(in: 1.0...2.8) * depth

        let drop = RainDropModel(
            xRatio: x,
            yStart: -0.2,
            yEnd: 1.15,
            wind: wind,
            length: length,
            width: width,
            opacity: min(0.95, 0.25 + depth * 0.55),
            blur: max(0, 1.6 - depth),
            duration: duration,
            depth: depth
        )

        drops.append(drop)

        DispatchQueue.main.asyncAfter(deadline: .now() + duration * 0.92) {
            splashes.append(
                SplashModel(
                    xRatio: x + wind / max(size.width, 1),
                    yRatio: Double.random(in: 0.82...1.03),
                    scale: depth
                )
            )
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + duration + 0.15) {
            drops.removeAll { $0.id == drop.id }
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + duration + 0.55) {
            splashes.removeAll { $0.createdAt < Date().addingTimeInterval(-0.7) }
        }

        spawnRain(in: size)
    }
}

}

fileprivate struct RainDropModel: Identifiable {
let id = UUID()
let xRatio: Double
let yStart: Double
let yEnd: Double
let wind: Double
let length: Double
let width: Double
let opacity: Double
let blur: Double
let duration: Double
let depth: Double
}

fileprivate struct SplashModel: Identifiable {
let id = UUID()
let xRatio: Double
let yRatio: Double
let scale: Double
let createdAt = Date()
}

fileprivate struct RealisticDrop: View {
let drop: RainDropModel
let screen: CGSize

@State private var fall: CGFloat = 0

var body: some View {
    Capsule()
        .fill(
            LinearGradient(
                colors: [
                    .white.opacity(0.9),
                    .cyan.opacity(0.45),
                    .blue.opacity(0.08)
                ],
                startPoint: .top,
                endPoint: .bottom
            )
        )
        .overlay(
            Capsule()
                .stroke(.white.opacity(0.55), lineWidth: drop.width * 0.25)
                .blur(radius: 0.5)
        )
        .frame(width: drop.width, height: drop.length)
        .shadow(color: .white.opacity(0.25), radius: drop.depth * 2)
        .blur(radius: drop.blur)
        .opacity(drop.opacity)
        .rotationEffect(.degrees(-10 + drop.wind * 0.15))
        .position(
            x: screen.width * drop.xRatio + CGFloat(drop.wind) * fall,
            y: screen.height * (drop.yStart + (drop.yEnd - drop.yStart) * Double(fall))
        )
        .onAppear {
            withAnimation(.linear(duration: drop.duration)) {
                fall = 1
            }
        }
}

}

fileprivate struct SplashView: View {
let splash: SplashModel
let screen: CGSize

@State private var scale: CGFloat = 0.2
@State private var opacity: Double = 0.7

var body: some View {
    ZStack {
        Ellipse()
            .stroke(.cyan.opacity(opacity), lineWidth: 1.2)
            .frame(width: 24 * splash.scale, height: 6 * splash.scale)

        ForEach(0..<5, id: \.self) { i in
            Circle()
                .fill(.white.opacity(opacity))
                .frame(width: 2.5 * splash.scale, height: 2.5 * splash.scale)
                .offset(
                    x: CGFloat(i - 2) * 7 * splash.scale,
                    y: CGFloat.random(in: -5...2)
                )
        }
    }
    .scaleEffect(scale)
    .position(
        x: screen.width * splash.xRatio,
        y: screen.height * splash.yRatio
    )
    .onAppear {
        withAnimation(.easeOut(duration: 0.45)) {
            scale = 1.45
            opacity = 0
        }
    }
}

}

struct P275_Raining_Previews: PreviewProvider {
static var previews: some View {
P275_Raining()
}
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions