Skip to content

Commit 4ac55f1

Browse files
authored
Merge pull request twostraws#24 from twodayslate/genie
Add Genie/Suck Transition Effect
2 parents 97011b8 + 2d196e5 commit 4ac55f1

File tree

5 files changed

+142
-0
lines changed

5 files changed

+142
-0
lines changed

LICENSE

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,11 @@ Wind
6666
Based on: https://gl-transitions.com/editor/wind
6767
Original author: gre
6868
Metal port: Paul Hudson
69+
License: MIT
70+
71+
Genie
72+
---
73+
Based on: https://www.shadertoy.com/view/flyfRt
74+
Original author: altaha-ansari
75+
Metal port: twodayslate
6976
License: MIT

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1464,6 +1464,53 @@ struct ContentView: View {
14641464
}
14651465
```
14661466

1467+
</details>
1468+
1469+
### Genie
1470+
<details>
1471+
<summary> Details (Click to expand) </summary>
1472+
1473+
A transition that makes it look like the view is getting sucked into a corner.
1474+
1475+
**Parameters:**
1476+
1477+
- None.
1478+
1479+
Example code:
1480+
1481+
```swift
1482+
struct ContentView: View {
1483+
@State private var showingFirstView = true
1484+
1485+
var body: some View {
1486+
VStack {
1487+
if showingFirstView {
1488+
Image(systemName: "figure.walk.circle")
1489+
.font(.system(size: 300))
1490+
.foregroundStyle(.white)
1491+
.padding()
1492+
.background(.blue)
1493+
.drawingGroup()
1494+
.transition(.genie())
1495+
} else {
1496+
Image(systemName: "figure.run.circle")
1497+
.font(.system(size: 300))
1498+
.foregroundStyle(.white)
1499+
.padding()
1500+
.background(.indigo)
1501+
.drawingGroup()
1502+
.transition(.genie())
1503+
}
1504+
1505+
Button("Toggle Views") {
1506+
withAnimation(.easeIn(duration: 1.5)) {
1507+
showingFirstView.toggle()
1508+
}
1509+
}
1510+
}
1511+
}
1512+
}
1513+
```
14671514
</details>
14681515
</details>
14691516

@@ -1508,6 +1555,7 @@ Some shaders were ported to Metal by me, from other open-source samples also rel
15081555
- Radial is based on [Radial](https://gl-transitions.com/editor/Radial) by Xaychru / gre.
15091556
- Swirl is based on [Swirl](https://gl-transitions.com/editor/Swirl) by Sergey Kosarevsky / gre.
15101557
- Wind is based on [Wind](https://gl-transitions.com/editor/wind) by gre.
1558+
- Genie is based on [Mac Genie Effecty](https://www.shadertoy.com/view/flyfRt) by altaha-ansari.
15111559

15121560
## Where to learn more
15131561

Sandbox/Inferno/ShaderDescriptions/TransitionShader.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,6 @@ struct TransitionShader: Hashable, Identifiable {
4949
TransitionShader(name: "Radial", transition: .radial),
5050
TransitionShader(name: "Swirl", transition: .swirl(radius: 0.5)),
5151
TransitionShader(name: "Wind", transition: .wind(size: 0.1)),
52+
TransitionShader(name: "Genie", transition: .genie()),
5253
]
5354
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// Genie.metal
3+
// Inferno
4+
//
5+
// Created by Zachary Gorak on 12/2/23.
6+
//
7+
8+
#include <metal_stdlib>
9+
#include <SwiftUI/SwiftUI_Metal.h>
10+
using namespace metal;
11+
12+
float2 remap(float2 uv, float2 inputLow, float2 inputHigh, float2 outputLow, float2 outputHigh){
13+
float2 t = (uv - inputLow)/(inputHigh - inputLow);
14+
float2 final = mix(outputLow,outputHigh,t);
15+
return final;
16+
}
17+
18+
[[ stitchable ]] float2 genieTranstion(float2 position, float2 size, float effectValue) {
19+
// Normalized pixel coordinates (from 0 to 1)
20+
float2 normalizedPosition = position / size;
21+
float positiveEffect = effectValue*sign(effectValue);
22+
float progress = abs(sin(positiveEffect * M_PI_2_F));
23+
24+
float bias = pow((sin(normalizedPosition.y * M_PI_F) * 0.1), 0.9);
25+
26+
// right side
27+
float BOTTOM_POS = size.x;
28+
// width of the mini frame
29+
float BOTTOM_THICKNESS = 0.1;
30+
// height of the min frame
31+
float MINI_FRAME_THICKNESS = 0.0;
32+
// top right
33+
float2 MINI_FRAME_POS = float2(size.x, 0.0);
34+
35+
float min_x_curve = mix((BOTTOM_POS-BOTTOM_THICKNESS/2.0)+bias, 0.0, normalizedPosition.y);
36+
float max_x_curve = mix((BOTTOM_POS+BOTTOM_THICKNESS/2.0)-bias, 1.0, normalizedPosition.y);
37+
float min_x = mix(min_x_curve, MINI_FRAME_POS.x, progress);
38+
float max_x = mix(max_x_curve, MINI_FRAME_POS.x+MINI_FRAME_THICKNESS, progress);
39+
40+
float min_y = mix(0.0, MINI_FRAME_POS.y, progress);
41+
float max_y = mix(1.0, MINI_FRAME_POS.y+MINI_FRAME_THICKNESS, progress);
42+
43+
float2 modUV = remap(position, float2(min_x, min_y), float2(max_x, max_y), float2(0.0), float2(1.0));
44+
45+
return position + modUV * progress;
46+
47+
}

Sources/Inferno/SwiftUI/Transitions.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,30 @@ struct InfernoTransition: ViewModifier {
129129
}
130130
}
131131

132+
/// A Metal-powered distortion effect transition that needs to know
133+
/// the view's size. You probably don't want to use this directly, and
134+
/// should instead use one of the AnyTranstion extensions.
135+
@available(iOS 17, macOS 14, macCatalyst 17, tvOS 17, visionOS 1, *)
136+
struct InferoDistortionTranstion: ViewModifier {
137+
/// The name of the shader function we're rendering.
138+
var name: String
139+
140+
/// How far we are through the transition: 0 is unstarted, and 1 is finished.
141+
var progress = 0.0
142+
143+
func body(content: Content) -> some View {
144+
content
145+
.visualEffect { content, proxy in
146+
content
147+
.distortionEffect(
148+
InfernoShaderLibrary[dynamicMember: name](
149+
.float2(proxy.size),
150+
.float(progress)
151+
), maxSampleOffset: .zero)
152+
}
153+
}
154+
}
155+
132156
/// A transition that causes the incoming and outgoing views to become
133157
/// increasingly pixellated, then return to their normal state. While this
134158
/// happens the old view fades out and the new one fades in.
@@ -309,6 +333,21 @@ extension AnyTransition {
309333
)
310334
}
311335

336+
/// A transition that causes the incoming and outgoing views to become
337+
/// sucked in and ouf of the top right corner.
338+
public static func genie() -> AnyTransition {
339+
.asymmetric(
340+
insertion: .modifier(
341+
active: InferoDistortionTranstion(name: "genieTranstion", progress: 1),
342+
identity: InferoDistortionTranstion(name: "genieTranstion", progress: 0)
343+
),
344+
removal: .modifier(
345+
active: InferoDistortionTranstion(name: "genieTranstion", progress: 1),
346+
identity: InferoDistortionTranstion(name: "genieTranstion", progress: 0)
347+
)
348+
)
349+
}
350+
312351
/// A transition that creates an old-school radial wipe, starting from straight up.
313352
public static let radial: AnyTransition = .asymmetric(
314353
insertion: .modifier(

0 commit comments

Comments
 (0)