├── SlowRipple.swift └── metal.metal /SlowRipple.swift: -------------------------------------------------------------------------------- 1 | 2 | import SwiftUI 3 | 4 | struct SlowRipple: View { 5 | @State private var time : CGFloat = 0.1 6 | @State private var noise : CGFloat = 4 7 | @State private var strength : CGFloat = 1 8 | @State private var dragp : CGPoint = .zero 9 | 10 | private let timer = Timer.publish(every: 1/120, 11 | on: .main, 12 | in: .common).autoconnect() 13 | @State private var angle : CGFloat = 0 14 | 15 | 16 | var body: some View { 17 | ZStack{ 18 | ZStack{ 19 | LinearGradient(colors: [.brown.mix(with: .black, by: 0.8), .orange, .white], startPoint: .top, endPoint: .bottomTrailing) 20 | .frame(width:360,height:300) 21 | .blur(radius: 20) 22 | .layerEffect(ShaderLibrary.fbp(.boundingRect,.float2(dragp),.float(time), .float(noise), .float(strength)), maxSampleOffset: CGSize(width:200,height: 200)) 23 | 24 | Color.black.opacity(0.3) 25 | VStack{ 26 | Spacer() 27 | HStack{ 28 | Text("time : \(time, specifier: "%.2f") /") 29 | Text("noise : \(noise, specifier: "%.2f") /") 30 | Text("strength : \(strength, specifier: "%.2f")") 31 | Spacer() 32 | } 33 | 34 | } 35 | .frame(width:330, height: 260) 36 | .foregroundStyle(.white) 37 | .font(.system(size: 10, design: .monospaced)) 38 | 39 | } 40 | .frame(width:360,height:300) 41 | .cornerRadius(12) 42 | .shadow(radius: 8) 43 | 44 | VStack{ 45 | Spacer() 46 | Slider(value: $time, in:0...1) 47 | Slider(value: $noise, in:0...8) 48 | Slider(value: $strength, in:0...10) 49 | 50 | } 51 | .tint(.primary) 52 | .padding() 53 | .onReceive(timer) { _ in 54 | angle += 0.001 55 | 56 | dragp = CGPoint( 57 | x: 5 + cos(angle) * 300, 58 | y: 5 + sin(angle) * 300 59 | ) 60 | 61 | } 62 | 63 | 64 | } 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /metal.metal: -------------------------------------------------------------------------------- 1 | // 2 | // Metal.metal 3 | // SwiftMay21 4 | // 5 | // Created by Minsang Choi on 5/21/25. 6 | // 7 | 8 | #include 9 | #include 10 | using namespace metal; 11 | 12 | float random(float2 st) { 13 | return fract(sin(dot(st.xy, float2(12.9898, 78.233))) * 43758.5453123); 14 | } 15 | 16 | 17 | float value_noise(float2 st) { 18 | float2 i = floor(st); 19 | float2 f = fract(st); 20 | 21 | float a = random(i); 22 | float b = random(i + float2(1.0, 0.0)); 23 | float c = random(i + float2(0.0, 1.0)); 24 | float d = random(i + float2(1.0, 1.0)); 25 | 26 | 27 | float2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); 28 | 29 | 30 | return mix(mix(a, b, u.x), 31 | mix(c, d, u.x), u.y); 32 | } 33 | 34 | 35 | [[ stitchable ]] half4 fbp(float2 pos, SwiftUI::Layer l, float4 boundingBox, float2 dragp, float time, float noise, float strength) { 36 | 37 | float2 size = float2(boundingBox[2],boundingBox[3]); 38 | float2 uv = pos / size; 39 | float2 c = dragp / size; 40 | 41 | 42 | float noiseScale = noise; 43 | float rippleFrequency = 16.0; 44 | float rippleSpeed = 3.0; 45 | float noisePerturbation = strength; 46 | float displacementStrength = 0.3; 47 | 48 | float baseNoise = value_noise(uv * noiseScale); 49 | 50 | float2 rippleCenter = c; // Moving center 51 | 52 | float dist = distance(uv, rippleCenter); 53 | float rippleWave = sin( 54 | dist * rippleFrequency // Wavefronts based on distance 55 | - time * rippleSpeed // Animation over time 56 | + baseNoise * noisePerturbation // Noise perturbs the phase 57 | ); 58 | float2 direction = normalize(uv - rippleCenter + 1e-5); 59 | float2 displacement = direction * rippleWave * displacementStrength; 60 | float2 displacedUv = uv + displacement; 61 | 62 | float finalPattern = value_noise(displacedUv * noiseScale * 1.2 + float2(time * 0.1, 0.0)); // Slightly different scale/animation for the sampled pattern 63 | float shading = smoothstep(0.0, 0.8, rippleWave) * 0.5 + 0.5; 64 | 65 | 66 | float2 newpos = uv; 67 | newpos *= finalPattern * shading; 68 | half4 color = l.sample(newpos * size); 69 | 70 | return color; 71 | } 72 | --------------------------------------------------------------------------------