├── 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 | --------------------------------------------------------------------------------