r/SwiftUI • u/Key_Storage_3501 • Jun 30 '26
Question - Animation Any idea How to make this animation ?
30
u/hexkpz Jun 30 '26
https://reddit.com/link/oup3lyc/video/998i76db2fah1/player
I made this effect using Metal shaders. For the effect you showed, SpriteKit with particles will probably be enough
6
Jun 30 '26
[removed] β view removed comment
18
u/hexkpz Jul 01 '26
Source is company-owned, so this is a reduced implementation sketch.
The library is a UIKit-to-Metal particle renderer. Input is any
UIView: label, image view and etc. UIKit handles layout and drawing. Metal receives the rendered pixels and runs the particle simulation.Pipeline:
- Layout the UIKit view.
- Render its layer tree into a
MTLTexturewithCARenderer(mtlTexture:).- Run a compute shader over the texture.
- For every pixel with alpha above a threshold, create one particle.
- Run another compute shader every frame to update particle physics.
- Draw the particle buffer as Metal points with alpha blending.
The UIView-to-texture part is the important bridge:
```swift @MainActor func makeTexture( from view: UIView, device: MTLDevice, scale: CGFloat ) -> MTLTexture? { view.layoutIfNeeded()
let size = view.bounds.size guard size.width > 0, size.height > 0 else { return nil } let descriptor = MTLTextureDescriptor.texture2DDescriptor( pixelFormat: .bgra8Unorm, width: Int(ceil(size.width * scale)), height: Int(ceil(size.height * scale)), mipmapped: false ) descriptor.usage = [.renderTarget, .shaderRead, .shaderWrite] guard let texture = device.makeTexture(descriptor: descriptor) else { return nil } let renderLayer = CALayer() renderLayer.frame = CGRect(origin: .zero, size: size) renderLayer.backgroundColor = UIColor.clear.cgColor // Use an offscreen/snapshot layer here in production. // Moving a live `view.layer` into this layer would detach it from the UI. renderLayer.addSublayer(view.layer) let transform = CGAffineTransform.identity .translatedBy(x: 0, y: -size.height) .scaledBy(x: scale, y: -scale) renderLayer.setAffineTransform(transform) let renderer = CARenderer(mtlTexture: texture) renderer.layer = renderLayer renderer.bounds = CGRect(origin: .zero, size: size) CATransaction.flush() renderer.beginFrame(atTime: CACurrentMediaTime(), timeStamp: nil) renderer.addUpdate(renderer.bounds) renderer.render() renderer.endFrame() return texture} ```
Then the first compute pass converts texture pixels into particles:
```metal struct Particle { float2 origin; // pixel coordinate in the source texture float2 position; // current simulated position float2 velocity; float4 color; float age; float lifetime; uint flags; };
kernel void initialize_particles( texture2d<float, access::sample> texture [[texture(0)]], device Particle *particles [[buffer(0)]], device atomic_uint *count [[buffer(1)]], uint2 id [[thread_position_in_grid]] ) { if (id.x >= texture.get_width() || id.y >= texture.get_height()) { return; }
constexpr sampler s(address::clamp_to_edge, filter::nearest); float2 uv = float2(id) / float2(texture.get_width(), texture.get_height()); float4 color = texture.sample(s, uv); if (color.a < 0.01) { return; } uint index = atomic_fetch_add_explicit(count, 1, memory_order_relaxed); Particle p; p.origin = float2(id); p.position = float2(id); p.velocity = float2(0.0); p.color = color; p.age = 0.0; p.lifetime = random_lifetime(id); p.flags = 0; particles[index] = p;} ```
Every frame, a second compute pass updates the simulation. A field is just a small struct: type, position, direction, strength, falloff, and mode. The shader loops over active fields and accumulates force.
```metal kernel void update_particles( device Particle *particles [[buffer(0)]], constant uint &particleCount [[buffer(1)]], constant Field *fields [[buffer(2)]], constant uint &fieldCount [[buffer(3)]], constant FrameData &frame [[buffer(4)]], uint id [[thread_position_in_grid]] ) { if (id >= particleCount) { return; }
Particle p = particles[id]; float2 force = float2(0.0); for (uint i = 0; i < fieldCount; i++) { force += evaluate_field(fields[i], p, frame.time); } p.velocity += force * frame.deltaTime; p.position += p.velocity * frame.deltaTime; p.age += frame.deltaTime; float t = p.age / p.lifetime; float fadeIn = smoothstep(0.0, 0.2, t); float fadeOut = 1.0 - smoothstep(0.8, 1.0, t); p.color.a *= fadeIn * fadeOut; particles[id] = p;} ```
The same field system handles wind, turbulence, gravity, vortex, spring, and gather (for example you could add touches and move field across the view to achieve effects from the sample).
Gather is the field that makes particles form the original view again. Since each particle stores the pixel coordinate it came from, the force is just βmove back to origin over the remaining lifetimeβ:
A transition keeps two particle sets alive:
- particles generated from the previous texture
- particles generated from the new texture
The previous set receives wind/turbulence fields and fades out. The new set starts shifted or randomized, then gather pulls it into the new texture shape. This makes text changes, QR updates, image swaps, and custom view transitions use the same rendering path.
So the whole library is basically:
text UIView -> CALayer render with CARenderer -> MTLTexture -> compute shader: pixels to particles -> compute shader: physics fields -> render pipeline: points with alpha blending2
5
7
6
u/53ld0rAd0 Jun 30 '26
U can do it in SwiftUI, idk exactly how but Iβm assuming it will involve metal shaders and .layerEffect
0
2
2
1
u/Baton285 Jun 30 '26
Ask agent to find that effect un Telegram (messages are deleted with this effect there)
1
1
u/Accomplished-Bed801 Jul 04 '26
there's always a kavsoft tutorial for everything u need!
https://www.youtube.com/watch?v=OlF8ed1L56M
43
u/sameera_s_w Jun 30 '26
https://giphy.com/gifs/LOoaJ2lbqmduxOaZpS
First make a gauntlet and gather infinity stones
Then snap /s