Skip to content

Instantly share code, notes, and snippets.

@Koshimizu-Takehito
Last active May 11, 2024 03:03
Show Gist options
  • Save Koshimizu-Takehito/dff57477bc5c01442cf6a1704a2e2ef2 to your computer and use it in GitHub Desktop.
Save Koshimizu-Takehito/dff57477bc5c01442cf6a1704a2e2ef2 to your computer and use it in GitHub Desktop.
JuliaSetView
#include <metal_stdlib>
using namespace metal;
namespace JuliaSet {
/// HSV -> RGB
half3 hsv2rgb(half3 c) {
half3 rgb = clamp(abs(fmod(c.x * 6.0 + half3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
return c.z * mix(half3(1.0), rgb, c.y);
}
[[ stitchable ]] half4 main(float2 position, half4 color, float4 box, float scale, float2 offset, float2 location) {
position = - 1 + 2.0 * position / min(box.w, box.z); // [-1, 1]
position /= pow(scale, 2); // [-1/pow(scale, 2), 1/pow(scale, 2)]
position -= float2(0, 1/pow(scale, 2)) - 2.0 * location;
// Zn (xは実部、yは虚部)
float2 z = position.xy;
// Zn+1 (xは実部、yは虚部)
float2 zNext = float2(0);
// 複素平面上の座標 (xは実部、yは虚部)
float2 c = offset;
bool diverge = false;
int elapsed = 0;
for (int i = 0; i < 10000; i++) {
zNext.x = pow(z.x, 2) - pow(z.y, 2);
zNext.y = 2.0 * z.x * z.y ;
z = zNext + c;
if (length(z) > 2.0) {
diverge = true;
break;
}
elapsed = i;
}
if (diverge) {
half3 color = hsv2rgb(half3(half(elapsed + 200)/400, 0.7, 0.8));
return half4(color, 1);
} else {
return half4(0.0, 0.0, 0.0, 1.0);
}
}
}
import SwiftUI
struct ContentView: View {
@State private var id = UUID()
var body: some View {
NavigationStack {
RootView(id: $id)
.toolbar { barItem }
.tint(.white)
}
}
var barItem: some View {
Button("Reset") {
id = UUID()
}
.fontWeight(.semibold)
}
}
typealias Animatable5 = AnimatablePair<
Double, AnimatablePair<AnimatablePair<Double, Double>, AnimatablePair<Double, Double>>
>
struct JuliaSetView: View, Animatable {
var scale: Double
var constant: CGPoint
var location: CGPoint
var animatableData: Animatable5 {
get {
.init(scale, .init(.init(constant.x, constant.y), .init(location.x, location.y)))
}
set {
scale = newValue.first
constant.x = newValue.second.first.first
constant.y = newValue.second.first.second
location.x = newValue.second.second.first
location.y = newValue.second.second.second
}
}
var body: some View {
Rectangle().colorEffect(
ShaderFunction(library: .default, name: "JuliaSet::main")(
.boundingRect,
.float(scale),
.float2(constant.x, constant.y),
.float2(location.x, location.y)
)
)
}
}
private struct RootView: View {
@Binding var id: UUID
@GestureState private var magnifyBy = 1.0
@State private var constant = CGPoint(x: 0.3575, y: 0.3575)
@State private var location = CGPoint.zero
@State private var lastLocation = CGPoint.zero
@State private var scale: Double = 1
@State private var lastScale: Double = 0.5
var body: some View {
ZStack {
GeometryReader { geometry in
let size = geometry.size
JuliaSetView(scale: scale, constant: constant, location: location)
.onTapGesture(count: 1) { location in
onTap(location: location, in: size)
}
.gesture(dragGesture(size: size))
.gesture(magnification)
}
.ignoresSafeArea()
VStack {
Spacer()
Slider(value: $constant.x, in: 0.3504...(constant.y - 0.0001))
Slider(value: $constant.y, in: 0.3506...0.4)
}
.padding()
}
.onChange(of: id) { _, _ in
reset()
}
}
var magnification: some Gesture {
MagnifyGesture()
.updating($magnifyBy) { value, gestureState, transaction in
gestureState = value.magnification / 2.0
scale = lastScale + value.magnification / 2.0
}
.onEnded { value in
lastScale = scale
}
}
private func onTap(location: CGPoint, in size: CGSize) {
withAnimation {
let r = pow(scale, 2)
self.location.x += 1/r * (location.x - size.width/2) / size.width
self.location.y += 1/r * (location.y - size.height/2) / size.width
lastLocation = self.location
}
}
private func dragGesture(size: CGSize) -> some Gesture {
DragGesture()
.onChanged { action in
onDrag(action: action, in: size)
}
.onEnded { action in
onDrag(action: action, in: size)
lastLocation = location
}
}
private func onDrag(action: DragGesture.Value, in size: CGSize) {
let base = lastLocation
let r = pow(scale, 2)
self.location.x = base.x + 1/r * (action.translation.width) / size.width
self.location.y = base.y + 1/r * (action.translation.height) / size.width
}
func reset() {
let scale = self.scale
let duration = scale > 1 ? min(log(scale), 2) : 0.5
withAnimation(.spring(duration: duration)) {
self.scale = 1
self.lastScale = 0.5
} completion: {
withAnimation(.spring(duration: duration)) {
location = .zero
lastLocation = .zero
}
withAnimation(.spring(duration: 1.5)) {
constant = CGPoint(x: 0.3575, y: 0.3575)
}
}
}
}
#Preview {
ContentView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment