├── sandbox-screenshot.png
├── Sandbox
├── Inferno
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── Flag.imageset
│ │ │ ├── flag.png
│ │ │ ├── flag@2x.png
│ │ │ ├── flag@3x.png
│ │ │ └── Contents.json
│ │ ├── HWS.imageset
│ │ │ ├── HWS.png
│ │ │ ├── HWS@2x.png
│ │ │ ├── HWS@3x.png
│ │ │ └── Contents.json
│ │ ├── Logo.imageset
│ │ │ ├── logo.png
│ │ │ ├── logo@2x.png
│ │ │ ├── logo@3x.png
│ │ │ └── Contents.json
│ │ ├── Doggo.imageset
│ │ │ ├── Doggo.png
│ │ │ ├── Doggo@2x.png
│ │ │ ├── Doggo@3x.png
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── Icon-1024.png
│ │ │ ├── Icon-128.png
│ │ │ ├── Icon-16.png
│ │ │ ├── Icon-256.png
│ │ │ ├── Icon-32 1.png
│ │ │ ├── Icon-32.png
│ │ │ ├── Icon-512.png
│ │ │ ├── Icon-64.png
│ │ │ ├── Icon-256 1.png
│ │ │ ├── Icon-512 1.png
│ │ │ ├── Icon-Square.png
│ │ │ └── Contents.json
│ │ ├── AppIcon.solidimagestack
│ │ │ ├── Back.solidimagestacklayer
│ │ │ │ ├── Contents.json
│ │ │ │ └── Content.imageset
│ │ │ │ │ ├── Icon-Square.png
│ │ │ │ │ └── Contents.json
│ │ │ ├── Front.solidimagestacklayer
│ │ │ │ ├── Contents.json
│ │ │ │ └── Content.imageset
│ │ │ │ │ ├── Icon-Square.png
│ │ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ └── AccentColor.colorset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── Helpers
│ │ ├── PlatformShims.swift
│ │ ├── PreviewType.swift
│ │ ├── String-ShaderName.swift
│ │ ├── ToggleAlphaButton.swift
│ │ ├── TransformationType.swift
│ │ ├── ContentPreview.swift
│ │ └── ContentPreviewSelector.swift
│ ├── Inferno.entitlements
│ ├── InfernoApp.swift
│ ├── ShaderPreviews
│ │ ├── BlurPreview.swift
│ │ ├── RelativeTouchTransformationPreview.swift
│ │ ├── AbsoluteTouchTransformationPreview.swift
│ │ ├── GenerativePreview.swift
│ │ ├── TouchTransformationPreview.swift
│ │ ├── TransitionPreview.swift
│ │ ├── SimpleTransformationPreview.swift
│ │ ├── ProgressiveBlurPreview.swift
│ │ ├── TimeTransformationPreview.swift
│ │ └── ShapeBlurPreview.swift
│ ├── ShaderDescriptions
│ │ ├── BlurEffect.swift
│ │ ├── TransitionShader.swift
│ │ ├── GenerativeShader.swift
│ │ ├── SimpleTransformationShader.swift
│ │ ├── TouchTransformationShader.swift
│ │ └── TimeTransformationShader.swift
│ ├── WelcomeView.swift
│ ├── SwiftUI Wrappers
│ │ ├── VisualEffect+variableBlur.swift
│ │ └── View+variableBlur.swift
│ └── ContentView.swift
├── Inferno.xcodeproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── README.md
├── .gitignore
├── Shaders
├── Transition
│ ├── Shift.metal
│ ├── Circle.metal
│ ├── Diamond.metal
│ ├── Genie.metal
│ ├── CircleWave.metal
│ ├── DiamondWave.metal
│ ├── Radial.metal
│ ├── Wind.metal
│ ├── Swirl.metal
│ ├── Pixellate.metal
│ └── Crosswarp.metal
├── Transformation
│ ├── Passthrough.metal
│ ├── GradientFill.metal
│ ├── Recolor.metal
│ ├── InvertAlpha.metal
│ ├── Wave.metal
│ ├── AnimatedGradientFill.metal
│ ├── Checkerboard.metal
│ ├── Interlace.metal
│ ├── ColorPlanes.metal
│ ├── Water.metal
│ ├── RelativeWave.metal
│ ├── WhiteNoise.metal
│ ├── Emboss.metal
│ ├── Infrared.metal
│ ├── SimpleLoupe.metal
│ ├── RainbowNoise.metal
│ ├── WarpingLoupe.metal
│ └── CircleWave.metal
├── Generation
│ ├── Sinebow.metal
│ └── LightGrid.metal
└── Blur
│ └── VariableGaussianBlur.metal
├── LICENSE
├── CODE_OF_CONDUCT.md
└── Transitions.swift
/sandbox-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/sandbox-screenshot.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/Flag.imageset/flag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/Flag.imageset/flag.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/HWS.imageset/HWS.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/HWS.imageset/HWS.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/Logo.imageset/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/Logo.imageset/logo.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/Doggo.imageset/Doggo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/Doggo.imageset/Doggo.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/HWS.imageset/HWS@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/HWS.imageset/HWS@2x.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/HWS.imageset/HWS@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/HWS.imageset/HWS@3x.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/Doggo.imageset/Doggo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/Doggo.imageset/Doggo@2x.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/Doggo.imageset/Doggo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/Doggo.imageset/Doggo@3x.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/Flag.imageset/flag@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/Flag.imageset/flag@2x.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/Flag.imageset/flag@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/Flag.imageset/flag@3x.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/Logo.imageset/logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/Logo.imageset/logo@2x.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/Logo.imageset/logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/Logo.imageset/logo@3x.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-1024.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-128.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-16.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-256.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-32 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-32 1.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-32.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-512.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-64.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-256 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-256 1.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-512 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-512 1.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-Square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Icon-Square.png
--------------------------------------------------------------------------------
/Sandbox/Inferno.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sandbox/README.md:
--------------------------------------------------------------------------------
1 | This is a small project that demonstrates each of the shaders being applied with some example settings.
2 |
3 | Note: to avoid duplicating code, this references Transitions.swift and all its shaders from the parent directory.
4 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Icon-Square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Icon-Square.png
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Icon-Square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twodayslate/Inferno/main/Sandbox/Inferno/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Icon-Square.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | xcuserdata/
4 |
5 | build/
6 | DerivedData/
7 | *.moved-aside
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | *.ipa
17 | *.dSYM.zip
18 | *.dSYM
19 | .build/
20 |
21 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.solidimagestack/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | },
6 | "layers" : [
7 | {
8 | "filename" : "Front.solidimagestacklayer"
9 | },
10 | {
11 | "filename" : "Back.solidimagestacklayer"
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Helpers/PlatformShims.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PlatformShims.swift
3 | // Inferno
4 | //
5 | // Created by Paul Hudson on 18/11/2023.
6 | //
7 |
8 | import SwiftUI
9 |
10 | #if !os(macOS)
11 | extension View {
12 | func navigationSubtitle(_ text: String) -> some View {
13 | self
14 | }
15 | }
16 | #endif
17 |
--------------------------------------------------------------------------------
/Sandbox/Inferno.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Icon-Square.png",
5 | "idiom" : "vision",
6 | "scale" : "2x"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Icon-Square.png",
5 | "idiom" : "vision",
6 | "scale" : "2x"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Inferno.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/HWS.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "HWS.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "HWS@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "HWS@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/Flag.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "flag.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "flag@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "flag@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/Logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "logo.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "logo@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "logo@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/Doggo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Doggo.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "Doggo@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "Doggo@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Helpers/PreviewType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PreviewType.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | /// The various preview options we provide.
11 | enum PreviewType: String {
12 | // A flag emoji.
13 | case emoji
14 |
15 | // An image from our asset catalog.
16 | case image
17 |
18 | // A solid rectangle.
19 | case shape
20 |
21 | // An interesting SF Symbol.
22 | case symbol
23 | }
24 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Helpers/String-ShaderName.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String-ShaderName.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | /// An extension that converts strings to their equivalent shader names.
11 | extension String {
12 | /// Converts a string to its equivalent Metal shader function name.
13 | var shaderName: String {
14 | let camelCase = prefix(1).lowercased() + dropFirst()
15 | return camelCase.replacing(" ", with: "")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/InfernoApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfernoApp.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 |
11 | @main
12 | /// The main entry point for the sandbox app.
13 | struct InfernoApp: App {
14 | var body: some Scene {
15 | WindowGroup {
16 | ContentView()
17 | #if !os(iOS)
18 | .frame(minWidth: 1000, minHeight: 800)
19 | #endif
20 | }
21 | #if !os(iOS)
22 | .defaultSize(width: 1000, height: 800)
23 | #endif
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Helpers/ToggleAlphaButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToggleAlphaButton.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ToggleAlphaButton: View {
11 | @Binding var opacity: Double
12 |
13 | var body: some View {
14 | Button("Toggle Opacity", systemImage: "cube.transparent") {
15 | withAnimation {
16 | if opacity.isZero {
17 | opacity = 1
18 | } else {
19 | opacity = 0
20 | }
21 | }
22 | }
23 | .labelStyle(.titleAndIcon)
24 | }
25 | }
26 |
27 | #Preview {
28 | ToggleAlphaButton(opacity: .constant(1))
29 | }
30 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Helpers/TransformationType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TransformationType.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 |
11 | /// Contains the various different effects we want to apply.
12 | enum TransformationType {
13 | // Applies a colorEffect() modifier.
14 | case colorEffect
15 |
16 | // Applies a distortionEffect() modifier.
17 | case distortionEffect
18 |
19 | // Applies a visualEffect() modifier, with
20 | // a colorEffect() modifier nested inside.
21 | case visualEffectColor
22 |
23 | // Applies a visualEffect() modifier, with
24 | // a distortionEffect() modifier nested inside.
25 | case visualEffectDistortion
26 | }
27 |
--------------------------------------------------------------------------------
/Shaders/Transition/Shift.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Distortion.metal
3 | // Inferno
4 | //
5 | // Created by Zachary Gorak on 12/2/23.
6 | //
7 |
8 | #include
9 | #include
10 | using namespace metal;
11 |
12 |
13 | [[ stitchable ]] float2 shiftTranstion(float2 position, float2 size, float effectValue) {
14 | float skewF = 0.1*size.x;
15 | float yRatio = position.y/size.y;
16 |
17 | float positiveEffect = effectValue*sign(effectValue);
18 | float skewProgress = min(0.5-abs(positiveEffect-0.5), 0.2)/0.2;
19 |
20 | float skew = effectValue>0 ? yRatio*skewF*skewProgress : (1-yRatio)*skewF*skewProgress;
21 | float shift = effectValue*size.x;
22 |
23 | return float2(position.x+(shift+skew*sign(effectValue)), position.y);
24 | }
25 |
--------------------------------------------------------------------------------
/Shaders/Transformation/Passthrough.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Passthrough.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A shader that outputs the same color it was provided with.
12 | ///
13 | /// This shader does nothing at all. It's helpful to include as an example
14 | /// shader, or to disable another shader you had selected.
15 | ///
16 | /// - Parameter position: The user-space coordinate of the current pixel.
17 | /// - Parameter color: The current color of the pixel.
18 | /// - Returns: The original pixel color.
19 | [[ stitchable ]] half4 passthrough(float2 position, half4 color) {
20 | // Just send back to the current color as the new color.
21 | return color;
22 | }
23 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ShaderPreviews/BlurPreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BlurPreview.swift
3 | // Inferno
4 | //
5 | // Created by Dale Price on 11/28/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A SwiftUI view that displays a specific blur effect preview.
11 | struct BlurPreview: View {
12 | /// The opacity of the preview view.
13 | @State private var opacity = 1.0
14 |
15 | /// The effect to render.
16 | var effect: BlurEffect
17 |
18 | var body: some View {
19 | Group {
20 | switch effect.effect {
21 | case .progressiveBlur:
22 | ProgressiveBlurPreview(opacity: $opacity)
23 | case .shape(let shape):
24 | ShapeBlurPreview(opacity: $opacity, shape: shape)
25 | }
26 | }
27 | .toolbar {
28 | ToggleAlphaButton(opacity: $opacity)
29 | }
30 | .navigationSubtitle(effect.name)
31 | }
32 | }
33 |
34 | #Preview {
35 | BlurPreview(effect: BlurEffect.effects[0])
36 | }
37 |
--------------------------------------------------------------------------------
/Shaders/Transformation/GradientFill.metal:
--------------------------------------------------------------------------------
1 | //
2 | // GradientFill.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | #include
10 | using namespace metal;
11 |
12 | /// A shader that generates a gradient fill.
13 | ///
14 | /// This creates a diagonal gradient effect by calculating unique red and blue values
15 | /// by dividing the X/Y and Y/X position of the pixel respectively.
16 | ///
17 | /// - Parameter position: The user-space coordinate of the current pixel.
18 | /// - Parameter color: The current color of the pixel, used for alpha calculations.
19 | /// - Returns: The new pixel color.
20 | [[ stitchable ]] half4 gradientFill(float2 position, half4 color) {
21 | // Send back a new color based on the position of the pixel
22 | // factoring in the original alpha to get smooth edges.
23 | return half4(position.x / position.y, 0.0h, position.y / position.x, 1.0h) * color.a;
24 | }
25 |
--------------------------------------------------------------------------------
/Shaders/Transformation/Recolor.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Recolor.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A shader that changes texture colors to a replacement, while respecting the
12 | /// current alpha value.
13 | ///
14 | /// This works by replacing all colors with the replacement color, but also taking into
15 | /// account the alpha value of the current color so we respect transparency.
16 | ///
17 | /// - Parameter position: The user-space coordinate of the current pixel.
18 | /// - Parameter color: The current color of the pixel.
19 | /// - Parameter replacement: The new color to use in place of the current color.
20 | /// - Returns: The new pixel color.
21 | [[ stitchable ]] half4 recolor(float2 position, half4 color, half4 replacement) {
22 | // Send back the RGB values from the replacement color
23 | // factoring in the original alpha to preserve opacity.
24 | return replacement * color.a;
25 | }
26 |
--------------------------------------------------------------------------------
/Shaders/Transformation/InvertAlpha.metal:
--------------------------------------------------------------------------------
1 | //
2 | // InvertAlpha.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A shader that inverts the alpha of an image, replacing transparent colors with
12 | /// a supplied color.
13 | ///
14 | /// This sends back the original RGB values for the color, but subtracts the current
15 | /// alpha from 1 to invert it. So, if the alpha was 1 it becomes 0, if the alpha was
16 | /// 0 it becomes 1, and so on.
17 | ///
18 | /// - Parameter position: The user-space coordinate of the current pixel.
19 | /// - Parameter color: The current color of the pixel.
20 | /// - Parameter replacement: The replacement color to use for pixels.
21 | /// - Returns: The new pixel color.
22 | [[ stitchable ]] half4 invertAlpha(float2 position, half4 color, half4 replacement) {
23 | // Send back the RGB values from our input pixel, but
24 | // flip the alpha value around.
25 | return half4(replacement.rgb, 1.0h) * (1.0h - color.a);
26 | }
27 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Helpers/ContentPreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentPreview.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// Provides three different preview options for shaders
11 | /// where that makes sense.
12 | struct ContentPreview: View {
13 | @AppStorage("currentPreviewType") private var currentPreviewType = PreviewType.symbol
14 |
15 | // Note: No matter what, we ensure a 400x400
16 | // frame to avoid the view jumping around.
17 | var body: some View {
18 | switch currentPreviewType {
19 | case .emoji:
20 | Text("🏳️🌈")
21 | .frame(width: 400, height: 400)
22 | .font(.system(size: 300))
23 | case .image:
24 | Image(.doggo)
25 | .frame(width: 400, height: 400)
26 | case .shape:
27 | RoundedRectangle(cornerRadius: 40)
28 | .fill(.white)
29 | .frame(height: 400)
30 | .padding(.horizontal, 50)
31 | case .symbol:
32 | Image(systemName: "figure.walk.circle")
33 | .frame(width: 400, height: 400)
34 | .font(.system(size: 300))
35 | .foregroundStyle(.white)
36 | }
37 | }
38 | }
39 |
40 | #Preview {
41 | ContentPreview()
42 | }
43 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Helpers/ContentPreviewSelector.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentPreviewSelector.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A simple UI to select between the various preview types.
11 | struct ContentPreviewSelector: View {
12 | @AppStorage("currentPreviewType") private var currentPreviewType = PreviewType.symbol
13 |
14 | var body: some View {
15 | #if !os(visionOS)
16 | Picker("Preview using:", selection: $currentPreviewType) {
17 | Text("Emoji").tag(PreviewType.emoji)
18 | Text("Image").tag(PreviewType.image)
19 | Text("Shape").tag(PreviewType.shape)
20 | Text("Symbol").tag(PreviewType.symbol)
21 | }
22 | .frame(maxWidth: 400)
23 | .pickerStyle(.segmented)
24 | .padding()
25 | #else
26 | HStack {
27 | Button("Emoji", action: { currentPreviewType = .emoji })
28 | Button("Image", action: { currentPreviewType = .image })
29 | Button("Shape", action: { currentPreviewType = .shape })
30 | Button("Symbol", action: { currentPreviewType = .symbol })
31 | }
32 | .frame(maxWidth: 400)
33 | .padding()
34 | #endif
35 | }
36 | }
37 |
38 | #Preview {
39 | ContentPreview()
40 | }
41 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ShaderPreviews/RelativeTouchTransformationPreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RelativeTouchTransformationPreview.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A view that sends drag gesture data to a shader based on the relative
11 | /// position of a touch – how much the user dragged since the last movement.
12 | struct RelativeTouchTransformationPreview: View {
13 | /// The current touch location.
14 | @State private var touch = CGSize.zero
15 |
16 | /// The opacity of our preview view, so users can check how fading works.
17 | @Binding var opacity: Double
18 |
19 | /// The input value to control shader strength or similar.
20 | @Binding var value: Double
21 |
22 | /// The shader we're rendering.
23 | var shader: TouchTransformationShader
24 |
25 | var body: some View {
26 | VStack {
27 | ContentPreviewSelector()
28 | ContentPreview()
29 | .opacity(opacity)
30 | .drawingGroup()
31 | .layerEffect(
32 | shader.createShader(touchLocation: CGPoint(x: touch.width, y: touch.height), value: value),
33 | maxSampleOffset: .zero
34 | )
35 | .gesture(
36 | DragGesture(minimumDistance: 0)
37 | .onChanged { touch = $0.translation }
38 | )
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ShaderPreviews/AbsoluteTouchTransformationPreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AbsoluteTouchTransformationPreview.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A view that sends drag gesture data to a shader based on the absolute
11 | /// position of a touch.
12 | struct AbsoluteTouchTransformationPreview: View {
13 | /// The current touch location.
14 | @State private var touch = CGPoint.zero
15 |
16 | /// The opacity of our preview view, so users can check how fading works.
17 | @Binding var opacity: Double
18 |
19 | /// The input value to control shader strength or similar.
20 | @Binding var value: Double
21 |
22 | /// The shader we're rendering.
23 | var shader: TouchTransformationShader
24 |
25 | var body: some View {
26 | VStack {
27 | ContentPreviewSelector()
28 | ContentPreview()
29 | .opacity(opacity)
30 | .visualEffect { content, proxy in
31 | content
32 | .layerEffect(
33 | shader.createShader(size: proxy.size, touchLocation: touch, value: value),
34 | maxSampleOffset: .zero
35 | )
36 | }
37 | .gesture(
38 | DragGesture(minimumDistance: 0)
39 | .onChanged { touch = $0.location }
40 | )
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Shaders/Transformation/Wave.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Wave.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A shader that generates a uniform wave effect.
12 | ///
13 | /// This works by offsetting the Y position of each pixel by some amount of its X
14 | /// position. Using sin() for this generates values between -1 and 1, but this then
15 | /// gets multiplied by 10 to increase the strength.
16 | ///
17 | /// - Parameter position: The user-space coordinate of the current pixel.
18 | /// - Parameter time: The number of elapsed seconds since the shader was created
19 | /// - Parameter speed: How fast to make the waves ripple. Try starting with a value of 5.
20 | /// - Parameter smoothing: How much to smooth out the ripples, where greater values
21 | /// produce a smoother effect. Try starting with a value of 20.
22 | /// - Parameter strength: How pronounced to make the ripple effect. Try starting with a
23 | /// value of 5.
24 | /// - Returns: The new pixel color.
25 | [[ stitchable ]] float2 wave(float2 position, float time, float speed, float smoothing, float strength) {
26 | // Offset our Y value by some amount of our X position.
27 | // Using time * 5 speeds up the wave, and dividing the
28 | // X position by 20 smooths out the wave to avoid jaggies.
29 | position.y += sin(time * speed + position.x / smoothing) * strength;
30 |
31 | // Send back the offset position.
32 | return position;
33 | }
34 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ShaderPreviews/GenerativePreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GenerativePreview.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A trivial SwiftUI view that renders a Metal shader into a whole
11 | /// rectangle space, so it has complete control over rendering.
12 | struct GenerativePreview: View {
13 | /// The initial time this view was created, so we can send
14 | /// elapsed time to the shader.
15 | @State private var start = Date.now
16 |
17 | /// The opacity of our preview view, so users can check how fading works.
18 | @State private var opacity = 1.0
19 |
20 | /// The shader we're rendering.
21 | var shader: GenerativeShader
22 |
23 | var body: some View {
24 | VStack {
25 | ContentPreviewSelector()
26 |
27 | TimelineView(.animation) { tl in
28 | let time = start.distance(to: tl.date)
29 |
30 | ContentPreview()
31 | .opacity(opacity)
32 | .visualEffect { content, proxy in
33 | content.colorEffect(
34 | shader.createShader(elapsedTime: time, size: proxy.size)
35 | )
36 | }
37 | }
38 | }
39 | .toolbar {
40 | ToggleAlphaButton(opacity: $opacity)
41 | }
42 | .navigationSubtitle(shader.name)
43 | }
44 | }
45 |
46 | #Preview {
47 | TransitionPreview(shader: .example)
48 | }
49 |
50 |
--------------------------------------------------------------------------------
/Shaders/Transition/Circle.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Circle.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A transition where many circles grow upwards to reveal the new content.
12 | ///
13 | /// This works by calculating how far this pixel is from the center of its nearest
14 | /// circle, then either sending back the original color or transparent depending
15 | /// on whether our distance is less than the transition progress.
16 | ///
17 | /// - Parameter position: The user-space coordinate of the current pixel.
18 | /// - Parameter color: The current color of the pixel.
19 | /// - Parameter amount: The progress of the transition, from 0 to 1.
20 | /// - Parameter circleSize: How big to make the circles.
21 | /// - Returns: The new pixel color.
22 | [[ stitchable ]] half4 circleTransition(float2 position, half4 color, float amount, float circleSize) {
23 | // Figure out our position relative to the nearest
24 | // circle.
25 | half2 f = half2(fract(position / circleSize));
26 |
27 | // Calculate the Euclidean distance from this pixel
28 | // to the center of the nearest circle.
29 | half d = distance(f, 0.5h);
30 |
31 | // If the transition has progressed beyond our distance,
32 | // factoring in our X/Y UV coordinate…
33 | if (d < amount) {
34 | // Send back the color
35 | return color;
36 | } else {
37 | // Otherwise send back clear.
38 | return half4(0.0h);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ShaderDescriptions/BlurEffect.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BlurShader.swift
3 | // Inferno
4 | //
5 | // Created by Dale Price on 11/28/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// An effect based on a blur shader that specifies the effect to use.
11 | struct BlurEffect: Hashable, Identifiable {
12 |
13 | /// Specifies the type of blur effect to apply. These all use the same shader but draw a different mask using SwiftUI's `GraphicsContext`.
14 | enum EffectType {
15 | /// A blur masked with a linear gradient.
16 | case progressiveBlur
17 |
18 | /// A blur masked with a shape.
19 | case shape(any Shape)
20 | }
21 |
22 | // Unique, random identifier for this effect.
23 | var id = UUID()
24 |
25 | /// The human readable name for this effect.
26 | var name: String
27 |
28 | /// The type of blur effect to use.
29 | var effect: EffectType
30 |
31 | /// Custom Equatable conformance.
32 | static func ==(lhs: BlurEffect, rhs: BlurEffect) -> Bool {
33 | lhs.id == rhs.id
34 | }
35 |
36 | /// Custom Hashable conformance.
37 | func hash(into hasher: inout Hasher) {
38 | hasher.combine(id)
39 | }
40 |
41 | /// All the blur effects we want to show.
42 | static let effects: [BlurEffect] = [
43 | BlurEffect(name: "Progressive Blur", effect: .progressiveBlur),
44 | BlurEffect(name: "Vignette", effect: .shape(Ellipse())),
45 | BlurEffect(name: "Rounded Rectangle Mask", effect: .shape(RoundedRectangle(cornerRadius: 25.0)))
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/Shaders/Transition/Diamond.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Diamond.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A transition where many diamonds grow upwards to reveal the new content.
12 | ///
13 | /// This works by calculating how far this pixel is from the center of its nearest
14 | /// diamond, then either sending back the original color or transparent depending
15 | /// on whether our distance is less than the transition progress. The distance
16 | /// to the diamond is calculated using Manhattan distance.
17 | ///
18 | /// - Parameter position: The user-space coordinate of the current pixel.
19 | /// - Parameter color: The current color of the pixel.
20 | /// - Parameter amount: The progress of the transition, from 0 to 1.
21 | /// - Parameter circleSize: How big to make the diamonds.
22 | /// - Returns: The new pixel color.
23 | [[ stitchable ]] half4 diamondTransition(float2 position, half4 color, float amount, float diamondSize) {
24 | // Figure out our position relative to the nearest
25 | // diamond.
26 | half2 f = half2(fract(position / diamondSize));
27 |
28 | // Calculate the Manhattan distance from our pixel to
29 | // the center of the nearest diamond.
30 | half d = abs(f.x - 0.5h) + abs(f.y - 0.5h);
31 |
32 | // If the transition has progressed beyond our distance…
33 | if (d < amount) {
34 | // Send back the color
35 | return color;
36 | } else {
37 | // Otherwise send back clear.
38 | return half4(0.0h);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Shaders/Transformation/AnimatedGradientFill.metal:
--------------------------------------------------------------------------------
1 | //
2 | // AnimatedGradientFill.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A shader that generates a constantly cycling color gradient, centered
12 | /// on the input view.
13 | ///
14 | /// This works be calculating the angle from center of our input view
15 | /// to the current pixel, then using that angle to create RGB values.
16 | /// Using abs() for those color components ensures all values lie in
17 | /// the range 0 to 1.
18 | ///
19 | /// - Parameter position: The user-space coordinate of the current pixel.
20 | /// - Parameter color: The current color of the pixel.
21 | /// - Parameter time: The number of elapsed seconds since the shader was created
22 | /// - Returns: The new pixel color.
23 | [[ stitchable ]] half4 animatedGradientFill(float2 position, half4 color, float2 size, float time) {
24 | // Calculate our coordinate in UV space, 0 to 1.
25 | half2 uv = half2(position / size);
26 |
27 | // Get the same UV in the range -1 to 1, so that
28 | // 0 is in the center.
29 | half2 rp = uv * 2.0h - 1.0h;
30 |
31 | // Calculate the angle top this pixel, adding in time
32 | // so it's constantly changing.
33 | half angle = atan2(rp.y, rp.x) + time;
34 |
35 | // Send back variations on the sine of that angle, so we
36 | // get a range of colors. The use of abs() here avoids
37 | // negative values for any color component.
38 | return half4(abs(sin(angle)), abs(sin(angle + 2.0h)), abs(sin(angle + 4.0h)), 1.0h) * color.a;
39 | }
40 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/WelcomeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WelcomeView.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// An initial view to give an overview of the sandbox, plus links to more information.
11 | struct WelcomeView: View {
12 | /// Tracks whether the user is currently hovering over the Hacking with Swift logo.
13 | @State private var logoHover = false
14 |
15 | var body: some View {
16 | VStack(spacing: 10) {
17 | Spacer()
18 |
19 | Image(.logo)
20 | .padding(.bottom, 10)
21 |
22 | Link("github.com/twostraws/inferno", destination: URL(string: "https://github.com/twostraws/inferno")!)
23 | .font(.title2.bold())
24 |
25 | Text("This is a small sandbox so you can see the various shaders in action. To use the shaders in your own code, follow the instructions in the documentation rather than taking code from here.")
26 | .multilineTextAlignment(.center)
27 | .font(.title3)
28 |
29 | Spacer()
30 | Spacer()
31 |
32 | Link(destination: URL(string: "https://www.hackingwithswift.com")!) {
33 | VStack {
34 | Image(.HWS)
35 | .renderingMode(logoHover ? .template : .original)
36 | Text("A Hacking with Swift Project")
37 | }
38 | }
39 | .onHover { logoHover = $0 }
40 | .foregroundStyle(.white)
41 | }
42 | .navigationSubtitle("Welcome to the Shader Sandbox")
43 | .padding()
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Icon-Square.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | },
9 | {
10 | "filename" : "Icon-16.png",
11 | "idiom" : "mac",
12 | "scale" : "1x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "Icon-32 1.png",
17 | "idiom" : "mac",
18 | "scale" : "2x",
19 | "size" : "16x16"
20 | },
21 | {
22 | "filename" : "Icon-32.png",
23 | "idiom" : "mac",
24 | "scale" : "1x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "Icon-64.png",
29 | "idiom" : "mac",
30 | "scale" : "2x",
31 | "size" : "32x32"
32 | },
33 | {
34 | "filename" : "Icon-128.png",
35 | "idiom" : "mac",
36 | "scale" : "1x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "Icon-256 1.png",
41 | "idiom" : "mac",
42 | "scale" : "2x",
43 | "size" : "128x128"
44 | },
45 | {
46 | "filename" : "Icon-256.png",
47 | "idiom" : "mac",
48 | "scale" : "1x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "Icon-512 1.png",
53 | "idiom" : "mac",
54 | "scale" : "2x",
55 | "size" : "256x256"
56 | },
57 | {
58 | "filename" : "Icon-512.png",
59 | "idiom" : "mac",
60 | "scale" : "1x",
61 | "size" : "512x512"
62 | },
63 | {
64 | "filename" : "Icon-1024.png",
65 | "idiom" : "mac",
66 | "scale" : "2x",
67 | "size" : "512x512"
68 | }
69 | ],
70 | "info" : {
71 | "author" : "xcode",
72 | "version" : 1
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Shaders/Transition/Genie.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Genie.metal
3 | // Inferno
4 | //
5 | // Created by Zachary Gorak on 12/2/23.
6 | //
7 |
8 | #include
9 | #include
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 | }
48 |
--------------------------------------------------------------------------------
/Shaders/Transformation/Checkerboard.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Checkerboard.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A shader that replaces the current image with a checkerboard pattern,
12 | /// flipping between an input color and a replacement.
13 | ///
14 | /// This works by dividing our pixel position by the square size, then casting the
15 | /// result to an integer to round it downwards. It then uses XOR with the resulting
16 | /// X/Y value to figure out if exactly one of those values ends in a 1, and if it does
17 | /// sends back the replacement color. So, if we're in row 0 and column 1 it will send
18 | /// back the replacement color, as will row 1 column 0, but row 0/column 0 and
19 | /// row 1/column 1 will both send back the original value.
20 | ///
21 | /// - Parameter position: The user-space coordinate of the current pixel.
22 | /// - Parameter color: The current color of the pixel.
23 | /// - Parameter replacement: The replacement color to be used for checkered squares.
24 | /// - Parameter size: The size of the whole image, in user-space.
25 | /// - Returns: The new pixel color, either the replacement color or clear.
26 | [[ stitchable ]] half4 checkerboard(float2 position, half4 color, half4 replacement, float size) {
27 | // Calculate which square of the checkerboard we're in,
28 | // rounding values down.
29 | uint2 posInChecks = uint2(position.x / size, position.y / size);
30 |
31 | // XOR our X and Y position, then check if the result
32 | // is odd.
33 | bool isColor = (posInChecks.x ^ posInChecks.y) & 1;
34 |
35 | // If it's odd send back the replacement color,
36 | // otherwise send back the original
37 | return isColor ? replacement * color.a : color;
38 | }
39 |
--------------------------------------------------------------------------------
/Shaders/Transformation/Interlace.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Interlace.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A shader that applies an interlacing effect where horizontal lines of the
12 | /// original color are separated by lines of another color.
13 | ///
14 | /// This works using modulus: if the current pixel's position modulo twice the line
15 | /// width is less than the line width then draw the original color. Otherwise blend the
16 | /// original color with the interlacing color based on the user's provided strength.
17 | ///
18 | /// - Parameter width: The width of the interlacing lines. Ranges of 1 to 4 work best;
19 | /// try starting with 1.
20 | /// - Parameter replacement: The color to use for interlacing lines. Try starting with black.
21 | /// - Parameter strength: How much to blend interlaced lines with color. Specify 0
22 | /// (not at all) up to 1 (fully).
23 | /// - Returns: The new pixel color.
24 | [[ stitchable ]] half4 interlace(float2 position, half4 color, float width, half4 replacement, float strength) {
25 | // If the current color is not transparent…
26 | if (color.a > 0.0h) {
27 | // If we are an alternating horizontal line…
28 | if (fmod(position.y, width * 2.0) <= width) {
29 | // Render the original color
30 | return color;
31 | } else {
32 | // Otherwise blend the original color with the provided color
33 | // at whatever strength was requested, multiplying by this pixel's
34 | // alpha to avoid a hard edge.
35 | return half4(mix(color, replacement, strength)) * color.a;
36 | }
37 | } else {
38 | // Use the current (transparent) color
39 | return color;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Shaders/Transformation/ColorPlanes.metal:
--------------------------------------------------------------------------------
1 | //
2 | // ColorPlanes.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | #include
10 | using namespace metal;
11 |
12 | /// A shader that separates the RGB values for a pixel and offsets them to create
13 | /// a glitch-style effect.
14 | ///
15 | /// This works by reading different pixels than the original for both the red
16 | /// and blue color components, offsetting them by the `offset` value
17 | /// and a fixed multiplier. So, for a pixel at (100, 100) with an `offset`
18 | /// of 10, we would use the original green and alpha values at that location,
19 | /// but offset the red by (20, 20) and the blue by (10, 10) – we would use
20 | /// the red value from (120, 120) and the blue value from (110, 110).
21 | ///
22 | /// - Parameter position: The user-space coordinate of the current pixel.
23 | /// - Parameter layer: The SwiftUI layer we're reading from.
24 | /// - Parameter offset: How much to offset colors by.
25 | /// - Returns: The new pixel color.
26 | [[ stitchable ]] half4 colorPlanes(float2 position, SwiftUI::Layer layer, float2 offset) {
27 | // Our red value should be read from double our offset.
28 | float2 red = position - (offset * 2.0);
29 |
30 | // Our blue value should be read from our offset.
31 | float2 blue = position - offset;
32 |
33 | // Read the green value from the actual position.
34 | half4 color = layer.sample(position);
35 |
36 | // Make our color use the value at the red location.
37 | color.r = layer.sample(red).r;
38 |
39 | // The same, for the blue location.
40 | color.b = layer.sample(blue).b;
41 |
42 | // Send back the result, factoring in the original alpha
43 | // to get smoother edges.
44 | return color * color.a;
45 | }
46 |
--------------------------------------------------------------------------------
/Shaders/Transition/CircleWave.metal:
--------------------------------------------------------------------------------
1 | //
2 | // CircleWave.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A transition where many circles grow upwards to reveal the new content,
12 | /// with the circles moving outwards from the top-left edge.
13 | ///
14 | /// This works works identically to the circle transition, except it factors in the
15 | /// X and Y coordinate of the current pixel. This means pixels in the top-left
16 | /// of the source layer will transition first, because their UV positions are closer
17 | /// to (0, 0).
18 | ///
19 | /// - Parameter position: The user-space coordinate of the current pixel.
20 | /// - Parameter color: The current color of the pixel.
21 | /// - Parameter size: The size of the whole image, in user-space.
22 | /// - Parameter amount: The progress of the transition, from 0 to 1.
23 | /// - Parameter circleSize: How big to make the circles.
24 | /// - Returns: The new pixel color.
25 | [[ stitchable ]] half4 circleWaveTransition(float2 position, half4 color, float2 size, float amount, float circleSize) {
26 | // Calculate our coordinate in UV space, 0 to 1.
27 | half2 uv = half2(position / size);
28 |
29 | // Figure out our position relative to the nearest
30 | // circle.
31 | half2 f = half2(fract(position / circleSize));
32 |
33 | // Calculate the Euclidean distance from this pixel
34 | // to the center of the nearest circle.
35 | half d = distance(f, 0.5);
36 |
37 | // If the transition has progressed beyond our distance,
38 | // factoring in our X/Y UV coordinate…
39 | if (d + uv.x + uv.y < amount * 3.0) {
40 | // Send back the color
41 | return color;
42 | } else {
43 | // Otherwise send back clear.
44 | return half4(0.0h);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Shaders/Transition/DiamondWave.metal:
--------------------------------------------------------------------------------
1 | //
2 | // DiamondWave.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A transition where many diamonds grow upwards to reveal the new content,
12 | /// with the diamonds moving outwards from the top-left edge.
13 | ///
14 | /// This works works identically to the diamond transition, except it factors in the
15 | /// X and Y coordinate of the current pixel. This means pixels in the top-left
16 | /// of the source layer will transition first, because their UV positions are closer
17 | /// to (0, 0).
18 | ///
19 | /// - Parameter position: The user-space coordinate of the current pixel.
20 | /// - Parameter color: The current color of the pixel.
21 | /// - Parameter size: The size of the whole image, in user-space.
22 | /// - Parameter amount: The progress of the transition, from 0 to 1.
23 | /// - Parameter circleSize: How big to make the diamonds.
24 | /// - Returns: The new pixel color.
25 | [[ stitchable ]] half4 diamondWaveTransition(float2 position, half4 color, float2 size, float amount, float diamondSize) {
26 | // Calculate our coordinate in UV space, 0 to 1.
27 | half2 uv = half2(position / size);
28 |
29 | // Figure out our position relative to the nearest
30 | // diamond.
31 | half2 f = half2(fract(position / diamondSize));
32 |
33 | // Calculate the Manhattan distance from our pixel to
34 | // the center of the nearest diamond.
35 | half d = abs(f.x - 0.5h) + abs(f.y - 0.5h);
36 |
37 | // If the transition has progressed beyond our distance,
38 | // factoring in our X/Y UV coordinate…
39 | if (d + uv.x + uv.y < amount * 3.0) {
40 | // Send back the color
41 | return color;
42 | } else {
43 | // Otherwise send back clear.
44 | return half4(0.0h);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ShaderPreviews/TouchTransformationPreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TouchTransformationPreview.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// An intermediate view that resolves to either absolute or relative
11 | /// touch transformation previews, depending on the shader.
12 | struct TouchTransformationPreview: View {
13 | /// The opacity of our preview view, so users can check how fading works.
14 | @State private var opacity = 1.0
15 |
16 | /// The input value to control shader strength or similar.
17 | @State private var sliderValue = 1.0
18 |
19 | /// The shader we're rendering.
20 | var shader: TouchTransformationShader
21 |
22 | var body: some View {
23 | VStack {
24 | if shader.usesSize {
25 | AbsoluteTouchTransformationPreview(opacity: $opacity, value: $sliderValue, shader: shader)
26 | } else {
27 | RelativeTouchTransformationPreview(opacity: $opacity, value: $sliderValue, shader: shader)
28 | }
29 |
30 | LabeledContent("Value: ") {
31 | Slider(value: $sliderValue, in: shader.valueRange ?? 0...1)
32 | }
33 | .frame(maxWidth: 500)
34 | .opacity(shader.valueRange != nil ? 1 : 0)
35 | .onAppear {
36 | // If we have a value range for this shader,
37 | // assume a default value of the middle of
38 | // that range.
39 | if let valueRange = shader.valueRange {
40 | sliderValue = (valueRange.upperBound - valueRange.lowerBound) / 2
41 | }
42 | }
43 |
44 | Text("Drag around on the image to try the shader.")
45 | }
46 | .toolbar {
47 | ToggleAlphaButton(opacity: $opacity)
48 | }
49 | .navigationSubtitle(shader.name)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ShaderPreviews/TransitionPreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TransitionPreview.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A view that lets users see what a given transition looks like,
11 | /// by flipping between two sample views.
12 | struct TransitionPreview: View {
13 | /// Whether we're showing the first view or the second view.
14 | @State private var showingFirstView = true
15 |
16 | /// The opacity of our preview view, so users can check how fading works.
17 | @State private var opacity = 1.0
18 |
19 | /// The shader we're rendering.
20 | var shader: TransitionShader
21 |
22 | var body: some View {
23 | VStack {
24 | if showingFirstView {
25 | Image(systemName: "figure.walk.circle")
26 | .font(.system(size: 300))
27 | .foregroundStyle(.white)
28 | .padding()
29 | .background(.blue)
30 | .opacity(opacity)
31 | .drawingGroup()
32 | .transition(shader.transition)
33 | } else {
34 | Image(systemName: "figure.run.circle")
35 | .font(.system(size: 300))
36 | .foregroundStyle(.white)
37 | .padding()
38 | .background(.indigo)
39 | .opacity(opacity)
40 | .drawingGroup()
41 | .transition(shader.transition)
42 | }
43 |
44 | Button("Toggle Views") {
45 | withAnimation(.easeIn(duration: 1.5)) {
46 | showingFirstView.toggle()
47 | }
48 | }
49 | }
50 | .toolbar {
51 | ToggleAlphaButton(opacity: $opacity)
52 | }
53 | .navigationSubtitle(shader.name)
54 | }
55 | }
56 |
57 | #Preview {
58 | TransitionPreview(shader: .example)
59 | }
60 |
--------------------------------------------------------------------------------
/Shaders/Transformation/Water.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Water.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A shader that generates a water effect.
12 | ///
13 | /// This works by pushing pixels around based on a simple algorithm: we pass
14 | /// the original coordinate, speed, and frequency into the sin() and cos() functions
15 | /// to get different numbers between -1 and 1, then multiply that by the user's
16 | /// strength parameter to see how far away we should be moved.
17 | ///
18 | /// - Parameter position: The user-space coordinate of the current pixel.
19 | /// - Parameter time: The number of elapsed seconds since the shader was created
20 | /// - Parameter size: The size of the whole image, in user-space.
21 | /// - Parameter speed: How fast to make the water ripple. Ranges from 0.5 to 10
22 | /// work best; try starting with 3.
23 | /// - Parameter strength: How pronounced the rippling effect should be.
24 | /// Ranges from 1 to 5 work best; try starting with 3.
25 | /// - Parameter frequency: How often ripples should be created. Ranges from
26 | /// 5 to 25 work best; try starting with 10.
27 | /// - Returns: The new pixel color.
28 | [[ stitchable ]] float2 water(float2 position, float2 size, float time, float speed, float strength, float frequency) {
29 | // Calculate our coordinate in UV space, 0 to 1.
30 | half2 uv = half2(position / size);
31 |
32 | // Bring both speed and strength into the kinds of
33 | // ranges we need for this effect.
34 | half adjustedSpeed = time * speed * 0.05h;
35 | half adjustedStrength = strength / 100.0h;
36 |
37 | // Offset the coordinate by a small amount in each
38 | // direction, based on wave frequency and wave strength.
39 | uv.x += sin((uv.x + adjustedSpeed) * frequency) * adjustedStrength;
40 | uv.y += cos((uv.y + adjustedSpeed) * frequency) * adjustedStrength;
41 |
42 | // Bring the position back up to user-space coordinates.
43 | return float2(uv) * size;
44 | }
45 |
--------------------------------------------------------------------------------
/Shaders/Transformation/RelativeWave.metal:
--------------------------------------------------------------------------------
1 | //
2 | // RelativeWave.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A shader that generates a wave effect, where no effect is applied on the left
12 | /// side of the input, and the full effect is applied on the right side.
13 | ///
14 | /// This works by offsetting the Y position of each pixel by some amount of its X
15 | /// position. Using sin() for this generates values between -1 and 1, but this then
16 | /// gets multiplied by 10 to increase the strength. The final amount to offset is
17 | /// multiplied by the distance from the left edge in UV space, meaning that pixels
18 | /// near the left edge are moved much less than pixels near the right edge.
19 | ///
20 | /// - Parameter position: The user-space coordinate of the current pixel.
21 | /// - Parameter time: The number of elapsed seconds since the shader was created
22 | /// - Parameter size: The size of the whole image, in user-space.
23 | /// - Parameter speed: How fast to make the waves ripple. Try starting with a value of 5.
24 | /// - Parameter smoothing: How much to smooth out the ripples, where greater values
25 | /// produce a smoother effect. Try starting with a value of 20.
26 | /// - Parameter strength: How pronounced to make the ripple effect. Try starting with a
27 | /// value of 10.
28 | /// - Returns: The new pixel color.
29 | [[ stitchable ]] float2 relativeWave(float2 position, float2 size, float time, float speed, float smoothing, float strength) {
30 | // Calculate our coordinate in UV space, 0 to 1.
31 | half2 uv = half2(position / size);
32 |
33 | // Offset our Y value by some amount of our X position.
34 | // Using time * 5 speeds up the wave, and dividing the
35 | // X position by 20 smooths out the wave to avoid jaggies.
36 | half offset = sin(time * speed + position.x / smoothing);
37 |
38 | // Apply some amount of that offset based on how far we
39 | // are from the leading edge.
40 | position.y += offset * uv.x * strength;
41 |
42 | // Send back the offset position.
43 | return position;
44 | }
45 |
--------------------------------------------------------------------------------
/Shaders/Transition/Radial.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Radial.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | #include
10 | using namespace metal;
11 |
12 | /// A transition that mimics a radial wipe.
13 | ///
14 | /// This works by calculating the angle from the center of the input view
15 | /// to this pixel, offsetting it by 90 degrees (π ÷ 2) so that our transition
16 | /// begins from the top. Once we know that angle, we can figure out whether
17 | /// this pixel lies in a part of the transition that has yet to be wiped or not.
18 | ///
19 | /// - Parameter position: The user-space coordinate of the current pixel.
20 | /// - Parameter layer: The SwiftUI layer we're reading from.
21 | /// - Parameter size: The size of the whole image, in user-space.
22 | /// - Parameter amount: The progress of the transition, from 0 to 1.
23 | /// - Returns: The new pixel color.
24 | [[stitchable]] half4 radialTransition(float2 position, SwiftUI::Layer layer, float2 size, float amount) {
25 | // Calculate our coordinate in UV space, 0 to 1.
26 | half2 uv = half2(position / size);
27 |
28 | // Get the same UV in the range -1 to 1, so that
29 | // 0 is in the center.
30 | half2 rp = uv * 2.0h - 1.0h;
31 |
32 | // Read the current color of this pixel.
33 | half4 currentColor = layer.sample(position);
34 |
35 | // Calculate the angle to this pixel, adjusted by
36 | // half π (90 degrees) so our transition starts
37 | // directly up rather than to the left.
38 | half angle = atan2(rp.y, rp.x) + M_PI_2_H;
39 |
40 | // Wrap the angle around so it's always in the
41 | // range 0...2π.
42 | if (angle < 0.0h) angle += M_PI_H * 2.0h;
43 |
44 | // Rotate clockwise rather than anti-clockwise.
45 | angle = M_PI_H * 2.0h - angle;
46 |
47 | // Calculate how far this pixel is through the transition.
48 | half progress = smoothstep(0.0h, 1.0h, angle - (half(amount) - 0.5h) * M_PI_H * 4.0h);
49 |
50 | // Send back a blend between transparent and the original
51 | // color based on the progress.
52 | return mix(0.0h, currentColor, progress);
53 | }
54 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Inferno
2 | MIT License
3 |
4 | Copyright (c) 2023 Paul Hudson and other authors.
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
24 |
25 |
26 | Many shaders were ported to Metal from elsewhere then had comments added, and
27 | some were subsequently extended to add extra functionality. The original authors
28 | and sources and linked below. All licenses are MIT. Any mistakes or performance
29 | problems introduced in the porting process are entirely my fault.
30 |
31 |
32 | Circle, Circle Wave, Diamond, Diamond Wave
33 | ---
34 | Based on: https://gl-transitions.com/editor/PolkaDotsCurtain
35 | Original author: bobylito,
36 | Metal port and enhancements: Paul Hudson
37 | License: MIT
38 |
39 |
40 | Crosswarp
41 | ---
42 | Based on: https://gl-transitions.com/editor/crosswarp
43 | Original author: Eke Péter
44 | Metal port: Paul Hudson
45 | License: MIT
46 |
47 |
48 | Radial
49 | ---
50 | Based on: https://gl-transitions.com/editor/Radial
51 | Original author: Xaychru / gre
52 | Metal port: Paul Hudson
53 | License: MIT
54 |
55 |
56 | Swirl
57 | ---
58 | Based on: https://gl-transitions.com/editor/Swirl
59 | Author: Sergey Kosarevsky / gre
60 | Metal port: Paul Hudson
61 | License: MIT
62 |
63 |
64 | Wind
65 | ---
66 | Based on: https://gl-transitions.com/editor/wind
67 | Original author: gre
68 | Metal port: Paul Hudson
69 | License: MIT
--------------------------------------------------------------------------------
/Shaders/Transformation/WhiteNoise.metal:
--------------------------------------------------------------------------------
1 | //
2 | // WhiteNoise.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A simple function that attempts to generate a random number based on various
12 | /// fixed input parameters.
13 | /// - Parameter offset: A fixed value that controls pseudorandomness.
14 | /// - Parameter position: The position of the pixel we're working with.
15 | /// - Parameter time: The number of elapsed seconds since the shader was created.
16 | /// - Returns: The original pixel color.
17 | float whiteRandom(float offset, float2 position, float time) {
18 | // Pick two numbers that are unlikely to repeat.
19 | float2 nonRepeating = float2(12.9898 * time, 78.233 * time);
20 |
21 | // Multiply our texture coordinates by the
22 | // non-repeating numbers, then add them together.
23 | float sum = dot(position, nonRepeating);
24 |
25 | // Calculate the sine of our sum to get a range
26 | // between -1 and 1.
27 | float sine = sin(sum);
28 |
29 | // Multiply the sine by a big, non-repeating number
30 | // so that even a small change will result in a big
31 | // color jump.
32 | float hugeNumber = sine * 43758.5453 * offset;
33 |
34 | // Send back just the numbers after the decimal point.
35 | return fract(hugeNumber);
36 | }
37 |
38 | /// A shader that generates dynamic, grayscale noise.
39 | ///
40 | /// This works identically to the Rainbow Noise shader, except it uses grayscale
41 | /// rather than rainbow colors.
42 | ///
43 | /// - Parameter position: The user-space coordinate of the current pixel.
44 | /// - Parameter color: The current color of the pixel.
45 | /// - Parameter time: The number of elapsed seconds since the shader was created
46 | /// - Returns: The new pixel color.
47 | [[ stitchable ]] half4 whiteNoise(float2 position, half4 color, float time) {
48 | // If it's not transparent…
49 | if (color.a > 0.0h) {
50 | // Make a color where the RGB values are the same
51 | // random number and A is 1; multiply by the
52 | // original alpha to get smooth edges.
53 | return half4(half3(whiteRandom(1.0, position, time)), 1.0h) * color.a;
54 | } else {
55 | // Use the current (transparent) color.
56 | return color;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ShaderDescriptions/TransitionShader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TransitionShader.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A shader able to act as a SwiftUI transition.
11 | struct TransitionShader: Hashable, Identifiable {
12 | /// The unique, random identifier for this shader, so we can show these
13 | /// things in a loop.
14 | var id = UUID()
15 |
16 | /// The human-readable name for this shader. This must work with the
17 | /// String-ShaderName extension so the name matches the underlying
18 | /// Metal shader function name.
19 | var name: String
20 |
21 | /// the SwiftUI transition to use for this shader.
22 | var transition: AnyTransition
23 |
24 | /// We need a custom equatable conformance to compare only the IDs, because
25 | /// the `transition` property blocks the synthesized conformance.
26 | static func ==(lhs: TransitionShader, rhs: TransitionShader) -> Bool {
27 | lhs.id == rhs.id
28 | }
29 |
30 | /// We need a custom hashable conformance to compare only the IDs, because
31 | /// the `transition` property blocks the synthesized conformance.
32 | func hash(into hasher: inout Hasher) {
33 | hasher.combine(id)
34 | }
35 |
36 | /// An example shader used for Xcode previews.
37 | static let example = shaders[0]
38 |
39 | /// All the transition shaders we want to show.
40 | static let shaders = [
41 | TransitionShader(name: "Circle", transition: .circles(size: 20)),
42 | TransitionShader(name: "Circle Wave", transition: .circleWave(size: 20)),
43 | TransitionShader(name: "Crosswarp (→)", transition: .crosswarpLTR),
44 | TransitionShader(name: "Crosswarp (←)", transition: .crosswarpRTL),
45 | TransitionShader(name: "Diamond", transition: .diamonds(size: 20)),
46 | TransitionShader(name: "Diamond Wave", transition: .diamondWave(size: 20)),
47 | TransitionShader(name: "Pixellate", transition: .pixellate()),
48 | TransitionShader(name: "Radial", transition: .radial),
49 | TransitionShader(name: "Swirl", transition: .swirl(radius: 0.5)),
50 | TransitionShader(name: "Wind", transition: .wind(size: 0.1)),
51 | TransitionShader(name: "Shift", transition: .shift()),
52 | TransitionShader(name: "Genie", transition: .genie())
53 | ]
54 | }
55 |
--------------------------------------------------------------------------------
/Shaders/Transformation/Emboss.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Emboss.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | #include
10 | using namespace metal;
11 |
12 | /// A shader that creates an embossing effect by adding brightness from pixels in one
13 | /// direction, and subtracting brightness from pixels in the other direction.
14 | ///
15 | /// This works in several steps. First, we create our base new color using the
16 | /// pixel's existing color.
17 |
18 | /// Second, we read values diagonally up and to the right, then down and to the
19 | /// left, to see what's nearby, and add or subtract them from our color. How much
20 | /// we add our subtract depends on the strength the user provided.
21 |
22 | /// If you're not sure how this works, imagine a pixel on the top edge of a view.
23 | /// Above it has nothing, so nothing gets added to the base color. Below it has a
24 | /// pixel of the same color, so that color gets subtracted from the base color to
25 | /// make it black. The same is true in reverse of pixels on the bottom edge: they
26 | /// have nothing below so nothing is subtracted, but they have a pixel above so
27 | /// that gets added, making it a bright color.
28 | ///
29 | /// As for pixels in the middle, they'll get embossed based on the pixels either side
30 | /// of them. If a red pixel is surrounded by a sea of other red pixels, then red will
31 | /// get added from above then subtracted in equal measure from below, so the
32 | /// final color will be the original.
33 | ///
34 | /// - Parameter position: The user-space coordinate of the current pixel.
35 | /// - Parameter layer: The SwiftUI layer we're reading from.
36 | /// - Parameter strength: How far we should we read pixels.
37 | /// - Returns: The new pixel color.
38 | [[ stitchable ]] half4 emboss(float2 position, SwiftUI::Layer layer, float strength) {
39 | // Read the current pixel color.
40 | half4 currentColor = layer.sample(position);
41 |
42 | // Take a copy of that, so we can modify it safely.
43 | half4 newColor = currentColor;
44 |
45 | // Add an offset pixel, adding more of it based on strength
46 | newColor += layer.sample(position + 1.0) * strength;
47 |
48 | // Do the opposite in the other direction.
49 | newColor -= layer.sample(position - 1.0) * strength;
50 |
51 | // Send back the resulting color, factoring in the
52 | // original alpha to get smooth edges.
53 | return half4(newColor) * currentColor.a;
54 | }
55 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ShaderPreviews/SimpleTransformationPreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SimpleTransformationPreview.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// Renders a simple shader, optionally provide a replacement color
11 | /// and also a customizable input value.
12 | struct SimpleTransformationPreview: View {
13 | /// The replacement color to send to the shader.
14 | @State private var newColor = Color.red
15 |
16 | /// The input value to control shader strength or similar.
17 | @State private var sliderValue = 1.0
18 |
19 | /// The opacity of our preview view, so users can check how fading works.
20 | @State private var opacity = 1.0
21 |
22 | /// The shader we're rendering.
23 | var shader: SimpleTransformationShader
24 |
25 | var body: some View {
26 | VStack {
27 | ContentPreviewSelector()
28 |
29 | if shader.type == .colorEffect {
30 | ContentPreview()
31 | .opacity(opacity)
32 | .colorEffect(
33 | shader.createShader(color: newColor, value: sliderValue)
34 | )
35 | } else {
36 | ContentPreview()
37 | .opacity(opacity)
38 | .visualEffect { content, proxy in
39 | content.layerEffect(
40 | shader.createShader(color: newColor, value: sliderValue, size: proxy.size), maxSampleOffset: .zero)
41 | }
42 | }
43 |
44 | ColorPicker("Replacement Color", selection: $newColor)
45 | .opacity(shader.usesReplacementColor ? 1 : 0)
46 |
47 | LabeledContent("Value: ") {
48 | Slider(value: $sliderValue, in: shader.valueRange ?? 0...1)
49 | }
50 | .frame(maxWidth: 500)
51 | .opacity(shader.valueRange != nil ? 1 : 0)
52 | .onAppear {
53 | // If we have a value range for this shader,
54 | // assume a default value of the middle of
55 | // that range.
56 | if let valueRange = shader.valueRange {
57 | sliderValue = (valueRange.upperBound - valueRange.lowerBound) / 2
58 | }
59 | }
60 | }
61 | .toolbar {
62 | ToggleAlphaButton(opacity: $opacity)
63 | }
64 | .navigationSubtitle(shader.name)
65 | }
66 | }
67 |
68 | #Preview {
69 | SimpleTransformationPreview(shader: .example)
70 | }
71 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ShaderDescriptions/GenerativeShader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GenerativeShader.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A shader that generates its contents fully from scratch.
11 | struct GenerativeShader: Hashable, Identifiable {
12 | /// The unique, random identifier for this shader, so we can show these
13 | /// things in a loop.
14 | var id = UUID()
15 |
16 | /// The human-readable name for this shader. This must work with the
17 | /// String-ShaderName extension so the name matches the underlying
18 | /// Metal shader function name.
19 | var name: String
20 |
21 | /// Some shaders need completely custom initialization, so this is effectively
22 | /// a trap door to allow that to happen rather than squeeze all sorts of
23 | /// special casing into the code.
24 | var initializer: ((_ time: Double, _ size: CGSize) -> Shader)?
25 |
26 | /// We need a custom equatable conformance to compare only the IDs, because
27 | /// the `initializer` property blocks the synthesized conformance.
28 | static func ==(lhs: GenerativeShader, rhs: GenerativeShader) -> Bool {
29 | lhs.id == rhs.id
30 | }
31 |
32 | /// We need a custom hashable conformance to compare only the IDs, because
33 | /// the `initializer` property blocks the synthesized conformance.
34 | func hash(into hasher: inout Hasher) {
35 | hasher.combine(id)
36 | }
37 |
38 | /// Converts this shader to its Metal shader by resolving its name.
39 | func createShader(elapsedTime: Double, size: CGSize) -> Shader {
40 | if let initializer {
41 | return initializer(elapsedTime, size)
42 | } else {
43 | let shader = ShaderLibrary[dynamicMember: name.shaderName]
44 | return shader(
45 | .float2(size),
46 | .float(elapsedTime)
47 | )
48 | }
49 | }
50 |
51 | /// An example shader used for Xcode previews.
52 | static let example = shaders[0]
53 |
54 | /// All the generative shaders we want to show.
55 | static let shaders = [
56 | GenerativeShader(name: "Light Grid") { time, size in
57 | let shader = ShaderLibrary[dynamicMember: "lightGrid"]
58 |
59 | return shader(
60 | .float2(size),
61 | .float(time),
62 | .float(8),
63 | .float(3),
64 | .float(1),
65 | .float(3)
66 | )
67 | },
68 | GenerativeShader(name: "Sinebow")
69 | ]
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/Shaders/Transformation/Infrared.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Infrared.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 | #include
8 | using namespace metal;
9 |
10 | /// A shader that creates simulated infrared colors by replacing bright colors with
11 | /// blends of red and yellow, and darker colors with blends of yellow and blue.
12 | ///
13 | /// This works by calculating the brightness of the current color, then creating a new
14 | /// color based on that brightness. If the brightness is lower than 0.5 on a scale of
15 | /// 0 to 1, the new color is a mix of blue and yellow based; if the brightness is 0.5 or
16 | /// higher, the new color is a mix of yellow and red.
17 | ///
18 | /// - Parameter position: The user-space coordinate of the current pixel.
19 | /// - Parameter color: The current color of the pixel.
20 | /// - Returns: The new pixel color.
21 | [[ stitchable ]] half4 infrared(float2 position, half4 color) {
22 | // If it's not transparent…
23 | if (color.a > 0) {
24 | // Create three colors: blue (cold), yellow (medium), and hot (red).
25 | half3 cold = half3(0.0h, 0.0h, 1.0h);
26 | half3 medium = half3(1.0h, 1.0h, 0.0h);
27 | half3 hot = half3(1.0h, 0.0h, 0.0h);
28 |
29 | // These values correspond to how important each
30 | // color is to the overall brightness. The total
31 | // is 1.
32 | half3 grayValues = half3(0.2125h, 0.7154h, 0.0721h);
33 |
34 | // The dot() function multiplies all the colors
35 | // in our source color with all the values in
36 | // our grayValues conversion then sums them.
37 | // This means that `luma` will be a number
38 | // between 0 and 1, based on the input RGB
39 | // values and their relative brightnesses.
40 | half luma = dot(color.rgb, grayValues);
41 |
42 | // Declare the color we're going to use.
43 | half3 newColor;
44 |
45 | // If we have brightness of lower than 0.5…
46 | if (luma < 0.5h) {
47 | // Create a mix of blue and yellow; luma / 0.5 means this will be a range from 0 to 1.
48 | newColor = mix(cold, medium, luma / 0.5h);
49 | } else {
50 | // Create a mix of yellow and red; (luma - 0.5) / 0.5 means this will be a range of 0 to 1.
51 | newColor = mix(medium, hot, (luma - 0.5h) / 0.5h);
52 | }
53 |
54 | // Create the final color, multiplying by this
55 | // pixel's alpha (to avoid a hard edge).
56 | return half4(newColor, 1.0h) * color.a;
57 | } else {
58 | // Use the current (transparent) color.
59 | return color;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ShaderPreviews/ProgressiveBlurPreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProgressiveBlurPreview.swift
3 | // Inferno
4 | //
5 | // Created by Dale Price on 11/28/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A SwiftUI view that renders a gradient blur using the VariableGaussianBlur Metal shader.
11 | struct ProgressiveBlurPreview: View {
12 | /// A binding to the opacity set in the parent view.
13 | @Binding var opacity: Double
14 |
15 | /// The start position of the variable blur, from 0 (top) to 1 (bottom).
16 | @State private var start = 0.0
17 |
18 | /// The start position of the variable blur, from 0 (top) to 1 (bottom).
19 | @State private var end = 1.0
20 |
21 | /// The blur radius.
22 | @State private var radius = 10.0
23 |
24 | /// The maximum number of samples to use for the blur.
25 | @State private var maxSamples = 15.0
26 |
27 | var body: some View {
28 | VStack {
29 | ContentPreviewSelector()
30 |
31 | ContentPreview()
32 | .variableBlur(radius: radius, maxSampleCount: Int(maxSamples)) { geometryProxy, context in
33 | // Draw a rectangle covering the entire mask and fill it using a linear gradient from the specified points within the view's frame.
34 | context.fill(
35 | Path(geometryProxy.frame(in: .local)),
36 | with: .linearGradient(
37 | .init(colors: [.white, .clear]),
38 | startPoint: .init(x: 0, y: geometryProxy.size.height * start),
39 | endPoint: .init(x: 0, y: geometryProxy.size.height * end)
40 | )
41 | )
42 | }
43 | // We need to give the view an ID based on the parameters used within the drawing callback so that SwiftUI will call it again when they change.
44 | .id(start)
45 | .id(end)
46 | .opacity(opacity)
47 |
48 | GroupBox {
49 | LabeledContent("Blur Radius") {
50 | Slider(value: $radius, in: 0.0...100.0)
51 | }
52 | LabeledContent("Blur Mask Start") {
53 | Slider(value: $start, in: 0.0...1.0)
54 | }
55 | LabeledContent("Blur Mask End") {
56 | Slider(value: $end, in: 0.0...1.0)
57 | }
58 | LabeledContent("Maximum Sample Count") {
59 | Slider(value: $maxSamples, in: 1.0...30.0, step: 1.0)
60 | }
61 | }
62 | .scenePadding()
63 | .frame(maxWidth: 500)
64 | }
65 | }
66 | }
67 |
68 | #Preview {
69 | ProgressiveBlurPreview(opacity: .constant(1))
70 | }
71 |
--------------------------------------------------------------------------------
/Shaders/Transformation/SimpleLoupe.metal:
--------------------------------------------------------------------------------
1 | //
2 | // SimpleLoupe.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | #include
10 | using namespace metal;
11 |
12 | /// A shader that creates a circular zoom effect over a precise location.
13 | ///
14 | /// This works by calculating how far the user's touch is from the current pixel,
15 | /// and, if it is within a certain distance, causing it to be zoomed. The default zoom
16 | /// is 1, meaning that 1 pixel should be drawn in the space allocated to 1 pixel. If the
17 | /// zoom goes down to 0.5, it means 0.5 pixels should be drawn in the space allocated
18 | /// to 1 pixel, causing it to be stretched.
19 | ///
20 | /// - Parameter position: The user-space coordinate of the current pixel.
21 | /// - Parameter layer: The SwiftUI layer we're reading from.
22 | /// - Parameter size: The size of the whole image, in user-space.
23 | /// - Parameter touch: The location the user is touching, where the zoom should be centered.
24 | /// - Parameter maxDistance: How large the loupe zoom area should be.
25 | /// Try starting with 0.05.
26 | /// - Parameter zoomFactor: How much to zoom the contents of the loupe.
27 | /// Try starting with 2.
28 | /// - Returns: The new pixel color.
29 | [[ stitchable ]] half4 simpleLoupe(float2 position, SwiftUI::Layer layer, float2 size, float2 touch, float maxDistance, float zoomFactor) {
30 | // Calculate our coordinate in UV space, 0 to 1.
31 | half2 uv = half2(position / size);
32 |
33 | // Figure out where the user's touch is in UV space.
34 | half2 center = half2(touch / size);
35 |
36 | // Calculate how far this pixel is from the touch.
37 | half2 delta = uv - center;
38 |
39 | // Make sure we can create a round loupe even in views
40 | // that aren't square.
41 | half aspectRatio = size.x / size.y;
42 |
43 | // Figure out the squared Euclidean distance from
44 | // this pixel to the touch, factoring in aspect ratio.
45 | half distance = (delta.x * delta.x) + (delta.y * delta.y) / aspectRatio;
46 |
47 | // Show 1 pixel in the space by default.
48 | half totalZoom = 1.0h;
49 |
50 | // If we're inside the loupe area…
51 | if (distance < maxDistance) {
52 | // Halve the number of pixels we're showing – stretch
53 | // the pixels upwards to fill the same space.
54 | totalZoom /= zoomFactor;
55 | }
56 |
57 | // Calculate the new pixel to read by applying that zoom
58 | // to the distance from the pixel to the touch, then
59 | // offsetting it back to the center.
60 | half2 newPosition = delta * totalZoom + center;
61 |
62 | // Sample and return that color, taking the position
63 | // back to user space.
64 | return layer.sample(float2(newPosition) * size);
65 | }
66 |
--------------------------------------------------------------------------------
/Shaders/Transition/Wind.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Wind.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | #include
10 | using namespace metal;
11 |
12 | /// A transition that causes images to be removed in random streaks flowing
13 | /// from right to left.
14 | ///
15 | /// This works by generating a pseudorandom value for each pixel based
16 | /// on its Y position. This determines how the base progress of the wind effect
17 | /// for each pixel. In order to create the actual effect, we move each vertical
18 | /// line from "fully on the screen" to "fully off the screen" based on the transition
19 | /// progress, using the pseudorandom value as an offset to create a jagged edge.
20 | /// So, although all parts of the transition move at the same speed, they at least
21 | /// have different X positions.
22 | ///
23 | /// - Parameter position: The user-space coordinate of the current pixel.
24 | /// - Parameter layer: The SwiftUI layer we're reading from.
25 | /// - Parameter size: The size of the whole image, in user-space.
26 | /// - Parameter amount: The progress of the transition, from 0 to 1.
27 | /// - Parameter windSize: How big the wind streaks should be, relative to the view's width.
28 | /// - Returns: The new pixel color.
29 | [[stitchable]] half4 windTransition(float2 position, SwiftUI::Layer layer, float2 size, float amount, float windSize = 0.2) {
30 | // Calculate our coordinate in UV space, 0 to 1.
31 | half2 uv = half2(position / size);
32 |
33 | // These numbers are commonly used to create
34 | // seemingly random values from non-random inputs,
35 | // in our case the Y coordinate of the current pixel.
36 | half random = fract(sin(dot(half2(0.0h, uv.y), half2(12.9898h, 78.233h))) * 43758.5453h);
37 |
38 | // Adjust the horizontal UV coordinate with the wind effect.
39 | // This step scales uv.x to the range of 0 to 1 - windSize
40 | // and then shifts it by windSize, so we start with no movement
41 | // but finish fully off the screen.
42 | half adjustedX = uv.x * (1.0h - windSize) + windSize * random;
43 |
44 | // Calculate the offset for the transition based on the amount.
45 | // This moves the transition effect across the screen, taking
46 | // into account the need to overshoot fully based on the size
47 | // of the wind effect.
48 | half transitionOffset = amount * (1.0h + windSize);
49 |
50 | // Calculate the transition progress for each pixel.
51 | // The smoothstep() function smooths the transition between
52 | // 0 and -windSize, creating a more gradual effect as the
53 | // transition moves.
54 | half progress = smoothstep(0.0h, half(-windSize), adjustedX - transitionOffset);
55 |
56 | // Blend the original color with the transparent color
57 | // based on the value of progress.
58 | return mix(layer.sample(position), 0.0h, progress);
59 | }
60 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ShaderPreviews/TimeTransformationPreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimeTransformationPreview.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// Renders a shader that requires a TimelineView to animate itself.
11 | struct TimeTransformationPreview: View {
12 | /// The initial time this view was created, so we can send
13 | /// elapsed time to the shader.
14 | @State private var startTime = Date.now
15 |
16 | /// The opacity of our preview view, so users can check how fading works.
17 | @State private var opacity = 1.0
18 |
19 | /// The shader we're rendering.
20 | var shader: TimeTransformationShader
21 |
22 | var body: some View {
23 | VStack {
24 | ContentPreviewSelector()
25 |
26 | TimelineView(.animation) { timeline in
27 | let elapsed = startTime.distance(to: timeline.date)
28 |
29 | switch shader.type {
30 | case .colorEffect:
31 | ContentPreview()
32 | .opacity(opacity)
33 | .colorEffect(
34 | shader.createShader(elapsedTime: elapsed)
35 | )
36 |
37 | case .distortionEffect:
38 | ContentPreview()
39 | .opacity(opacity)
40 | .font(.system(size: 300))
41 | .foregroundStyle(.white)
42 | .distortionEffect(
43 | shader.createShader(elapsedTime: elapsed),
44 | maxSampleOffset: .zero
45 | )
46 |
47 | case .visualEffectColor:
48 | ContentPreview()
49 | .opacity(opacity)
50 | .visualEffect { content, proxy in
51 | content
52 | .colorEffect(
53 | shader.createShader(elapsedTime: elapsed, size: proxy.size)
54 | )
55 | }
56 |
57 | case .visualEffectDistortion:
58 | ContentPreview()
59 | .opacity(opacity)
60 | .visualEffect { content, proxy in
61 | content
62 | .distortionEffect(
63 | shader.createShader(elapsedTime: elapsed, size: proxy.size),
64 | maxSampleOffset: .zero
65 | )
66 | }
67 | }
68 | }
69 | }
70 | .toolbar {
71 | ToggleAlphaButton(opacity: $opacity)
72 | }
73 | .navigationSubtitle(shader.name)
74 | }
75 | }
76 |
77 | #Preview {
78 | TimeTransformationPreview(shader: .example)
79 | }
80 |
--------------------------------------------------------------------------------
/Shaders/Transformation/RainbowNoise.metal:
--------------------------------------------------------------------------------
1 | //
2 | // RainbowNoise.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A simple function that attempts to generate a random number based on various
12 | /// fixed input parameters.
13 | ///
14 | /// This works using a simple (but brilliant!) and well-known trick: if you calculate the
15 | /// dot product of a coordinate with a float2 containing two numbers that are unlikely
16 | /// to repeat, then calculate the sine of that and multiply it by a large number, you'll
17 | /// end up with what looks more or less like random numbers in the fraction
18 | /// digits – i.e., everything after the decimal place.
19 | ///
20 | /// This is perfect for our needs: those numbers will already range from 0 to
21 | /// 0.99999... so we can use that for our color value by calling it once for each
22 | /// of the RGB components.
23 | ///
24 | /// - Parameter offset: A fixed value that controls pseudorandomness.
25 | /// - Parameter position: The position of the pixel we're working with.
26 | /// - Parameter time: The number of elapsed seconds since the shader was created.
27 | /// - Returns: The original pixel color.
28 | float rainbowRandom(float offset, float2 position, float time) {
29 | // Pick two numbers that are unlikely to repeat.
30 | float2 nonRepeating = float2(12.9898 * time, 78.233 * time);
31 |
32 | // Multiply our texture coordinates by the
33 | // non-repeating numbers, then add them together.
34 | float sum = dot(position, nonRepeating);
35 |
36 | // calculate the sine of our sum to get a range
37 | // between -1 and 1.
38 | float sine = sin(sum);
39 |
40 | // Multiply the sine by a big, non-repeating number
41 | // so that even a small change will result in a big
42 | // color jump.
43 | float hugeNumber = sine * 43758.5453 * offset;
44 |
45 | // Send back just the numbers after the decimal point.
46 | return fract(hugeNumber);
47 | }
48 |
49 |
50 | /// A shader that generates dynamic, multi-colored noise.
51 | /// - Parameter position: The user-space coordinate of the current pixel.
52 | /// - Parameter color: The current color of the pixel.
53 | /// - Parameter time: The number of elapsed seconds since the shader was created
54 | /// - Returns: The new pixel color.
55 | [[ stitchable ]] half4 rainbowNoise(float2 position, half4 color, float time) {
56 | // If it's not transparent…
57 | if (color.a > 0.0h) {
58 | // Make a color where the RGB values are the same
59 | // random number and A is 1; multiply by the
60 | // original alpha to get smooth edges.
61 | return half4(
62 | rainbowRandom(1.23, position, time),
63 | rainbowRandom(5.67, position, time),
64 | rainbowRandom(8.90, position, time),
65 | 1.0h
66 | ) * color.a;
67 | } else {
68 | // Use the current (transparent) color.
69 | return color;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/SwiftUI Wrappers/VisualEffect+variableBlur.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VisualEffect+variableBlur.swift
3 | // Inferno
4 | //
5 | // Created by Dale Price on 11/28/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @available(iOS 17, macOS 14, macCatalyst 17, tvOS 17, visionOS 1, *)
11 | public extension VisualEffect {
12 |
13 | /// Applies a variable blur, with the blur radius at each pixel determined by a mask image.
14 | ///
15 | /// - Tip: To automatically generate a mask image of the same size as the view, use ``SwiftUI/View/variableBlur(radius:maxSampleCount:verticalPassFirst:maskRenderer:)`` which creates the image from drawing instructions you provide to a `GraphicsContext`.
16 | ///
17 | /// - Parameters:
18 | /// - radius: The maximum radial size of the blur in areas where the mask is fully opaque.
19 | /// - maxSampleCount: The maximum number of samples to take from the view's layer in each direction. Higher numbers produce a smoother, higher quality blur but are more GPU intensive. Values larger than `radius` have no effect.
20 | /// - verticalPassFirst: Whether or not to perform the vertical blur pass before the horizontal one. Changing this parameter may reduce smearing artifacts. Defaults to `false`, i.e. perform the horizontal pass first.
21 | /// - mask: An image with an alpha channel to use as mask to determine the strength of the blur effect at each pixel. Fully transparent areas are unblurred; fully opaque areas are blurred by the full radius; partially transparent areas are blurred by the radius multiplied by the alpha value. The mask will be uv-mapped to cover the entire view.
22 | /// - isEnabled: Whether the effect is enabled or not.
23 | /// - Returns: A new view that renders `self` with the blur shader applied as a layer effect.
24 | ///
25 | /// - Important: Because this effect is based on SwiftUI's `layerEffect`, views backed by AppKit or UIKit views may not render. Instead, they log a warning and display a placeholder image to highlight the error.
26 | func variableBlur(
27 | radius: CGFloat,
28 | maxSampleCount: Int = 15,
29 | verticalPassFirst: Bool = false,
30 | mask: Image,
31 | isEnabled: Bool = true
32 | ) -> some VisualEffect {
33 | self.layerEffect(
34 | ShaderLibrary.variableBlur(
35 | .boundingRect,
36 | .float(radius),
37 | .float(CGFloat(maxSampleCount)),
38 | .image(mask),
39 | .float(verticalPassFirst ? 1 : 0)
40 | ),
41 | maxSampleOffset: CGSize(width: radius , height: radius),
42 | isEnabled: isEnabled
43 | )
44 | .layerEffect(
45 | ShaderLibrary.variableBlur(
46 | .boundingRect,
47 | .float(radius),
48 | .float(CGFloat(maxSampleCount)),
49 | .image(mask),
50 | .float(verticalPassFirst ? 0 : 1)
51 | ),
52 | maxSampleOffset: CGSize(width: radius, height: radius),
53 | isEnabled: isEnabled
54 | )
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Shaders/Transformation/WarpingLoupe.metal:
--------------------------------------------------------------------------------
1 | //
2 | // WarpingLoupe.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | #include
10 | using namespace metal;
11 |
12 | /// A shader that creates a circular zoom effect over a precise location, with
13 | /// variable zoom around the touch area to create a glass orb-like effect.
14 | ///
15 | /// This works identically to the simple loupe shader, except that we add back
16 | /// to the zoom some amount of our distance. This means that pixels directly under
17 | /// the user's finger have 0.5 zoom (e.g. half a pixel being stretched to fit the space
18 | /// allocated to 1 pixel), but pixels that are increasingly far away (but still within the
19 | /// maximum distance) are stretched less.
20 | ///
21 | /// - Parameter position: The user-space coordinate of the current pixel.
22 | /// - Parameter layer: The SwiftUI layer we're reading from.
23 | /// - Parameter size: The size of the whole image, in user-space.
24 | /// - Parameter touch: The location the user is touching, where the zoom should be centered.
25 | /// - Parameter maxDistance: How large the loupe zoom area should be.
26 | /// Try starting with 0.05.
27 | /// - Parameter zoomFactor: How much to zoom the contents of the loupe.
28 | /// - Returns: The new pixel color.
29 | [[ stitchable ]] half4 warpingLoupe(float2 position, SwiftUI::Layer layer, float2 size, float2 touch, float maxDistance, float zoomFactor) {
30 | // Calculate our coordinate in UV space, 0 to 1.
31 | half2 uv = half2(position / size);
32 |
33 | // Figure out where the user's touch is in UV space.
34 | half2 center = half2(touch / size);
35 |
36 | // Calculate how far this pixel is from the touch.
37 | half2 delta = uv - center;
38 |
39 | // Make sure we can create a round loupe even in views
40 | // that aren't square.
41 | half aspectRatio = size.x / size.y;
42 |
43 | // Figure out the squared Euclidean distance from
44 | // this pixel to the touch, factoring in aspect ratio.
45 | half distance = (delta.x * delta.x) + (delta.y * delta.y) / aspectRatio;
46 |
47 | // Show 1 pixel in the space by default.
48 | half totalZoom = 1.0h;
49 |
50 | // If we're inside the loupe area…
51 | if (distance < maxDistance) {
52 | // Halve the number of pixels we're showing – stretch
53 | // the pixels upwards to fill the same space.
54 | totalZoom /= zoomFactor;
55 |
56 | // Add back to the zoom some amount of the distance,
57 | // causing the zoom effect to lessen as pixels are
58 | // further from the touch point.
59 | float zoomAdjustment = smoothstep(0.0h, half(maxDistance), distance);
60 | totalZoom += zoomAdjustment / 2.0h;
61 | }
62 |
63 | // Calculate the new pixel to read by applying that zoom
64 | // to the distance from the pixel to the touch, then
65 | // offsetting it back to the center.
66 | half2 newPosition = delta * totalZoom + center;
67 |
68 | // Sample and return that color, taking the position
69 | // back to user space.
70 | return layer.sample(float2(newPosition) * size);
71 | }
72 |
73 |
74 |
--------------------------------------------------------------------------------
/Shaders/Transition/Swirl.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Swirl.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | #include
10 | using namespace metal;
11 |
12 | /// A transition that causes images to swirl like a vortex as they fade out.
13 | ///
14 | /// This starts by calculating how far the current pixel is from the center of
15 | /// the view, which means we apply the effect at varying strengths so that
16 | /// the swirl is stronger at the center than it is further away.
17 | ///
18 | /// Once we have that, we decide whether we're swirling up or down. This
19 | /// is because transitions moves through three states: full opaque and unswirled,
20 | /// half visible and fully swirled, then fully transparent and unswirled.
21 | ///
22 | /// Finally we need to figure out what to pixel value to send back. This is done
23 | /// by rotating the original coordinate by some amount based on where
24 | /// we are in the swirl (with values closer to the center being swirled less)
25 | /// and how through the transition is.
26 | ///
27 | /// - Parameter position: The user-space coordinate of the current pixel.
28 | /// - Parameter layer: The SwiftUI layer we're reading from.
29 | /// - Parameter size: The size of the whole image, in user-space.
30 | /// - Parameter amount: The progress of the transition, from 0 to 1.
31 | /// - Parameter radius: How large the swirl should be relative to the view it's transitioning.
32 | /// - Returns: The new pixel color.
33 | [[stitchable]] half4 swirl(float2 position, SwiftUI::Layer layer, float2 size, float amount, float radius) {
34 | // Calculate our coordinate in UV space, -0.5 to 0.5.
35 | half2 uv = half2(position / size);
36 | uv -= 0.5h;
37 |
38 | // Calculate the distance from the center to the current point.
39 | half distanceFromCenter = length(uv);
40 |
41 | // Only apply the swirl effect within the specified radius.
42 | if (distanceFromCenter < radius) {
43 | // Calculate the swirl effect's strength based on distance from center.
44 | half swirlStrength = (radius - distanceFromCenter) / radius;
45 |
46 | // Determine the amount of swirl.
47 | // If amount is less than 0.5, interpolate from
48 | // 0 to 1; otherwise, interpolate from 1 back to 0.
49 | half swirlAmount;
50 |
51 | if (amount <= 0.5) {
52 | swirlAmount = mix(0.0h, 1.0h, half(amount) / 0.5h);
53 | } else {
54 | swirlAmount = mix(1.0h, 0.0h, (half(amount) - 0.5h) / 0.5h);
55 | }
56 |
57 | // Calculate the swirl angle based on the swirl strength and amount.
58 | half swirlAngle = swirlStrength * swirlStrength * swirlAmount * 8.0h * M_PI_H;
59 |
60 | // Compute sine and cosine for the rotation.
61 | half sinAngle = sin(swirlAngle);
62 | half cosAngle = cos(swirlAngle);
63 |
64 | // Rotate the UV coordinates according to the swirl angle.
65 | uv = half2(dot(uv, half2(cosAngle, -sinAngle)), dot(uv, half2(sinAngle, cosAngle)));
66 | }
67 |
68 | // Move UVs back to the range 0...1.
69 | uv += 0.5h;
70 |
71 | // Now blend the pixel at that location with the clear
72 | // color based on amount, so we fade out over time.
73 | return mix(layer.sample(float2(uv) * size), 0.0h, amount);
74 | }
75 |
--------------------------------------------------------------------------------
/Shaders/Generation/Sinebow.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Sinebow.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A shader that generates multiple twisting and turning lines that cycle through colors.
12 | ///
13 | /// This shader calculates how far each pixel is from one of 10 lines.
14 | /// Each line has its own undulating color and position based on various
15 | /// sine waves, so the pixel's color is calculating by starting from black
16 | /// and adding in a little of each line's color based on its distance.
17 | ///
18 | /// - Parameter position: The user-space coordinate of the current pixel.
19 | /// - Parameter color: The current color of the pixel.
20 | /// - Parameter size: The size of the whole image, in user-space.
21 | /// - Parameter time: The number of elapsed seconds since the shader was created
22 | /// - Returns: The new pixel color.
23 | [[ stitchable ]] half4 sinebow(float2 position, half4 color, float2 size, float time) {
24 | // Calculate our aspect ratio.
25 | half aspectRatio = size.x / size.y;
26 |
27 | // Calculate our coordinate in UV space, -1 to 1.
28 | half2 uv = half2(position / size.x) * 2.0h - 1.0h;
29 |
30 | // Make sure we can create the effect roughly equally no
31 | // matter what aspect ratio we're in.
32 | uv.x /= aspectRatio;
33 |
34 | // Calculate the overall wave movement.
35 | half wave = sin(uv.x + time);
36 |
37 | // Square that movement, and multiply by a large number
38 | // to make the peaks and troughs be nice and big.
39 | wave *= wave * 50.0h;
40 |
41 | // Assume a black color by default.
42 | half3 waveColor = half3(0.0h);
43 |
44 | // Create 10 lines in total.
45 | for (half i = 0.0h; i < 10.0h; i++) {
46 | // The base brightness of this pixel is 1%, but we
47 | // need to factor in the position after our wave
48 | // calculation is taken into account. The abs()
49 | // call ensures negative numbers become positive,
50 | // so we care about the absolute distance to the
51 | // nearest line, rather than ignoring values that
52 | // are negative.
53 | half luma = abs(1.0h / (100.0h * uv.y + wave));
54 |
55 | // This calculates a second sine wave that's unique
56 | // to each line, so we get waves inside waves.
57 | half y = sin(uv.x * sin(time) + i * 0.2h + time);
58 |
59 | // This offsets each line by that second wave amount,
60 | // so the waves move non-uniformly.
61 | uv.y += 0.05h * y;
62 |
63 | // Our final color is based on fixed red and blue
64 | // values, but green fluctuates much more so that
65 | // the overall brightness varies more randomly.
66 | // The * 0.5 + 0.5 part ensures the sin() values
67 | // are between 0 and 1 rather than -1 and 1.
68 | half3 rainbow = half3(
69 | sin(i * 0.3h + time) * 0.5h + 0.5h,
70 | sin(i * 0.3h + 2.0h + sin(time * 0.3h) * 2.0h) * 0.5h + 0.5h,
71 | sin(i * 0.3h + 4.0h) * 0.5h + 0.5h
72 | );
73 |
74 | // Add that to the current wave color, ensuring that
75 | // pixels receive some brightness from all lines.
76 | waveColor += rainbow * luma;
77 | }
78 |
79 | // Send back the finished color, taking into account the
80 | // current alpha value.
81 | return half4(waveColor, 1.0h) * color.a;
82 | }
83 |
--------------------------------------------------------------------------------
/Shaders/Transition/Pixellate.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Pixellate.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | #include
10 | using namespace metal;
11 |
12 | /// A transition that causes images to pixellate increasingly while fading out.
13 | ///
14 | /// This is an effect that sounds simple, but has a lot of complexity to it.
15 | ///
16 | /// First, we figure out where in the transition we currently are. The transition works
17 | /// by making an image start fully visible and unpixellated, move to half visible
18 | /// and fully pixellated, then move to being fully transparent and unpixellated. This
19 | /// is calculated in the `direction` value.
20 | ///
21 | /// Second, we perform quantization, which is a fancy way of saying we restrict
22 | /// the number of values we're working with. We *could* pixellate this with a smooth
23 | /// animation, and certainly that's possible when the `steps` parameter is 60 or greater.
24 | /// However, for a more retro feel we can lower the animation speed so it moves more
25 | /// jerkily – we restrict the range of animation frames from (eg) 60 down to 10.
26 | ///
27 | /// Third, we figure out which pixel we're drawing. We were given the original position
28 | /// as input, but in the pixellation effect we want to divide that by the square size
29 | /// so we get chunky blocks of color,
30 | ///
31 | /// Finally, we blend the pixel color with the transparent color as the
32 | /// transition progresses, so it fades out.
33 | ///
34 | /// - Parameter position: The user-space coordinate of the current pixel.
35 | /// - Parameter layer: The SwiftUI layer we're reading from.
36 | /// - Parameter size: The size of the whole image, in user-space.
37 | /// - Parameter amount: The progress of the transition, from 0 to 1.
38 | /// - Parameter squares: How many pixel squares to generate.
39 | /// - Parameter steps: How many animation steps we want to go through. Anything
40 | /// above 60 or so is effectively smooth animation, whereas values such as 20 will
41 | /// make a more jumpy animation.
42 | /// - Returns: The new pixel color.
43 | [[stitchable]] half4 pixellate(float2 position, SwiftUI::Layer layer, float2 size, float amount, float squares, float steps) {
44 | // Calculate our coordinate in UV space, 0 to 1.
45 | half2 uv = half2(position / size);
46 |
47 | // Determine the direction of transition and restrict it
48 | // to the 0...1 range. This will count from 0.0 to 0.5,
49 | // then back down to to 0.0 again.
50 | half direction = min(amount, 1.0 - amount);
51 |
52 | // Quantize d to create a stepping effect. So, rather
53 | // than moving smoothly between states, we move in
54 | // discrete steps based on the number of steps specified
55 | // above. This causes a rougher transition, which works
56 | // great with the pixellation style.
57 | half steppedProgress = ceil(direction * steps) / steps;
58 |
59 | // Calculate the size of each square based on steppedProgress
60 | // and the minimum number of squares.
61 | half2 squareSize = 2.0h * steppedProgress / half2(squares);
62 |
63 | // If steppedProgress is greater than 0, adjust uv to be the
64 | // center of a square. Otherwise, use uv as is.
65 | half2 newPosition;
66 |
67 | // If our stepped progress is 0…
68 | if (steppedProgress == 0.0h) {
69 | // Use the original pixel location, to avoid a divide by 0.
70 | newPosition = uv;
71 | } else {
72 | // Otherwise snap to the nearest point.
73 | newPosition = (floor(uv / squareSize) + 0.5h) * squareSize;
74 | }
75 |
76 | // Now blend the pixel at that location with the clear
77 | // color based on transition progress, so we fade out
78 | // as we pixellate.
79 | return mix(layer.sample(float2(newPosition) * size), 0.0h, amount);
80 | }
81 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ShaderPreviews/ShapeBlurPreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShapeBlurPreview.swift
3 | // Inferno
4 | //
5 | // Created by Dale Price on 11/28/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A SwiftUI view that renders a variable blur masked by a shape, using the VariableGaussianBlur Metal shader.
11 | struct ShapeBlurPreview: View {
12 | /// A binding to the opacity set in the parent view.
13 | @Binding var opacity: Double
14 |
15 | /// The shape to use as a mask for the variable blur shader.
16 | var shape: any Shape
17 |
18 | /// The amount to inset the shape relative to the view's frame, from 0 (no inset) to 0.5 (the center of the view)
19 | @State private var shapeInset = 0.25
20 |
21 | /// The amount to fade the edges of the shape in the mask, defining how the variable blur effect fades in at the edges.
22 | @State private var shapeFade = 5.0
23 |
24 | /// The blur radius.
25 | @State private var radius = 20.0
26 |
27 | /// The maximum number of samples to use for the blur.
28 | @State private var maxSamples = 15.0
29 |
30 | /// Whether to invert the mask (blur within the shape or outside the shape).
31 | @State private var invertMask = true
32 |
33 | var body: some View {
34 | VStack {
35 | ContentPreviewSelector()
36 |
37 | ContentPreview()
38 | .variableBlur(radius: radius, maxSampleCount: Int(maxSamples)) { geometryProxy, context in
39 | // Add a blur to the mask to fade the edges of the shape.
40 | context.addFilter(
41 | .blur(radius: shapeFade)
42 | )
43 |
44 | // Mask off the shape centered on the view, inset by `shapeInset` relative to the view's frame according to the proxy.
45 | let horizInset = geometryProxy.size.width * shapeInset
46 | let vertInset = geometryProxy.size.height * shapeInset
47 | context.clip(
48 | to: shape.path(in: CGRect(
49 | x: horizInset,
50 | y: vertInset,
51 | width: geometryProxy.size.width - horizInset * 2,
52 | height: geometryProxy.size.height - horizInset * 2)
53 | ), options: invertMask ? .inverse : []
54 | )
55 |
56 | // Fill the area of the mask that isn't clipped.
57 | context.fill(
58 | Path(geometryProxy.frame(in: .local)),
59 | with: .color(.white)
60 | )
61 | }
62 | .opacity(opacity)
63 | // We need to give the view an ID based on the parameters used within the drawing callback so that SwiftUI will call it again when they change.
64 | .id(shapeInset)
65 | .id(shapeFade)
66 | .id(invertMask)
67 |
68 | GroupBox {
69 | LabeledContent("Blur Radius") {
70 | Slider(value: $radius, in: 0.0...100.0)
71 | }
72 | LabeledContent("Mask Shape Inset") {
73 | Slider(value: $shapeInset, in: 0.0...0.5)
74 | }
75 | LabeledContent("Mask Shape Blur") {
76 | Slider(value: $shapeFade, in: 0.0...100.0)
77 | }
78 | Toggle("Invert Mask", isOn: $invertMask)
79 | LabeledContent("Maximum Sample Count") {
80 | Slider(value: $maxSamples, in: 1.0...30.0, step: 1.0)
81 | }
82 | }
83 | .scenePadding()
84 | .frame(maxWidth: 500)
85 | }
86 | }
87 | }
88 |
89 | #Preview {
90 | ShapeBlurPreview(opacity: .constant(1), shape: Circle())
91 | }
92 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// Shows a list of all available shaders, navigating to the various previewing
11 | /// views depending on which one is shown.
12 | struct ContentView: View {
13 | /// The currently selected list item. This is only used so that
14 | /// we automatically select WelcomeView on launch.
15 | @State private var selection: Int? = 0
16 |
17 | var body: some View {
18 | NavigationSplitView {
19 | List(selection: $selection) {
20 | NavigationLink("Home", destination: WelcomeView.init)
21 | .tag(0)
22 |
23 | Section("Simple Transformation") {
24 | ForEach(SimpleTransformationShader.shaders) { shader in
25 | NavigationLink(value: shader) {
26 | Text(shader.name)
27 | }
28 | }
29 | }
30 |
31 | Section("Animated") {
32 | ForEach(TimeTransformationShader.shaders) { shader in
33 | NavigationLink(value: shader) {
34 | Text(shader.name)
35 | }
36 | }
37 | }
38 |
39 | Section("Touchable") {
40 | ForEach(TouchTransformationShader.shaders) { shader in
41 | NavigationLink(value: shader) {
42 | Text(shader.name)
43 | }
44 | }
45 | }
46 |
47 | Section("Transitions") {
48 | ForEach(TransitionShader.shaders) { shader in
49 | NavigationLink(value: shader) {
50 | Text(shader.name)
51 | }
52 | }
53 | }
54 |
55 | Section("Generation") {
56 | ForEach(GenerativeShader.shaders) { shader in
57 | NavigationLink(value: shader) {
58 | Text(shader.name)
59 | }
60 | }
61 | }
62 |
63 | Section("Blurs") {
64 | ForEach(BlurEffect.effects) { effect in
65 | NavigationLink(value: effect) {
66 | Text(effect.name)
67 | }
68 | }
69 | }
70 | }
71 | .navigationTitle("Inferno Sandbox")
72 | .navigationDestination(for: SimpleTransformationShader.self) { shader in
73 | // SwiftUI tries to reuse views here, which means
74 | // switching between shaders doesn't trigger `onAppear()`
75 | // and so doesn't reset the value slider.
76 | SimpleTransformationPreview(shader: shader).id(UUID())
77 | }
78 | .navigationDestination(for: TimeTransformationShader.self, destination: TimeTransformationPreview.init)
79 | .navigationDestination(for: TouchTransformationShader.self) { shader in
80 | // SwiftUI tries to reuse views here, which means
81 | // switching between shaders doesn't trigger `onAppear()`
82 | // and so doesn't reset the value slider.
83 | TouchTransformationPreview(shader: shader).id(UUID())
84 | }
85 | .navigationDestination(for: TransitionShader.self) { shader in
86 | // SwiftUI tries to reuse views here, which causes
87 | // switching between transitions to behave
88 | // strangely. So, we force a random UUID every
89 | // time we change destination.
90 | TransitionPreview(shader: shader).id(UUID())
91 | }
92 | .navigationDestination(for: GenerativeShader.self, destination: GenerativePreview.init)
93 | .navigationDestination(for: BlurEffect.self, destination: BlurPreview.init)
94 | .frame(minWidth: 200)
95 | } detail: {
96 | WelcomeView()
97 | }
98 | }
99 | }
100 |
101 | #Preview {
102 | ContentView()
103 | }
104 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/SwiftUI Wrappers/View+variableBlur.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+variableBlur.swift
3 | // Inferno
4 | //
5 | // Created by Dale Price on 11/28/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @available(iOS 17, macOS 14, macCatalyst 17, tvOS 17, visionOS 1, *)
11 | public extension View {
12 |
13 | /// Applies a variable blur to the view, with the blur radius at each pixel determined by a mask image.
14 | ///
15 | /// - Parameters:
16 | /// - radius: The radial size of the blur in areas where the mask is fully opaque.
17 | /// - maxSampleCount: The maximum number of samples the shader may take from the view's layer in each direction. Higher numbers produce a smoother, higher quality blur but are more GPU intensive. Values larger than `radius` have no effect. The default of 15 provides balanced results but may cause banding on some images at larger blur radii.
18 | /// - verticalPassFirst: Whether or not to perform the vertical blur pass before the horizontal one. Changing this parameter may reduce smearing artifacts. Defaults to `false`, i.e. perform the horizontal pass first.
19 | /// - mask: An `Image` to use as the mask for the blur strength.
20 | /// - Returns: The view with the variable blur effect applied.
21 | func variableBlur(
22 | radius: CGFloat,
23 | maxSampleCount: Int = 15,
24 | verticalPassFirst: Bool = false,
25 | mask: Image
26 | ) -> some View {
27 | self.visualEffect { content, _ in
28 | content.variableBlur(
29 | radius: radius,
30 | maxSampleCount: maxSampleCount,
31 | verticalPassFirst: verticalPassFirst,
32 | mask: mask
33 | )
34 | }
35 | }
36 |
37 | /// Applies a variable blur to the view, with the blur radius at each pixel determined by a mask that you create.
38 | ///
39 | /// - Parameters:
40 | /// - radius: The radial size of the blur in areas where the mask is fully opaque.
41 | /// - maxSampleCount: The maximum number of samples the shader may take from the view's layer in each direction. Higher numbers produce a smoother, higher quality blur but are more GPU intensive. Values larger than `radius` have no effect. The default of 15 provides balanced results but may cause banding on some images at larger blur radii.
42 | /// - verticalPassFirst: Whether or not to perform the vertical blur pass before the horizontal one. Changing this parameter may reduce smearing artifacts. Defaults to `false`, i.e. perform the horizontal pass first.
43 | /// - maskRenderer: A rendering closure to draw the mask used to determine the intensity of the blur at each pixel. The closure receives a `GeometryProxy` with the view's layout information, and a `GraphicsContext` to draw into.
44 | /// - Returns: The view with the variable blur effect applied.
45 | ///
46 | /// The strength of the blur effect at any point on the view is determined by the transparency of the mask at that point. Areas where the mask is fully opaque are blurred by the full radius; areas where the mask is partially transparent are blurred by a proportionally smaller radius. Areas where the mask is fully transparent are left unblurred.
47 | ///
48 | /// - Tip: To achieve a progressive blur or gradient blur, draw a gradient from transparent to opaque in your mask image where you want the transition from clear to blurred to take place.
49 | ///
50 | /// - Note: Because the blur is split into horizontal and vertical passes for performance, certain mask images over certain patterns may cause "smearing" artifacts along one axis. Changing the `verticalPassFirst` parameter may reduce this, but may cause smearing in the other direction.. To avoid smearing entirely, avoid drawing hard edges in your `maskRenderer`.
51 | ///
52 | /// - Important: Because this effect is implemented as a SwiftUI `layerEffect`, it is subject to the same limitations. Namely, views backed by AppKit or UIKit views may not render. Instead, they log a warning and display a placeholder image to highlight the error.
53 | func variableBlur(
54 | radius: CGFloat,
55 | maxSampleCount: Int = 15,
56 | verticalPassFirst: Bool = false,
57 | maskRenderer: @escaping (GeometryProxy, inout GraphicsContext) -> Void
58 | ) -> some View {
59 | self.visualEffect { content, geometryProxy in
60 | content.variableBlur(
61 | radius: radius,
62 | maxSampleCount: maxSampleCount,
63 | verticalPassFirst: verticalPassFirst,
64 | mask: Image(size: geometryProxy.size, renderer: { context in
65 | maskRenderer(geometryProxy, &context)
66 | })
67 | )
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Shaders/Transition/Crosswarp.metal:
--------------------------------------------------------------------------------
1 | //
2 | // Crosswarp.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | #include
10 | using namespace metal;
11 |
12 | /// A transition that stretches and fades pixels starting from the right edge.
13 | ///
14 | /// This is actually two transitions paired together, so we can combine them to make
15 | /// views move in unison. Both work in roughly the same way: we figure out how
16 | /// far the current pixel is through the overall transition, using smoothstep()
17 | /// to make sure we clamp the range to 0...1, and add some gentle easing.
18 | ///
19 | /// Once we have that, we decide which pixel to read by mixing our original
20 | /// pixel position with (0.5, 0.5), the center of the image, based on the pixel
21 | /// transition amount, meaning that at amount 0 we use our original pixel location
22 | /// at amount 1 we use the center, and all intermediate values use some blend
23 | /// of the two.
24 | ///
25 | /// Now we know which pixel to sample, we mix that the transparent color
26 | /// based on the same transition amount, causing the pixels to fade out.
27 | ///
28 | /// - Parameter position: The user-space coordinate of the current pixel.
29 | /// - Parameter layer: The SwiftUI layer we're reading from.
30 | /// - Parameter size: The size of the whole image, in user-space.
31 | /// - Parameter amount: The progress of the transition, from 0 to 1.
32 | /// - Returns: The new pixel color.
33 | [[stitchable]] half4 crosswarpLTRTransition(float2 position, SwiftUI::Layer layer, float2 size, float amount) {
34 | // Calculate our coordinate in UV space, 0 to 1.
35 | half2 uv = half2(position / size);
36 |
37 | // Calculate how far this pixel is through the
38 | // transition. When amount is 0, the left edge will
39 | // be -1 and the right edge will be 0. When amount is
40 | // 0.5, the left edge will be 0, and the right edge
41 | // will be 1. When amount is 1, the left edge will be
42 | // 1, and the right edge 2.
43 | half progress = amount * 2.0h + uv.x - 1.0h;
44 |
45 | // Move smoothly between 0 and 1 with easing, making
46 | // sure to clamp to 0 and 1 at the same time.
47 | half x = smoothstep(0.0h, 1.0h, progress);
48 |
49 | // We want to read pixels increasingly close to the
50 | // center of our texture as the transition progresses.
51 | // So, we mix our original UV with (0.5, 0.5) based
52 | // on the value of x computed above.
53 | half2 newPosition = mix(uv, half2(0.5h), x);
54 |
55 | // Now blend the pixel at that location with the clear
56 | // color based on x, so we fade out over time.
57 | return mix(layer.sample(float2(newPosition) * size), 0.0h, x);
58 | }
59 |
60 | /// A transition that stretches and fades pixels starting from the left edge.
61 | /// - Parameter position: The user-space coordinate of the current pixel.
62 | /// - Parameter layer: The SwiftUI layer we're reading from.
63 | /// - Parameter size: The size of the whole image, in user-space.
64 | /// - Parameter amount: The progress of the transition, from 0 to 1.
65 | /// - Returns: The new pixel color.
66 | [[stitchable]] half4 crosswarpRTLTransition(float2 position, SwiftUI::Layer layer, float2 size, float amount) {
67 | // Calculate our coordinate in UV space, 0 to 1.
68 | half2 uv = half2(position / size);
69 |
70 | // Calculate how far this pixel is through the
71 | // transition. When amount is 0, the left edge will
72 | // be 0 and the right edge will be -1. When amount is
73 | // 0.5, the left edge will be 1, and the right edge
74 | // will be 0. When amount is 1, the left edge will be
75 | // 2, and the right edge 1.
76 | half progress = amount * 2.0h + (1.0h - uv.x) - 1.0h;
77 |
78 | // Move smoothly between 0 and 1 with easing, making
79 | // sure to clamp to 0 and 1 at the same time.
80 | half x = smoothstep(0.0h, 1.0h, progress);
81 |
82 | // We want to read pixels increasingly close to the
83 | // original position as the transition progresses.
84 | // So, we move the UV origin towards the center,
85 | // scale the value upwards by 1 minus our smoothed
86 | // progress, then move the UV back to where it was.
87 | half2 newPosition = (uv - 0.5h) * (1.0h - x) + 0.5h;
88 |
89 | // Now blend the pixel at that location with the clear
90 | // color based on x, so we fade out over time.
91 | return mix(layer.sample(float2(newPosition) * size), 0.0h, x);
92 | }
93 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ShaderDescriptions/SimpleTransformationShader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TransformationShader.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A small shader that adjusts its input without any further data, e.g. recoloring.
11 | struct SimpleTransformationShader: Hashable, Identifiable {
12 | /// The unique, random identifier for this shader, so we can show these
13 | /// things in a loop.
14 | var id = UUID()
15 |
16 | /// The human-readable name for this shader. This must work with the
17 | /// String-ShaderName extension so the name matches the underlying
18 | /// Metal shader function name.
19 | var name: String
20 |
21 | /// What kind of SwiftUI modifier needs to be applied to create this shader.
22 | var type: TransformationType = .colorEffect
23 |
24 | /// Whether this shader should be given a replacement color as input.
25 | var usesReplacementColor: Bool
26 |
27 | /// When provided, what range of values should be proposed to the user
28 | /// to control this shader.
29 | var valueRange: ClosedRange?
30 |
31 | /// Some shaders need completely custom initialization, so this is effectively
32 | /// a trap door to allow that to happen rather than squeeze all sorts of
33 | /// special casing into the code.
34 | var initializer: ((_ size: CGSize, _ color: Color, _ value: Double) -> Shader)?
35 |
36 | /// We need a custom equatable conformance to compare only the IDs, because
37 | /// the `initializer` property blocks the synthesized conformance.
38 | static func ==(lhs: SimpleTransformationShader, rhs: SimpleTransformationShader) -> Bool {
39 | lhs.id == rhs.id
40 | }
41 |
42 | /// We need a custom hashable conformance to compare only the IDs, because
43 | /// the `initializer` property blocks the synthesized conformance.
44 | func hash(into hasher: inout Hasher) {
45 | hasher.combine(id)
46 | }
47 |
48 | /// Creates the correct shader for this object, taking into account whether
49 | /// it needs a value range or replacement color. It feels like there ought
50 | /// to be a better way of doing this!
51 | func createShader(color: Color, value: Double, size: CGSize = .zero) -> Shader {
52 | if let initializer {
53 | return initializer(size, color, value)
54 | } else {
55 | let shader = ShaderLibrary[dynamicMember: name.shaderName]
56 |
57 | if valueRange != nil {
58 | if usesReplacementColor {
59 | return shader(
60 | .color(color),
61 | .float(value)
62 | )
63 | } else {
64 | return shader(
65 | .float(value)
66 | )
67 | }
68 | } else {
69 | if usesReplacementColor {
70 | return shader(
71 | .color(color)
72 | )
73 | } else {
74 | return shader()
75 | }
76 | }
77 | }
78 | }
79 |
80 | /// An example shader used for Xcode previews.
81 | static let example = shaders[0]
82 |
83 | /// All the simple transformation shaders we want to show.
84 | static let shaders = [
85 | SimpleTransformationShader(name: "Checkerboard", usesReplacementColor: true, valueRange: 1...20),
86 | SimpleTransformationShader(name: "Emboss", type: .visualEffectDistortion, usesReplacementColor: false, valueRange: 0...20),
87 | SimpleTransformationShader(name: "Gradient Fill", usesReplacementColor: false),
88 | SimpleTransformationShader(name: "Infrared", usesReplacementColor: false),
89 | SimpleTransformationShader(name: "Interlace", usesReplacementColor: true, valueRange: 1...5) { size, color, value in
90 | let shader = ShaderLibrary[dynamicMember: "interlace"]
91 |
92 | return shader(
93 | .float(value),
94 | .color(color),
95 | .float(1)
96 | )
97 | },
98 | SimpleTransformationShader(name: "Invert Alpha", usesReplacementColor: true),
99 | SimpleTransformationShader(name: "Passthrough", usesReplacementColor: false),
100 | SimpleTransformationShader(name: "Recolor", usesReplacementColor: true)
101 | ]
102 | }
103 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ShaderDescriptions/TouchTransformationShader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TouchTransformationShader.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A shader that accepts the location of a user touch, using that to control
11 | /// the shader effect somehow.
12 | struct TouchTransformationShader: Hashable, Identifiable {
13 | /// The unique, random identifier for this shader, so we can show these
14 | /// things in a loop.
15 | var id = UUID()
16 |
17 | /// The human-readable name for this shader. This must work with the
18 | /// String-ShaderName extension so the name matches the underlying
19 | /// Metal shader function name.
20 | var name: String
21 |
22 | /// Whether this shader needs to know the size of the image it's working with.
23 | var usesSize: Bool
24 |
25 | /// When provided, what range of values should be proposed to the user
26 | /// to control this shader.
27 | var valueRange: ClosedRange?
28 |
29 | /// Some shaders need completely custom initialization, so this is effectively
30 | /// a trap door to allow that to happen rather than squeeze all sorts of
31 | /// special casing into the code.
32 | var initializer: ((_ size: CGSize, _ touch: CGPoint, _ value: Double) -> Shader)?
33 |
34 | /// We need a custom equatable conformance to compare only the IDs, because
35 | /// the `initializer` property blocks the synthesized conformance.
36 | static func ==(lhs: TouchTransformationShader, rhs: TouchTransformationShader) -> Bool {
37 | lhs.id == rhs.id
38 | }
39 |
40 | /// We need a custom hashable conformance to compare only the IDs, because
41 | /// the `initializer` property blocks the synthesized conformance.
42 | func hash(into hasher: inout Hasher) {
43 | hasher.combine(id)
44 | }
45 |
46 | /// Creates the correct shader for this object, passing in the current
47 | /// touch location.
48 | func createShader(touchLocation: CGPoint, value: Double) -> Shader {
49 | if let initializer {
50 | return initializer(.zero, touchLocation, value)
51 | } else {
52 | let shader = ShaderLibrary[dynamicMember: name.shaderName]
53 |
54 | if valueRange != nil {
55 | return shader(
56 | .float2(touchLocation),
57 | .float(value)
58 | )
59 | } else {
60 | return shader(
61 | .float2(touchLocation)
62 | )
63 | }
64 | }
65 | }
66 |
67 | /// Creates the correct shader for this object, passing in the current
68 | /// size of the SwiftUI view it's applied to, as well as the current
69 | /// touch location.
70 | func createShader(size: CGSize, touchLocation: CGPoint, value: Double) -> Shader {
71 | if let initializer {
72 | return initializer(size, touchLocation, value)
73 | } else {
74 | let shader = ShaderLibrary[dynamicMember: name.shaderName]
75 |
76 | if valueRange != nil {
77 | return shader(
78 | .float2(size),
79 | .float2(touchLocation)
80 | )
81 | } else {
82 | return shader(
83 | .float2(size),
84 | .float2(touchLocation),
85 | .float(value)
86 | )
87 | }
88 | }
89 | }
90 |
91 | /// An example shader used for Xcode previews.
92 | static let example = shaders[0]
93 |
94 | /// All the touch transformation shaders we want to show.
95 | static let shaders = [
96 | TouchTransformationShader(name: "Color Planes", usesSize: false),
97 | TouchTransformationShader(name: "Simple Loupe", usesSize: true, valueRange: 0.001...0.1) { size, touch, value in
98 | let shader = ShaderLibrary[dynamicMember: "simpleLoupe"]
99 |
100 | return shader(
101 | .float2(size),
102 | .float2(touch),
103 | .float(value),
104 | .float(2)
105 | )
106 | },
107 | TouchTransformationShader(name: "Warping Loupe", usesSize: true, valueRange: 0.001...0.1) { size, touch, value in
108 | let shader = ShaderLibrary[dynamicMember: "warpingLoupe"]
109 |
110 | return shader(
111 | .float2(size),
112 | .float2(touch),
113 | .float(value),
114 | .float(2)
115 | )
116 | }
117 | ]
118 | }
119 |
--------------------------------------------------------------------------------
/Shaders/Transformation/CircleWave.metal:
--------------------------------------------------------------------------------
1 | //
2 | // CircleWave.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// A shader that generates circular waves moving out or in, with varying
12 | /// size, brightness, speed, strength, and more.
13 | ///
14 | /// This works by calculating what a color gradient would look like over the space
15 | /// of the view, then calculating the pixel's distance from the center of the wave.
16 | /// From there we can calculate the brightness of the pixel by taking the cosine of
17 | /// the wave density and speed to create a nice and smooth effect.
18 | ///
19 | /// - Parameter position: The user-space coordinate of the current pixel.
20 | /// - Parameter color: The current color of the pixel.
21 | /// - Parameter time: The number of elapsed seconds since the shader was created.
22 | /// - Parameter size: The size of the whole image, in user-space.
23 | /// - Parameter brightness: How bright the colors should be. Ranges from 0 to 5 work
24 | /// best; try starting with 0.5 and experiment.
25 | /// - Parameter speed: How fast the wave should travel. Ranges from -2 to 2 work best,
26 | /// where negative numbers cause waves to come inwards; try starting with 1.
27 | /// - Parameter strength: How intense the waves should be. Ranges from 0.02 to 5 work
28 | /// best; try starting with 2.
29 | /// - Parameter density: How large each wave should be. Ranges from 20 to 500 work
30 | /// best; try starting with 100.
31 | /// - Parameter center: The center of the effect, where 0.5/0.5 is dead center
32 | /// - Parameter circleColor: The color to use for the waves. Use darker colors to create
33 | /// a less intense core.
34 | /// - Returns: The new pixel color.
35 | [[ stitchable ]] half4 circleWave(float2 position, half4 color, float2 size, float time, float brightness, float speed, float strength, float density, float2 center, half4 circleColor) {
36 | // If it's not transparent…
37 | if (color.a > 0.0h) {
38 | // Calculate our coordinate in UV space, 0 to 1.
39 | half2 uv = half2(position / size);
40 |
41 | // Calculate how far this pixel is from the center point.
42 | half2 delta = uv - half2(center);
43 |
44 | // Make sure we can create round circles even in views
45 | // that aren't square.
46 | half aspectRatio = size.x / size.y;
47 |
48 | // Add the aspect ratio correction to our distance.
49 | delta.x *= aspectRatio;
50 |
51 | // Euclidean distance from our pixel to the center
52 | // of the circle.
53 | half pixelDistance = sqrt((delta.x * delta.x) + (delta.y * delta.y));
54 |
55 | // Calculate how fast to make the waves move; this
56 | // is negative so the waves move outwards with a
57 | // positive speed.
58 | half waveSpeed = -(time * speed * 10.0h);
59 |
60 | // Create RGB colors from the provided brightness.
61 | half3 newBrightness = half3(brightness);
62 |
63 | // Create a gradient by combining our R color with G
64 | // and B values calculated using our texture coordinate,
65 | // then multiply the result by the provided brightness.
66 | half3 gradientColor = half3(circleColor.r, circleColor.g, circleColor.b) * newBrightness;
67 |
68 | // Calculate how much color to apply to this pixel
69 | // by cubing its distance from the center.
70 | half colorStrength = pow(1.0h - pixelDistance, 3.0h);
71 |
72 | // Multiply by the user's input strength.
73 | colorStrength *= strength;
74 |
75 | // Calculate the size of our wave by multiplying
76 | // provided density with our distance from the center.
77 | half waveDensity = density * pixelDistance;
78 |
79 | // Decide how dark this pixel should be as a range
80 | // from -1 to 1 by adding the speed of the overall
81 | // wave by the density of the current pixel.
82 | half cosine = cos(waveSpeed + waveDensity);
83 |
84 | // Halve that cosine and add 0.5, which will give a
85 | // range of 0 to 1. This is our wave fluctuation,
86 | // which causes waves to vary between colored and dark.
87 | half cosineAdjustment = (0.5h * cosine) + 0.5h;
88 |
89 | // Calculate the brightness for this pixel by
90 | // multiplying its color strength with the sum
91 | // of the user's requested strength and our cosine
92 | // adjustment.
93 | half luma = colorStrength * (strength + cosineAdjustment);
94 |
95 | // Force the brightness to decay rapidly so we
96 | // don't hit the edges of our sprite.
97 | luma *= 1.0h - (pixelDistance * 2.0h);
98 | luma = max(0.0h, luma);
99 |
100 | // Multiply our gradient color by brightness for RGB,
101 | // and the brightness itself for A.
102 | half4 finalColor = half4(gradientColor * luma, luma);
103 |
104 | // Multiply the final color by the input alpha, to
105 | // get smooth edges.
106 | return finalColor * color.a;
107 | } else {
108 | // Use the current (transparent) color.
109 | return color;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/Sandbox/Inferno/ShaderDescriptions/TimeTransformationShader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimeTransformationShader.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A shader that accepts a time input value so that its effect changes over time.
11 | struct TimeTransformationShader: Hashable, Identifiable {
12 | /// The unique, random identifier for this shader, so we can show these
13 | /// things in a loop.
14 | var id = UUID()
15 |
16 | /// The human-readable name for this shader. This must work with the
17 | /// String-ShaderName extension so the name matches the underlying
18 | /// Metal shader function name.
19 | var name: String
20 |
21 | /// What kind of SwiftUI modifier needs to be applied to create this shader.
22 | /// There's no default value here, because there is so much variation.
23 | var type: TransformationType
24 |
25 | /// Some shaders need completely custom initialization, so this is effectively
26 | /// a trap door to allow that to happen rather than squeeze all sorts of
27 | /// special casing into the code.
28 | var initializer: ((_ time: Double, _ size: CGSize) -> Shader)?
29 |
30 | /// We need a custom equatable conformance to compare only the IDs, because
31 | /// the `initializer` property blocks the synthesized conformance.
32 | static func ==(lhs: TimeTransformationShader, rhs: TimeTransformationShader) -> Bool {
33 | lhs.id == rhs.id
34 | }
35 |
36 | /// We need a custom hashable conformance to compare only the IDs, because
37 | /// the `initializer` property blocks the synthesized conformance.
38 | func hash(into hasher: inout Hasher) {
39 | hasher.combine(id)
40 | }
41 |
42 | /// Creates the correct shader for this object, passing in the amount of time
43 | /// that has elapsed since the shader was created.
44 | func createShader(elapsedTime: Double) -> Shader {
45 | if let initializer {
46 | return initializer(elapsedTime, .zero)
47 | } else {
48 | let shader = ShaderLibrary[dynamicMember: name.shaderName]
49 | return shader(
50 | .float(elapsedTime)
51 | )
52 | }
53 | }
54 |
55 | /// Creates the correct shader for this object, passing in the the current
56 | /// size of the SwiftUI view it's applied to, and also the amount of time
57 | /// that has elapsed since the shader was created.
58 | func createShader(elapsedTime: Double, size: CGSize) -> Shader {
59 | if let initializer {
60 | return initializer(elapsedTime, size)
61 | } else {
62 | let shader = ShaderLibrary[dynamicMember: name.shaderName]
63 |
64 | return shader(
65 | .float2(size),
66 | .float(elapsedTime)
67 | )
68 | }
69 | }
70 |
71 | /// An example shader used for Xcode previews.
72 | static let example = shaders[0]
73 |
74 | /// All the time transformation shaders we want to show.
75 | static let shaders = [
76 | TimeTransformationShader(name: "Animated Gradient Fill", type: .visualEffectColor),
77 | TimeTransformationShader(name: "Circle Wave", type: .visualEffectColor) { time, size in
78 | let shader = ShaderLibrary[dynamicMember: "circleWave"]
79 |
80 | // This is such a great shader, but trying to squeeze
81 | // all these options into the main sandbox UI would
82 | // have caused all sorts of headaches. So, we use
83 | // custom initialization to inject sensible values
84 | // and the user (that's you!) can just manipulate
85 | // these however they want.
86 | return shader(
87 | .float2(size),
88 | .float(time),
89 | .float(0.5),
90 | .float(1),
91 | .float(2),
92 | .float(100),
93 | .float2(0.5, 0.5),
94 | .color(.green)
95 | )
96 | },
97 | TimeTransformationShader(name: "Rainbow Noise", type: .colorEffect),
98 | TimeTransformationShader(name: "Relative Wave", type: .visualEffectDistortion) { time, size in
99 | let shader = ShaderLibrary[dynamicMember: "relativeWave"]
100 |
101 | return shader(
102 | .float2(size),
103 | .float(time),
104 | .float(5),
105 | .float(20),
106 | .float(5)
107 | )
108 | },
109 | TimeTransformationShader(name: "Water", type: .visualEffectDistortion) { time, size in
110 | let shader = ShaderLibrary[dynamicMember: "water"]
111 |
112 | return shader(
113 | .float2(size),
114 | .float(time),
115 | .float(3),
116 | .float(3),
117 | .float(10)
118 | )
119 | },
120 | TimeTransformationShader(name: "Wave", type: .distortionEffect) { time, _ in
121 | let shader = ShaderLibrary[dynamicMember: "wave"]
122 |
123 | return shader(
124 | .float(time),
125 | .float(5),
126 | .float(10),
127 | .float(5)
128 | )
129 | },
130 | TimeTransformationShader(name: "White Noise", type: .colorEffect)
131 | ]
132 | }
133 |
--------------------------------------------------------------------------------
/Shaders/Generation/LightGrid.metal:
--------------------------------------------------------------------------------
1 | //
2 | // LightGrid.metal
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | #include
9 | using namespace metal;
10 |
11 | /// Creates a grid of multi-colored flashing lights.
12 | ///
13 | /// This works by creating a grid of colors by chunking the texture according
14 | /// to the density from the user. Each chunk is then assigned a random color
15 | /// variance using the same sine trick documented in WhiteNoise, which makes
16 | /// it fluctuate differently from other chunks around it.
17 | ///
18 | /// We then calculate the color for each chunk by taking a base color and
19 | /// adjusting it based on the random color variance we just calculated, so
20 | /// that each chunk displays a different color. This is done using sin() so we
21 | /// get a smooth color modulation.
22 | ///
23 | /// Finally, we pulsate each chunk so that it glows up and down, with black space
24 | /// between each chunk to create delineated a light effect. The black space is
25 | /// created using another call to sin() so that the color ramps from 0 to 1 then
26 | /// back down again.
27 | ///
28 | /// - Parameter position: The user-space coordinate of the current pixel.
29 | /// - Parameter color: The current color of the pixel.
30 | /// - Parameter size: The size of the whole image, in user-space.
31 | /// - Parameter time: The number of elapsed seconds since the shader was created
32 | /// - Parameter density: How many rows and columns to create. A range of 1 to 50
33 | /// works well; try starting with 8.
34 | /// - Parameter speed: How fast to make the lights vary their color. Higher values
35 | /// cause lights to flash faster and vary in color more. A range of 1 to 20 works well;
36 | /// try starting with 3.
37 | /// - Parameter groupSize: How many lights to place in each group. A range of 1 to 8
38 | /// works well depending on your density; starting with 1.
39 | /// - Parameter brightness: How bright to make the lights. A range of 0.2 to 10 works
40 | /// well; try starting with 3.
41 | /// - Returns: The new pixel color.
42 | [[ stitchable ]] half4 lightGrid(float2 position, half4 color, float2 size, float time, float density, float speed, float groupSize, float brightness) {
43 | // Calculate our aspect ratio.
44 | half aspectRatio = size.x / size.y;
45 |
46 | // Calculate our coordinate in UV space, 0 to 1.
47 | half2 uv = half2(position / size);
48 |
49 | // Make sure we can create the effect roughly equally no
50 | // matter what aspect ratio we're in.
51 | uv.x *= aspectRatio;
52 |
53 | // If it's not transparent…
54 | if (color.a > 0.0h) {
55 | // STEP 1: Split the grid up into groups based on user input.
56 | half2 point = uv * density;
57 |
58 | // STEP 2: Calculate the color variance for each group
59 | // pick two numbers that are unlikely to repeat.
60 | half2 nonRepeating = half2(12.9898h, 78.233h);
61 |
62 | // Assign this pixel to a group number.
63 | half2 groupNumber = floor(point);
64 |
65 | // Multiply our group number by the non-repeating
66 | // numbers, then add them together.
67 | half sum = dot(groupNumber, nonRepeating);
68 |
69 | // Calculate the sine of our sum to get a range
70 | // between -1 and 1.
71 | half sine = sin(sum);
72 |
73 | // Multiply the sine by a big, non-repeating number
74 | // so that even a small change will result in
75 | // a big color jump.
76 | float hugeNumber = float(sine) * 43758.5453;
77 |
78 | // Calculate the sine of our time and our huge number
79 | // and map it to the range 0...1.
80 | half variance = (0.5h * sin(time + hugeNumber)) + 0.5h;
81 |
82 | // Adjust the color variance by the provided speed.
83 | half acceleratedVariance = speed * variance;
84 |
85 |
86 | // STEP 3: Calculate the final color for this group.
87 | // Select a base color to work from.
88 | half3 baseColor = half3(3.0h, 1.5h, 0.0h);
89 |
90 | // Apply our variation to the base color, factoring in time.
91 | half3 variedColor = baseColor + acceleratedVariance + time;
92 |
93 | // Calculate the sine of our varied color so it has
94 | // the range -1 to 1.
95 | half3 variedColorSine = sin(variedColor);
96 |
97 | // Adjust the sine to lie in the range 0...1.
98 | half3 newColor = (0.5h * variedColorSine) + 0.5h;
99 |
100 |
101 | // STEP 4: Now we know the color, calculate the color pulse
102 | // Start by moving down and left a little to create black
103 | // lines at intersection points.
104 | half2 adjustedGroupSize = M_PI_H * 2.0h * groupSize * (point - (0.25h / groupSize));
105 |
106 | // Calculate the sine of our group size, then adjust it
107 | // to lie in the range 0...1.
108 | half2 groupSine = (0.5h * sin(adjustedGroupSize)) + 0.5h;
109 |
110 | // Use the sine to calculate a pulsating value between
111 | // 0 and 1, making our group fluctuate together.
112 | half2 pulse = smoothstep(0.0h, 1.0h, groupSine);
113 |
114 | // Calculate the final color by combining the pulse
115 | // strength and user brightness with the color
116 | // for this square.
117 | return half4(newColor * pulse.x * pulse.y * brightness, 1.0h) * color.a;
118 | } else {
119 | // Use the current (transparent) color.
120 | return color;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributor Covenant Code of Conduct
3 |
4 | ## Our Pledge
5 |
6 | We as members, contributors, and leaders pledge to make participation in our
7 | community a harassment-free experience for everyone, regardless of age, body
8 | size, visible or invisible disability, ethnicity, sex characteristics, gender
9 | identity and expression, level of experience, education, socio-economic status,
10 | nationality, personal appearance, race, caste, color, religion, or sexual
11 | identity and orientation.
12 |
13 | We pledge to act and interact in ways that contribute to an open, welcoming,
14 | diverse, inclusive, and healthy community.
15 |
16 | ## Our Standards
17 |
18 | Examples of behavior that contributes to a positive environment for our
19 | community include:
20 |
21 | * Demonstrating empathy and kindness toward other people
22 | * Being respectful of differing opinions, viewpoints, and experiences
23 | * Giving and gracefully accepting constructive feedback
24 | * Accepting responsibility and apologizing to those affected by our mistakes,
25 | and learning from the experience
26 | * Focusing on what is best not just for us as individuals, but for the overall
27 | community
28 |
29 | Examples of unacceptable behavior include:
30 |
31 | * The use of sexualized language or imagery, and sexual attention or advances of
32 | any kind
33 | * Trolling, insulting or derogatory comments, and personal or political attacks
34 | * Public or private harassment
35 | * Publishing others' private information, such as a physical or email address,
36 | without their explicit permission
37 | * Other conduct which could reasonably be considered inappropriate in a
38 | professional setting
39 |
40 | ## Enforcement Responsibilities
41 |
42 | Community leaders are responsible for clarifying and enforcing our standards of
43 | acceptable behavior and will take appropriate and fair corrective action in
44 | response to any behavior that they deem inappropriate, threatening, offensive,
45 | or harmful.
46 |
47 | Community leaders have the right and responsibility to remove, edit, or reject
48 | comments, commits, code, wiki edits, issues, and other contributions that are
49 | not aligned to this Code of Conduct, and will communicate reasons for moderation
50 | decisions when appropriate.
51 |
52 | ## Scope
53 |
54 | This Code of Conduct applies within all community spaces, and also applies when
55 | an individual is officially representing the community in public spaces.
56 | Examples of representing our community include using an official e-mail address,
57 | posting via an official social media account, or acting as an appointed
58 | representative at an online or offline event.
59 |
60 | ## Enforcement
61 |
62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
63 | reported to the community leaders responsible for enforcement at
64 | [INSERT CONTACT METHOD].
65 | All complaints will be reviewed and investigated promptly and fairly.
66 |
67 | All community leaders are obligated to respect the privacy and security of the
68 | reporter of any incident.
69 |
70 | ## Enforcement Guidelines
71 |
72 | Community leaders will follow these Community Impact Guidelines in determining
73 | the consequences for any action they deem in violation of this Code of Conduct:
74 |
75 | ### 1. Correction
76 |
77 | **Community Impact**: Use of inappropriate language or other behavior deemed
78 | unprofessional or unwelcome in the community.
79 |
80 | **Consequence**: A private, written warning from community leaders, providing
81 | clarity around the nature of the violation and an explanation of why the
82 | behavior was inappropriate. A public apology may be requested.
83 |
84 | ### 2. Warning
85 |
86 | **Community Impact**: A violation through a single incident or series of
87 | actions.
88 |
89 | **Consequence**: A warning with consequences for continued behavior. No
90 | interaction with the people involved, including unsolicited interaction with
91 | those enforcing the Code of Conduct, for a specified period of time. This
92 | includes avoiding interactions in community spaces as well as external channels
93 | like social media. Violating these terms may lead to a temporary or permanent
94 | ban.
95 |
96 | ### 3. Temporary Ban
97 |
98 | **Community Impact**: A serious violation of community standards, including
99 | sustained inappropriate behavior.
100 |
101 | **Consequence**: A temporary ban from any sort of interaction or public
102 | communication with the community for a specified period of time. No public or
103 | private interaction with the people involved, including unsolicited interaction
104 | with those enforcing the Code of Conduct, is allowed during this period.
105 | Violating these terms may lead to a permanent ban.
106 |
107 | ### 4. Permanent Ban
108 |
109 | **Community Impact**: Demonstrating a pattern of violation of community
110 | standards, including sustained inappropriate behavior, harassment of an
111 | individual, or aggression toward or disparagement of classes of individuals.
112 |
113 | **Consequence**: A permanent ban from any sort of public interaction within the
114 | community.
115 |
116 | ## Attribution
117 |
118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
119 | version 2.1, available at
120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
121 |
122 | Community Impact Guidelines were inspired by
123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
124 |
125 | For answers to common questions about this code of conduct, see the FAQ at
126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
127 | [https://www.contributor-covenant.org/translations][translations].
128 |
129 | [homepage]: https://www.contributor-covenant.org
130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
131 | [Mozilla CoC]: https://github.com/mozilla/diversity
132 | [FAQ]: https://www.contributor-covenant.org/faq
133 | [translations]: https://www.contributor-covenant.org/translations
134 |
--------------------------------------------------------------------------------
/Shaders/Blur/VariableGaussianBlur.metal:
--------------------------------------------------------------------------------
1 | //
2 | // VariableGaussianBlur.metal
3 | // Inferno
4 | //
5 | // Created by Dale Price on 11/28/23.
6 | //
7 |
8 | #include
9 | #include
10 | using namespace metal;
11 |
12 |
13 | /// Formula of a gaussian function for a single axis as described by https://en.wikipedia.org/wiki/Gaussian_blur. Creates a "bell curve" shape which we'll use for the weight of each sample when averaging.
14 | ///
15 | /// - Parameter distance: The distance from the origin along the current axis.
16 | /// - Parameter sigma: The desired standard deviation of the bell curve.
17 | inline half gaussian(half distance, half sigma) {
18 | // Calculate the exponent of the Gaussian equation.
19 | const half gaussianExponent = -(distance * distance) / (2.0h * sigma * sigma);
20 |
21 | // Calculate and return the entire Gaussian equation.
22 | return (1.0h / (2.0h * M_PI_H * sigma * sigma)) * exp(gaussianExponent);
23 | }
24 |
25 | /// Calculate pixel color using the weighted average of multiple samples along the X axis.
26 | ///
27 | /// - Parameter position: The coordinates of the current pixel.
28 | /// - Parameter layer: The SwiftUI layer we're reading from.
29 | /// - Parameter radius: The desired blur radius.
30 | /// - Parameter axisMultiplier: A vector defining which axis to sample along. Should be (1, 0) for X, or (0, 1) for Y.
31 | /// - Parameter maxSamples: The maximum number of samples to read in each direction from the current pixel. Texture sampling is expensive, so instead of sampling every pixel, we use a lower count spread out across the radius.
32 | half4 gaussianBlur1D(float2 position, SwiftUI::Layer layer, half radius, half2 axisMultiplier, half maxSamples) {
33 | // Calculate how far apart the samples should be: either 1 pixel or the desired radius divided by the maximum number of samples, whichever is farther.
34 | const half interval = max(1.0h, radius / maxSamples);
35 |
36 | // Take the first sample.
37 | // Calculate the weight for this sample in the weighted average using the Gaussian equation.
38 | const half weight = gaussian(0.0h, radius / 2.0h);
39 | // Sample the pixel at the current position and multiply its color by the weight, to use in the weighted average.
40 | // Each sample's color will be combined into the `weightedColorSum` variable (the numerator for the weighted average).
41 | half4 weightedColorSum = layer.sample(position) * weight;
42 | // The `totalWeight` variable will keep track of the sum of all weights (the denominator for the weighted average). Start with the weight of the current sample.
43 | half totalWeight = weight;
44 |
45 | // If the radius is high enough to take more samples, take them.
46 | if(interval <= radius) {
47 |
48 | // Take a sample every `interval` up to and including the desired blur radius.
49 | for (half distance = interval; distance <= radius; distance += interval) {
50 | // Calculate the sample offset as a 2D vector.
51 | const half2 offsetDistance = axisMultiplier * distance;
52 |
53 | // Calculate the sample's weight using the Gaussian equation. For the sigma value, we use half the blur radius so that the resulting bell curve fits nicely within the radius.
54 | const half weight = gaussian(distance, radius / 2.0h);
55 |
56 | // Add the weight to the total. Double the weight because we are taking two samples per iteration.
57 | totalWeight += weight * 2.0h;
58 |
59 | // Take two samples along the axis, one in the positive direction and one negative, multiply by weight, and add to the sum.
60 | weightedColorSum += layer.sample(float2(half2(position) + offsetDistance)) * weight;
61 | weightedColorSum += layer.sample(float2(half2(position) - offsetDistance)) * weight;
62 | }
63 | }
64 |
65 | // Return the weighted average color of the samples by dividing the weighted sum of the colors by the sum of the weights.
66 | return weightedColorSum / totalWeight;
67 | }
68 |
69 | /// Variable blur effect along the specified axis that samples from a texture to determine the blur radius multiplier at each pixel. This shader requires two passes, one along the X axis and one along the Y.
70 | ///
71 | /// The two-pass approach is better for performance as it scales linearly rather than exponentially with pixel count * radius * sample count, but can result in "streak" artifacts where blurred areas meet unblurred areas.
72 | ///
73 | /// - Parameter position: The coordinates of the current pixel in user space.
74 | /// - Parameter layer: The SwiftUI layer we're applying the blur to.
75 | /// - Parameter boundingRect: The bounding rectangle of the SwiftUI view in user space.
76 | /// - Parameter radius: The desired maximum blur radius for areas of the mask that are fully opaque.
77 | /// - Parameter maxSamples: The maximum number of samples to read _in each direction_ from the current pixel. Reducing this value increases performance but results in banding in the resulting blur.
78 | /// - Parameter mask: The texture to sample alpha values from to determine the blur radius at each pixel.
79 | /// - Parameter vertical: Specifies to blur along the Y axis. Because SwiftUI can't pass booleans to a shader, `0.0` is treated as false (i.e. blur the X axis) and any other value is treated as true (i.e. blur the Y axis).
80 | [[ stitchable ]] half4 variableBlur(float2 pos, SwiftUI::Layer layer, float4 boundingRect, float radius, float maxSamples, texture2d mask, float vertical) {
81 | // Calculate the position in UV space within the bounding rect (0 to 1).
82 | const float2 uv = float2(pos.x / boundingRect[2], pos.y / boundingRect[3]);
83 |
84 | // Sample the alpha value of the mask at the current UV position.
85 | const half maskAlpha = mask.sample(metal::sampler(metal::filter::linear), uv).a;
86 |
87 | // Determine the blur radius at this pixel by multiplying the alpha value from the mask with the radius parameter.
88 | const half pixelRadius = maskAlpha * half(radius);
89 |
90 | // If the resulting radius is 1 pixel or greater…
91 | if(pixelRadius >= 1) {
92 | // Set the "axis multiplier" value that tells the blur function whether to sample along the X or Y axis.
93 | const half2 axisMultiplier = vertical == 0.0 ? half2(1, 0) : half2(0, 1);
94 |
95 | // Return the blurred color.
96 | return gaussianBlur1D(pos, layer, pixelRadius, axisMultiplier, maxSamples);
97 | } else {
98 | // If the blur radius is less than 1 pixel, return the current pixel's color as-is.
99 | return layer.sample(pos);
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Transitions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Transitions.swift
3 | // Inferno
4 | // https://www.github.com/twostraws/Inferno
5 | // See LICENSE for license information.
6 | //
7 |
8 | // IMPORTANT:
9 | // This file contains all the SwiftUI transitions for the
10 | // shaders included with Inferno. If you want to use one
11 | // of our transitions, you need to copy this Swift file
12 | // into your project alongside the Metal files for your
13 | // chosen transitions.
14 |
15 | import SwiftUI
16 |
17 | /// A transition where many circles grow upwards to reveal the new content.
18 | struct CircleTransition: ViewModifier {
19 | /// How big to make the circles.
20 | var size = 20.0
21 |
22 | /// How far we are through the transition: 0 is unstarted, and 1 is finished.
23 | var progress = 0.0
24 |
25 | func body(content: Content) -> some View {
26 | content
27 | .colorEffect(
28 | ShaderLibrary.circleTransition(
29 | .float(progress),
30 | .float(size)
31 | )
32 | )
33 | }
34 | }
35 |
36 | /// A transition where many circles grow upwards to reveal the new content,
37 | /// with the circles moving outwards from the top-left edge.
38 | struct CircleWaveTransition: ViewModifier {
39 | /// How big to make the circles.
40 | var size = 20.0
41 |
42 | /// How far we are through the transition: 0 is unstarted, and 1 is finished.
43 | var progress = 0.0
44 |
45 | func body(content: Content) -> some View {
46 | content
47 | .visualEffect { content, proxy in
48 | content
49 | .colorEffect(
50 | ShaderLibrary.circleWaveTransition(
51 | .float2(proxy.size),
52 | .float(progress),
53 | .float(size)
54 | )
55 | )
56 | }
57 | }
58 | }
59 |
60 | /// A transition where many diamonds grow upwards to reveal the new content.
61 | struct DiamondTransition: ViewModifier {
62 | /// How big to make the diamonds.
63 | var size = 20.0
64 |
65 | /// How far we are through the transition: 0 is unstarted, and 1 is finished.
66 | var progress = 0.0
67 |
68 | func body(content: Content) -> some View {
69 | content
70 | .colorEffect(
71 | ShaderLibrary.diamondTransition(
72 | .float(progress),
73 | .float(size)
74 | )
75 | )
76 | }
77 | }
78 |
79 | /// A transition where many diamonds grow upwards to reveal the new content,
80 | /// with the diamonds moving outwards from the top-left edge.
81 | struct DiamondWaveTransition: ViewModifier {
82 | /// How big to make the diamonds.
83 | var size = 20.0
84 |
85 | /// How far we are through the transition: 0 is unstarted, and 1 is finished.
86 | var progress = 0.0
87 |
88 | func body(content: Content) -> some View {
89 | content
90 | .visualEffect { content, proxy in
91 | content
92 | .colorEffect(
93 | ShaderLibrary.diamondWaveTransition(
94 | .float2(proxy.size),
95 | .float(progress),
96 | .float(size)
97 | )
98 | )
99 | }
100 | }
101 | }
102 |
103 |
104 | /// A Metal-powered layer effect transition that needs to know the
105 | /// view's size. You probably don't want to use this directly, and
106 | /// should instead use one of the AnyTransition extensions.
107 | struct InfernoTransition: ViewModifier {
108 | /// The name of the shader function we're rendering.
109 | var name: String
110 |
111 | /// How far we are through the transition: 0 is unstarted, and 1 is finished.
112 | var progress = 0.0
113 |
114 | func body(content: Content) -> some View {
115 | content
116 | .visualEffect { content, proxy in
117 | content
118 | .layerEffect(
119 | ShaderLibrary[dynamicMember: name](
120 | .float2(proxy.size),
121 | .float(progress)
122 | ), maxSampleOffset: .zero)
123 | }
124 | }
125 | }
126 |
127 | struct InferoDistortionTranstion: ViewModifier {
128 | /// The name of the shader function we're rendering.
129 | var name: String
130 |
131 | /// How far we are through the transition: 0 is unstarted, and 1 is finished.
132 | var progress = 0.0
133 |
134 | func body(content: Content) -> some View {
135 | content
136 | .visualEffect { content, proxy in
137 | content
138 | .distortionEffect(
139 | ShaderLibrary[dynamicMember: name](
140 | .float2(proxy.size),
141 | .float(progress)
142 | ), maxSampleOffset: .zero)
143 | }
144 | }
145 | }
146 |
147 | /// A transition that causes the incoming and outgoing views to become
148 | /// increasingly pixellated, then return to their normal state. While this
149 | /// happens the old view fades out and the new one fades in.
150 | struct PixellateTransition: ViewModifier {
151 | /// How large the pixels should be.
152 | var squares = 10.0
153 |
154 | /// How many steps to use for the animation. Lower values make the
155 | /// pixels jump in more noticeable size increments, which creates
156 | /// very interesting retro effects.
157 | var steps = 60.0
158 |
159 | /// How far we are through the transition: 0 is unstarted, and 1 is finished.
160 | var progress = 0.0
161 |
162 | func body(content: Content) -> some View {
163 | content
164 | .visualEffect { content, proxy in
165 | content
166 | .layerEffect(
167 | ShaderLibrary.pixellate(
168 | .float2(proxy.size),
169 | .float(progress),
170 | .float(squares),
171 | .float(steps)
172 | ), maxSampleOffset: .zero)
173 | }
174 | }
175 | }
176 |
177 | /// A transition where views are twirled from the center and faded out.
178 | struct SwirlTransition: ViewModifier {
179 | /// How large the swirl should be relative to the view it's transitioning.
180 | var radius = 0.5
181 |
182 | /// How far we are through the transition: 0 is unstarted, and 1 is finished.
183 | var progress = 0.0
184 |
185 | func body(content: Content) -> some View {
186 | content
187 | .visualEffect { content, proxy in
188 | content
189 | .layerEffect(
190 | ShaderLibrary.swirl(
191 | .float2(proxy.size),
192 | .float(progress),
193 | .float(radius)
194 | ), maxSampleOffset: .zero)
195 | }
196 | }
197 | }
198 |
199 | /// A transition where views are removed blowing streaks from the right edge.
200 | struct WindTransition: ViewModifier {
201 | /// How long the streaks should be, relative to the view's width.
202 | var size = 0.2
203 |
204 | /// How far we are through the transition: 0 is unstarted, and 1 is finished.
205 | var progress = 0.0
206 |
207 | func body(content: Content) -> some View {
208 | content
209 | .visualEffect { content, proxy in
210 | content
211 | .layerEffect(
212 | ShaderLibrary.windTransition(
213 | .float2(proxy.size),
214 | .float(progress),
215 | .float(size)
216 | ), maxSampleOffset: .zero)
217 | }
218 | }
219 | }
220 |
221 | /// A collection of wrappers to make Inferno transitions easier to use.
222 | extension AnyTransition {
223 | /// A transition that makes a variety of circles simultaneously zoom up
224 | /// across the screen.
225 | /// - Parameters:
226 | /// - Parameter size: The size of the circles.
227 | static func circles(size: Double = 20) -> AnyTransition {
228 | .asymmetric(
229 | insertion: .modifier(
230 | active: CircleTransition(size: size, progress: 0),
231 | identity: CircleTransition(size: size, progress: 1)
232 | ),
233 | removal: .scale(scale: 1 + Double.ulpOfOne)
234 | )
235 | }
236 |
237 | /// A transition that makes a variety of circles zoom up across the screen,
238 | /// based on their X/Y position.
239 | /// - Parameters:
240 | /// - Parameter size: The size of the circles.
241 | static func circleWave(size: Double = 20) -> AnyTransition {
242 | .asymmetric(
243 | insertion: .modifier(
244 | active: CircleWaveTransition(size: size, progress: 0),
245 | identity: CircleWaveTransition(size: size, progress: 1)
246 | ),
247 | removal: .scale(scale: 1 + Double.ulpOfOne)
248 | )
249 | }
250 |
251 | /// A transition that makes a variety of diamonds simultaneously zoom up
252 | /// across the screen.
253 | /// - Parameter size: The size of the diamonds.
254 | static func diamonds(size: Double = 20) -> AnyTransition {
255 | .asymmetric(
256 | insertion: .modifier(
257 | active: DiamondTransition(size: size, progress: 0),
258 | identity: DiamondTransition(size: size, progress: 1)
259 | ),
260 | removal: .scale(scale: 1 + Double.ulpOfOne)
261 | )
262 | }
263 |
264 | /// A transition that makes a variety of circles zoom up across the screen,
265 | /// based on their X/Y position.
266 | /// - Parameters:
267 | /// - Parameter size: The size of the diamonds.
268 | static func diamondWave(size: Double = 20) -> AnyTransition {
269 | .asymmetric(
270 | insertion: .modifier(
271 | active: DiamondWaveTransition(size: size, progress: 0),
272 | identity: DiamondWaveTransition(size: size, progress: 1)
273 | ),
274 | removal: .scale(scale: 1 + Double.ulpOfOne)
275 | )
276 | }
277 |
278 | /// A transition that stretches a view from one edge to the other, while
279 | /// also fading it out. This one is for left-to-right transitions.
280 | static let crosswarpLTR: AnyTransition = .asymmetric(
281 | insertion: .modifier(
282 | active: InfernoTransition(name: "crosswarpLTRTransition", progress: 1),
283 | identity: InfernoTransition(name: "crosswarpLTRTransition", progress: 0)
284 | ),
285 | removal: .modifier(
286 | active: InfernoTransition(name: "crosswarpRTLTransition", progress: 1),
287 | identity: InfernoTransition(name: "crosswarpRTLTransition", progress: 0)
288 | )
289 | )
290 |
291 | /// A transition that stretches a view from one edge to the other, while
292 | /// also fading it out. This one is for right-to-left transitions.
293 | static let crosswarpRTL: AnyTransition = .asymmetric(
294 | insertion: .modifier(
295 | active: InfernoTransition(name: "crosswarpRTLTransition", progress: 1),
296 | identity: InfernoTransition(name: "crosswarpRTLTransition", progress: 0)
297 | ),
298 | removal: .modifier(
299 | active: InfernoTransition(name: "crosswarpLTRTransition", progress: 1),
300 | identity: InfernoTransition(name: "crosswarpLTRTransition", progress: 0)
301 | )
302 | )
303 |
304 | /// A transition that causes the incoming and outgoing views to become
305 | /// increasingly pixellated, then return to their normal state. While this
306 | /// happens the old view fades out and the new one fades in.
307 | /// - Parameters:
308 | /// - squares: How many pixel squares to create.
309 | /// - steps: How many animation steps to use; anything >= 60 looks smooth.
310 | static func pixellate(squares: Double = 20, steps: Double = 60) -> AnyTransition {
311 | .asymmetric(
312 | insertion: .modifier(
313 | active: PixellateTransition(squares: squares, steps: steps, progress: 1),
314 | identity: PixellateTransition(squares: squares, steps: steps, progress: 0)
315 | ),
316 | removal: .modifier(
317 | active: PixellateTransition(squares: squares, steps: steps, progress: 1),
318 | identity: PixellateTransition(squares: squares, steps: steps, progress: 0)
319 | )
320 | )
321 | }
322 |
323 | /// A transition that causes the incoming and outgoing views to become
324 | /// shifted/angled then return to their normal state. While this
325 | /// happens the old view slides out and the new one slides in.
326 | static func shift() -> AnyTransition {
327 | .asymmetric(
328 | insertion: .modifier(
329 | active: InferoDistortionTranstion(name: "shiftTranstion", progress: 1),
330 | identity: InferoDistortionTranstion(name: "shiftTranstion", progress: 0)
331 | ),
332 | removal: .modifier(
333 | active: InferoDistortionTranstion(name: "shiftTranstion", progress: -1),
334 | identity: InferoDistortionTranstion(name: "shiftTranstion", progress: 0)
335 | )
336 | )
337 | }
338 |
339 | /// A transition that causes the incoming and outgoing views to become
340 | /// sucked in and ouf of the top right corner.
341 | static func genie() -> AnyTransition {
342 | .asymmetric(
343 | insertion: .modifier(
344 | active: InferoDistortionTranstion(name: "genieTranstion", progress: 1),
345 | identity: InferoDistortionTranstion(name: "genieTranstion", progress: 0)
346 | ),
347 | removal: .modifier(
348 | active: InferoDistortionTranstion(name: "genieTranstion", progress: 1),
349 | identity: InferoDistortionTranstion(name: "genieTranstion", progress: 0)
350 | )
351 | )
352 | }
353 |
354 | /// A transition that creates an old-school radial wipe, starting from straight up.
355 | static let radial: AnyTransition = .asymmetric(
356 | insertion: .modifier(
357 | active: InfernoTransition(name: "radialTransition", progress: 1),
358 | identity: InfernoTransition(name: "radialTransition", progress: 0)
359 | ),
360 | removal: .scale(scale: 1 + Double.ulpOfOne)
361 | )
362 |
363 | /// A transition that increasingly twists the contents of the incoming and outgoing
364 | /// views, then untwists them to complete the transition. As this happens the two
365 | /// views fade to move smoothly from one to the other.
366 | /// - Parameters:
367 | /// - Parameter radius: How much of the view to swirl, in the range 0 to 1. Start with 0.5 and experiment.
368 | static func swirl(radius: Double = 0.5) -> AnyTransition {
369 | .asymmetric(
370 | insertion: .modifier(
371 | active: SwirlTransition(radius: radius, progress: 1),
372 | identity: SwirlTransition(radius: radius, progress: 0)
373 | ),
374 | removal: .modifier(
375 | active: SwirlTransition(radius: radius, progress: 1),
376 | identity: SwirlTransition(radius: radius, progress: 0)
377 | )
378 | )
379 | }
380 |
381 | /// A transition that makes it look the pixels of one image are being blown
382 | /// away horizontally.
383 | /// - Parameters:
384 | /// - Parameter size: How big the wind streaks should be, relative to the view's width.
385 | static func wind(size: Double = 0.2) -> AnyTransition {
386 | .asymmetric(
387 | insertion: .modifier(
388 | active: WindTransition(size: size, progress: 1),
389 | identity: WindTransition(size: size, progress: 0)
390 | ),
391 | removal: .scale(scale: 1 + Double.ulpOfOne)
392 | )
393 | }
394 | }
395 |
--------------------------------------------------------------------------------