Created
August 18, 2025 01:28
-
-
Save victorBaro/23b8172c87cc3c2d06801014a14b293c to your computer and use it in GitHub Desktop.
Playground to debug a metal shader transition
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
struct SlideAwayPlayground: View { | |
@State private var progress: Float = 0.5 | |
@State private var direction: Float = 1.0 | |
@State private var selectedImage = 0 | |
@State private var showGuides = true | |
@State private var animateProgress = false | |
@State private var animationSpeed: Double = 2.0 | |
let images: [ImageResource] = [.abstractUnsplash, .geoUnsplash, .aiavatar] | |
var body: some View { | |
VStack(spacing: 20) { | |
Text("SLIDE-AWAY SHADER PLAYGROUND") | |
.font(.title2) | |
.fontWeight(.bold) | |
.foregroundColor(.red) | |
.padding() | |
// Main shader view with guides | |
ZStack { | |
// Background grid for reference | |
if showGuides { | |
Rectangle() | |
.fill(Color.gray.opacity(0.1)) | |
.overlay( | |
VStack(spacing: 0) { | |
ForEach(0..<10) { _ in | |
HStack(spacing: 0) { | |
ForEach(0..<10) { _ in | |
Rectangle() | |
.stroke(Color.gray.opacity(0.3), lineWidth: 0.5) | |
.frame(width: 30, height: 30) | |
} | |
} | |
} | |
} | |
) | |
} | |
// The shader effect | |
Image(images[selectedImage]) | |
.resizable() | |
.aspectRatio(contentMode: .fill) | |
.frame(width: 300, height: 300) | |
.clipped() | |
.visualEffect { content, proxy in | |
content.distortionEffect( | |
// Find this metal code in the following post: | |
// https://nerdyak.tech/development/2023/06/16/distortionEffect-with-Metal-shaders-for-better-transitions.html | |
ShaderLibrary.slideAwayRipple( | |
.float2(Float(proxy.size.width), Float(proxy.size.height)), | |
.float(progress), | |
.float(direction) | |
), | |
maxSampleOffset: CGSize(width: 20, height: 40) | |
) | |
} | |
.border(Color.black, width: 2) | |
// Progress line indicator | |
if showGuides { | |
let linePosition = direction > 0 ? | |
(CGFloat(progress) * 300) : | |
(300 - CGFloat(progress) * 300) | |
Rectangle() | |
.fill(Color.red) | |
.frame(width: 2, height: 300) | |
.offset(x: linePosition - 150) | |
Text("PROGRESS LINE") | |
.font(.caption) | |
.fontWeight(.bold) | |
.foregroundColor(.red) | |
.offset(x: linePosition - 150, y: -170) | |
} | |
} | |
.frame(width: 300, height: 300) | |
// Controls | |
VStack(spacing: 15) { | |
// Progress control | |
VStack { | |
HStack { | |
Text("PROGRESS: \(progress, specifier: "%.3f")") | |
.fontWeight(.bold) | |
Spacer() | |
Button(animateProgress ? "STOP" : "ANIMATE") { | |
animateProgress.toggle() | |
} | |
.foregroundColor(animateProgress ? .red : .blue) | |
.fontWeight(.bold) | |
} | |
Slider(value: $progress, in: 0...2) { | |
Text("Progress") | |
} | |
.disabled(animateProgress) | |
HStack { | |
Text("0.0 (all hidden)") | |
.font(.caption) | |
Spacer() | |
Text("1.0 (identity)") | |
.font(.caption) | |
.fontWeight(.bold) | |
Spacer() | |
Text("2.0 (all pushed)") | |
.font(.caption) | |
} | |
} | |
// Direction control | |
VStack { | |
Text("DIRECTION: \(direction > 0 ? "LEFT→RIGHT (+1)" : "RIGHT→LEFT (-1)")") | |
.fontWeight(.bold) | |
HStack { | |
Button("LEFT→RIGHT (+1)") { | |
direction = 1.0 | |
} | |
.foregroundColor(direction > 0 ? .white : .blue) | |
.padding() | |
.background(direction > 0 ? Color.blue : Color.clear) | |
.border(Color.blue) | |
Button("RIGHT→LEFT (-1)") { | |
direction = -1.0 | |
} | |
.foregroundColor(direction < 0 ? .white : .red) | |
.padding() | |
.background(direction < 0 ? Color.red : Color.clear) | |
.border(Color.red) | |
} | |
} | |
// Image selection | |
VStack { | |
Text("IMAGE SELECTION") | |
.fontWeight(.bold) | |
Picker("Image", selection: $selectedImage) { | |
Text("Abstract").tag(0) | |
Text("Geometry").tag(1) | |
Text("AI Avatar").tag(2) | |
} | |
.pickerStyle(SegmentedPickerStyle()) | |
} | |
// Animation speed (when animating) | |
if animateProgress { | |
VStack { | |
Text("ANIMATION SPEED: \(animationSpeed, specifier: "%.1f")s") | |
.fontWeight(.bold) | |
Slider(value: $animationSpeed, in: 0.5...5.0) { | |
Text("Speed") | |
} | |
} | |
} | |
// Guides toggle | |
Button(showGuides ? "HIDE GUIDES" : "SHOW GUIDES") { | |
showGuides.toggle() | |
} | |
.foregroundColor(showGuides ? .red : .green) | |
.fontWeight(.bold) | |
} | |
.padding() | |
} | |
.onReceive(Timer.publish(every: 0.016, on: .main, in: .common).autoconnect()) { _ in | |
if animateProgress { | |
withAnimation(.linear(duration: 0.016)) { | |
progress += Float(0.016 / animationSpeed * 2) // 0 to 2 over animationSpeed seconds | |
if progress > 2.0 { | |
progress = 0.0 | |
} | |
} | |
} | |
} | |
} | |
} | |
#Preview { | |
SlideAwayPlayground() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment