Skip to content

Instantly share code, notes, and snippets.

@dkun7944
Last active January 14, 2024 23:45
Show Gist options
  • Star 61 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save dkun7944/e1e7e6bfed0362bc510c1766d5dc1428 to your computer and use it in GitHub Desktop.
Save dkun7944/e1e7e6bfed0362bc510c1766d5dc1428 to your computer and use it in GitHub Desktop.
SwiftUI + Swift.Shader CD
//
// cd.metal
// CD
//
// Created by Daniel Kuntz on 7/3/23.
//
#include <metal_stdlib>
using namespace metal;
float3 Hue(float H) {
half R = abs(H * 6 - 3) - 1;
half G = 2 - abs(H * 6 - 2);
half B = 2 - abs(H * 6 - 4);
return saturate(float3(R,G,B));
}
float4 HSVtoRGB(float3 HSV) {
return float4(((Hue(HSV.x) - 1) * HSV.y + 1) * HSV.z,1);
}
float2x2 Rot(float a) {
float s=sin(a), c=cos(a);
return float2x2(c, -s, s, c);
}
float3 streak(float2 uv, float rot, float3 col, float w) {
float2 rotUV = uv * Rot(rot);
float d = length(uv);
float mask = 0.0;
if (rotUV.y >= -1/(w/3.) && rotUV.y <= 1/(w/3.)) {
mask = 1.0;
}
float streak = cos(abs(rotUV.y*w)) * mask * d;
return float3(col.x, col.y, clamp(streak, 0., 0.3));
return clamp(col * streak, 0, 1);
}
[[ stitchable ]] half4 cd(float2 position, half4 color, float2 viewSize, float accel) {
float2 uv = position / viewSize;
uv -= .5;
uv.x *= viewSize.x / viewSize.y;
float4 col = float4(float3(0.4), 1.0);
for (float i = 0; i < 25; i++) {
float rot = (9*i+sin(accel*i*3));
float w = clamp((sin(i+1)+1) * 100.29, 10., 1000.);
float s = 0.7-(w/1000.0);
float3 streakCol = clamp(float3(sin(i*8.11)+1.0, s, 0.5), 0, 1);
float o = clamp(1 - (w / 180.0), 0., 1.) * 0.9;
float3 streakHSV = streak(uv, rot, streakCol, w);
col += HSVtoRGB(streakHSV) * o;
}
return half4(col);
}
//
// CDView.swift
// CD
//
// Created by Daniel Kuntz on 7/3/23.
//
import SwiftUI
struct ShapeWithHole: Shape {
let cutout: CGSize
func path(in rect: CGRect) -> Path {
var path = Rectangle().path(in: rect)
let hole = Circle().path(in: CGRect(origin: CGPoint(x: rect.midX - cutout.width / 2, y: rect.midY - cutout.height / 2), size: cutout)).reversed
path.addPath(hole)
return path
}
}
extension Path {
var reversed: Path {
let reversedCGPath = UIBezierPath(cgPath: cgPath)
.reversing()
.cgPath
return Path(reversedCGPath)
}
}
struct CircularTextView: View {
var title: String
var radius: Double
@State private var letterWidths: [Int:Double] = [:]
var lettersOffset: [(offset: Int, element: Character)] {
return Array(title.enumerated())
}
var body: some View {
ZStack {
ForEach(lettersOffset, id: \.offset) { index, letter in // Mark 1
VStack {
Text(String(letter))
.font(.system(size: 5, weight: .bold, design: .monospaced))
.foregroundColor(.yellow)
.kerning(5)
.background(LetterWidthSize()) // Mark 2
.onPreferenceChange(WidthLetterPreferenceKey.self, perform: { width in // Mark 2
letterWidths[index] = width
})
Spacer() // Mark 1
}
.rotationEffect(fetchAngle(at: index)) // Mark 3
}
}
.frame(width: 107, height: 107)
.rotationEffect(.degrees(280))
}
func fetchAngle(at letterPosition: Int) -> Angle {
let times2pi: (Double) -> Double = { $0 * 2 * .pi }
let circumference = times2pi(radius)
let finalAngle = times2pi(letterWidths.filter{$0.key <= letterPosition}.map(\.value).reduce(0, +) / circumference)
return .radians(finalAngle)
}
}
struct WidthLetterPreferenceKey: PreferenceKey {
static var defaultValue: Double = 0
static func reduce(value: inout Double, nextValue: () -> Double) {
value = nextValue()
}
}
struct LetterWidthSize: View {
var body: some View {
GeometryReader { geometry in
Color
.clear
.preference(key: WidthLetterPreferenceKey.self,
value: geometry.size.width)
}
}
}
struct CDView: View {
let size: CGFloat = 350.0
@StateObject private var manager = MotionManager()
@State private var p: CGFloat = 0.42
@State private var timer: Timer?
private let shaderFunction = ShaderFunction(library: .default, name: "cd")
var body: some View {
ZStack {
Color(white: 0.0)
Circle()
.frame(width: size - 50.0, height: size - 50.0)
.foregroundColor(.white)
.clipShape(ShapeWithHole(cutout: CGSize(width: 200.0, height: 200.0)))
.blur(radius: 50.0)
.opacity(0.5)
.offset(x: manager.roll * -80.0, y: manager.pitch * 80.0)
ZStack {
Group {
Circle()
.foregroundColor(.white.opacity(0.3))
.frame(width: size+12, height: size+12)
.clipShape(ShapeWithHole(cutout: CGSize(width: 150.0, height: 150.0)))
.shadow(color: .black, radius: 5)
Circle()
.foregroundColor(Color.black.opacity(0.9))
.frame(width: size+4, height: size+4)
.clipShape(ShapeWithHole(cutout: CGSize(width: 110.0, height: 110.0)))
Circle()
.foregroundColor(.white.opacity(0.15))
.frame(width: size+12, height: size+12)
.clipShape(ShapeWithHole(cutout: CGSize(width: 40.0, height: 40.0)))
Circle()
.stroke(Color.black.opacity(0.9), lineWidth: 1.0)
.frame(width: size+12, height: size+12)
Circle()
.stroke(Color.black, lineWidth: 1.0)
.frame(width: 40.0, height: 40.0)
Circle()
.stroke(Color.black.opacity(0.2), lineWidth: 2.0)
.frame(width: 80.0, height: 80.0)
Circle()
.stroke(Color.black, lineWidth: 16.0)
.frame(width: 110.0, height: 110.0)
}
Circle()
.foregroundColor(.white)
.frame(width: size+12, height: size+12)
.colorEffect(
Shader(function: shaderFunction,
arguments: [
.float2(Float(size),
Float(size)),
.float((manager.pitch * 0.02) + 0.02)
])
)
.blur(radius: 10.0)
.contrast(3.4)
.opacity(0.4)
.mask(Circle())
.clipShape(ShapeWithHole(cutout: CGSize(width: 110.0, height: 110.0)))
Circle()
.foregroundColor(.white)
.frame(width: size+2, height: size+2)
.colorEffect(
Shader(function: shaderFunction,
arguments: [
.float2(Float(size),
Float(size)),
.float((manager.pitch * 0.02) + 0.01)
])
)
.blur(radius: 10.0)
.contrast(3.4)
.brightness(0.2)
.mask(Circle())
.clipShape(ShapeWithHole(cutout: CGSize(width: 110.0, height: 110.0)))
Circle()
.foregroundColor(.white)
.frame(width: size, height: size)
.colorEffect(
Shader(function: shaderFunction,
arguments: [
.float2(Float(size),
Float(size)),
.float(((manager.pitch + manager.roll + 0.4) * 0.06))
])
)
.blur(radius: 28.0)
.contrast(3.7)
.mask(Circle())
.clipShape(ShapeWithHole(cutout: CGSize(width: 110.0, height: 110.0)))
.rotationEffect(.degrees((manager.pitch + manager.roll) * 20.0))
Circle()
.stroke(Color.black.opacity(0.5), lineWidth: 1.0)
.frame(width: 117.0, height: 117.0)
CircularTextView(title: "SWIFT.SHADER", radius: 80.0)
.rotationEffect(.degrees((manager.pitch + manager.roll)*20.0))
}
.drawingGroup()
.rotation3DEffect(.degrees(manager.pitch * 20.0), axis: (1, 0, 0))
.rotation3DEffect(.degrees(manager.roll * 20.0), axis: (0, 1, 0))
VStack {
Spacer()
Text(String(manager.pitch))
Text(String(manager.roll))
Text(String(((manager.pitch + manager.roll) * 0.02) - 0.01))
}
}
.ignoresSafeArea()
.statusBarHidden()
}
}
#Preview {
CDView()
}
//
// MotionManager.swift
// CD
//
// Created by Daniel Kuntz on 7/3/23.
//
import SwiftUI
import CoreMotion
final class MotionManager: ObservableObject {
@Published var pitch: Double = 0.0
@Published var roll: Double = 0.0
private var manager: CMMotionManager
init() {
self.manager = CMMotionManager()
self.manager.deviceMotionUpdateInterval = 1/60
self.manager.startDeviceMotionUpdates(to: .main) { [weak self] (motionData, error) in
guard error == nil else {
print(error!)
return
}
if let motionData = motionData {
self?.pitch = motionData.attitude.pitch
self?.roll = motionData.attitude.roll
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment