diff --git a/BlendApp.swift b/BlendApp.swift new file mode 100644 index 0000000..13f1830 --- /dev/null +++ b/BlendApp.swift @@ -0,0 +1,14 @@ +import SwiftUI + +@main +struct BlendApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + .windowStyle(HiddenTitleBarWindowStyle()) + .commands { + CommandGroup(replacing: .newItem) { } + } + } +} diff --git a/BlendLayerMetalRenderer.swift b/BlendLayerMetalRenderer.swift new file mode 100644 index 0000000..5212f3d --- /dev/null +++ b/BlendLayerMetalRenderer.swift @@ -0,0 +1,97 @@ +import MetalKit +import SwiftUI + +class BlendLayerMetalRenderer: NSObject, MTKViewDelegate { + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var baseTexture: MTLTexture? + private var blendTexture: MTLTexture? + private var samplerState: MTLSamplerState! + + var blendMode: CustomBlendMode = .normal + + init(device: MTLDevice, baseImage: NSImage, blendImage: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + setupSamplerState() + loadTextures(baseImage: baseImage, blendImage: blendImage) + } + + private func setupPipeline() { + let library = device.makeDefaultLibrary() + let vertexFunction = library?.makeFunction(name: "vertexShader") + let fragmentFunction = library?.makeFunction(name: "blendFragmentShader") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + private func setupVertexBuffer() { + let vertices: [Vertex] = [ + Vertex(position: SIMD2(-1, -1), textureCoordinate: SIMD2(0, 1)), + Vertex(position: SIMD2(1, -1), textureCoordinate: SIMD2(1, 1)), + Vertex(position: SIMD2(-1, 1), textureCoordinate: SIMD2(0, 0)), + Vertex(position: SIMD2(1, 1), textureCoordinate: SIMD2(1, 0)) + ] + + vertexBuffer = device.makeBuffer(bytes: vertices, + length: vertices.count * MemoryLayout.stride, + options: []) + } + + private func setupSamplerState() { + let samplerDescriptor = MTLSamplerDescriptor() + samplerDescriptor.minFilter = .linear + samplerDescriptor.magFilter = .linear + samplerState = device.makeSamplerState(descriptor: samplerDescriptor) + } + + private func loadTextures(baseImage: NSImage, blendImage: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + + if let cgImage = baseImage.CGImage { + baseTexture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + + if let cgImage = blendImage.CGImage { + blendTexture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func draw(in view: MTKView) { + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor else { + return + } + + let commandBuffer = commandQueue.makeCommandBuffer() + let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor) + + renderEncoder?.setRenderPipelineState(pipelineState) + renderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + renderEncoder?.setFragmentTexture(baseTexture, index: 0) + renderEncoder?.setFragmentTexture(blendTexture, index: 1) + renderEncoder?.setFragmentSamplerState(samplerState, index: 0) + + // Set blend mode uniform + var blendModeInt = Int32(blendMode.rawValue) + renderEncoder?.setFragmentBytes(&blendModeInt, length: MemoryLayout.size, index: 0) + + renderEncoder?.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) + renderEncoder?.endEncoding() + + commandBuffer?.present(drawable) + commandBuffer?.commit() + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} diff --git a/BlendLayerView.swift b/BlendLayerView.swift new file mode 100644 index 0000000..65b459f --- /dev/null +++ b/BlendLayerView.swift @@ -0,0 +1,103 @@ +import SwiftUI +import AppKit + +struct BlendLayerMetalView: NSViewRepresentable { + var baseImage: NSImage + var blendImage: NSImage + var blendMode: CustomBlendMode + + // For custom blend modes + @State private var compositedImage: NSImage? + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.enableSetNeedsDisplay = true + mtkView.isPaused = true + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ mtkView: MTKView, context: Context) { + context.coordinator.updateBlendMode(blendMode) + mtkView.setNeedsDisplay(mtkView.frame) + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + var parent: BlendLayerMetalView + var renderer: BlendLayerMetalRenderer + + init(_ parent: BlendLayerMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = BlendLayerMetalRenderer(device: device, + baseImage: parent.baseImage, + blendImage: parent.blendImage) + } + + func updateBlendMode(_ mode: CustomBlendMode) { + renderer.blendMode = mode + } + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + } +} + +struct BlendLayerView: View { + @State private var selectedBlendMode: CustomBlendMode = .normal + var baseImage: NSImage = NSImage(resource: .person) + var blendImage: NSImage = NSImage(resource: .person2) + + var body: some View { + VStack { + BlendLayerMetalView( + baseImage: baseImage, + blendImage: blendImage, + blendMode: selectedBlendMode + ) + .frame(width: 300, height: 300) + + Picker("Blend Mode", selection: $selectedBlendMode) { + Group { + Text("Normal").tag(CustomBlendMode.normal) + Text("Multiply").tag(CustomBlendMode.multiply) + Text("Screen").tag(CustomBlendMode.screen) + Text("Overlay").tag(CustomBlendMode.overlay) + Text("Darken").tag(CustomBlendMode.darken) + Text("Lighten").tag(CustomBlendMode.lighten) + } + + Group { + Text("Color Dodge").tag(CustomBlendMode.colorDodge) + Text("Color Burn").tag(CustomBlendMode.colorBurn) + Text("Soft Light").tag(CustomBlendMode.softLight) + Text("Hard Light").tag(CustomBlendMode.hardLight) + Text("Difference").tag(CustomBlendMode.difference) + Text("Exclusion").tag(CustomBlendMode.exclusion) + } + + Group { + Text("Hue").tag(CustomBlendMode.hue) + Text("Saturation").tag(CustomBlendMode.saturation) + Text("Color").tag(CustomBlendMode.color) + Text("Luminosity").tag(CustomBlendMode.luminosity) + Text("Linear Dodge").tag(CustomBlendMode.linearDodge) + Text("Hard Mix").tag(CustomBlendMode.hardMix) + } + } + .pickerStyle(MenuPickerStyle()) + .padding() + } + } +} + +#Preview { + BlendLayerView() +} diff --git a/BlendMode.swift b/BlendMode.swift new file mode 100644 index 0000000..63ec847 --- /dev/null +++ b/BlendMode.swift @@ -0,0 +1,70 @@ +import SwiftUI + +enum CustomBlendMode { + case normal + case colorDodge + case hardMix + case darken + case linearDodge + case difference + case multiply + case lighterColor + case exclusion + case colorBurn + case overlay + case subtract + case linearBurn + case softLight + case divide + case darker + case hardLight + case hue + case color + case vividLight + case saturation + case lighten + case linearLight + case screen + case pinLight + case luminosity + + var swiftUIBlendMode: BlendMode { + switch self { + case .normal: + return .normal + case .multiply: + return .multiply + case .screen: + return .screen + case .overlay: + return .overlay + case .darken: + return .darken + case .lighten: + return .lighten + case .colorDodge: + return .colorDodge + case .colorBurn: + return .colorBurn + case .softLight: + return .softLight + case .hardLight: + return .hardLight + case .difference: + return .difference + case .exclusion: + return .exclusion + case .hue: + return .hue + case .saturation: + return .saturation + case .color: + return .color + case .luminosity: + return .luminosity + // For custom blend modes not directly supported by SwiftUI + default: + return .normal + } + } +} diff --git a/ContentView.swift b/ContentView.swift new file mode 100644 index 0000000..2287a8a --- /dev/null +++ b/ContentView.swift @@ -0,0 +1,97 @@ +import SwiftUI + +struct ContentView: View { + @State private var selectedBlendMode: CustomBlendMode = .normal + @State private var baseImage: NSImage? + @State private var blendImage: NSImage? + + var body: some View { + HSplitView { + // Left sidebar with controls + VStack(alignment: .leading, spacing: 20) { + Text("Blend Modes") + .font(.headline) + + Picker("Blend Mode", selection: $selectedBlendMode) { + Group { + Text("Normal").tag(CustomBlendMode.normal) + Text("Multiply").tag(CustomBlendMode.multiply) + Text("Screen").tag(CustomBlendMode.screen) + Text("Overlay").tag(CustomBlendMode.overlay) + Text("Darken").tag(CustomBlendMode.darken) + Text("Lighten").tag(CustomBlendMode.lighten) + Text("Color Dodge").tag(CustomBlendMode.colorDodge) + Text("Color Burn").tag(CustomBlendMode.colorBurn) + Text("Soft Light").tag(CustomBlendMode.softLight) + Text("Hard Light").tag(CustomBlendMode.hardLight) + } + + Group { + Text("Difference").tag(CustomBlendMode.difference) + Text("Exclusion").tag(CustomBlendMode.exclusion) + Text("Hue").tag(CustomBlendMode.hue) + Text("Saturation").tag(CustomBlendMode.saturation) + Text("Color").tag(CustomBlendMode.color) + Text("Luminosity").tag(CustomBlendMode.luminosity) + Text("Linear Dodge").tag(CustomBlendMode.linearDodge) + Text("Hard Mix").tag(CustomBlendMode.hardMix) + Text("Vivid Light").tag(CustomBlendMode.vividLight) + Text("Linear Light").tag(CustomBlendMode.linearLight) + } + } + .pickerStyle(RadioGroupPickerStyle()) + .padding() + + VStack(alignment: .leading, spacing: 10) { + Button("Choose Base Image") { + openImagePicker(for: \.$baseImage) + } + + Button("Choose Blend Image") { + openImagePicker(for: \.$blendImage) + } + } + .padding() + + Spacer() + } + .frame(minWidth: 200, maxWidth: 250) + .padding() + + // Right side with blend preview + if let baseImage = baseImage, let blendImage = blendImage { + BlendLayerView( + baseImage: baseImage, + blendImage: blendImage, + blendMode: selectedBlendMode + ) + .frame(minWidth: 400, minHeight: 400) + } else { + Text("Choose base and blend images to start") + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + } + + private func openImagePicker(for binding: ReferenceWritableKeyPath>) { + let panel = NSOpenPanel() + panel.allowsMultipleSelection = false + panel.canChooseDirectories = false + panel.canChooseFiles = true + panel.allowedContentTypes = [.image] + + panel.begin { response in + if response == .OK, let url = panel.url { + if let image = NSImage(contentsOf: url) { + self[keyPath: binding].wrappedValue = image + } + } + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/Shade.xcodeproj/project.pbxproj b/Shade.xcodeproj/project.pbxproj index 49b816e..6325e04 100644 --- a/Shade.xcodeproj/project.pbxproj +++ b/Shade.xcodeproj/project.pbxproj @@ -3,14 +3,10 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ - 0493E6D62C72981800BA9F56 /* ShadeApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0493E6D52C72981800BA9F56 /* ShadeApp.swift */; }; - 0493E6D82C72981800BA9F56 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0493E6D72C72981800BA9F56 /* ContentView.swift */; }; - 0493E6DA2C72981900BA9F56 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0493E6D92C72981900BA9F56 /* Assets.xcassets */; }; - 0493E6DD2C72981900BA9F56 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0493E6DC2C72981900BA9F56 /* Preview Assets.xcassets */; }; 0493E6E82C72981900BA9F56 /* ShadeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0493E6E72C72981900BA9F56 /* ShadeTests.swift */; }; 0493E6F22C72981900BA9F56 /* ShadeUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0493E6F12C72981900BA9F56 /* ShadeUITests.swift */; }; 0493E6F42C72981900BA9F56 /* ShadeUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0493E6F32C72981900BA9F56 /* ShadeUITestsLaunchTests.swift */; }; @@ -35,11 +31,6 @@ /* Begin PBXFileReference section */ 0493E6D22C72981800BA9F56 /* Shade.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Shade.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 0493E6D52C72981800BA9F56 /* ShadeApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadeApp.swift; sourceTree = ""; }; - 0493E6D72C72981800BA9F56 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - 0493E6D92C72981900BA9F56 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 0493E6DC2C72981900BA9F56 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 0493E6DE2C72981900BA9F56 /* Shade.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Shade.entitlements; sourceTree = ""; }; 0493E6E32C72981900BA9F56 /* ShadeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ShadeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 0493E6E72C72981900BA9F56 /* ShadeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadeTests.swift; sourceTree = ""; }; 0493E6ED2C72981900BA9F56 /* ShadeUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ShadeUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -47,6 +38,10 @@ 0493E6F32C72981900BA9F56 /* ShadeUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadeUITestsLaunchTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + D0D331292D29937600CE9A9E /* Shade */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Shade; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 0493E6CF2C72981800BA9F56 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -75,7 +70,7 @@ 0493E6C92C72981800BA9F56 = { isa = PBXGroup; children = ( - 0493E6D42C72981800BA9F56 /* Shade */, + D0D331292D29937600CE9A9E /* Shade */, 0493E6E62C72981900BA9F56 /* ShadeTests */, 0493E6F02C72981900BA9F56 /* ShadeUITests */, 0493E6D32C72981800BA9F56 /* Products */, @@ -92,26 +87,6 @@ name = Products; sourceTree = ""; }; - 0493E6D42C72981800BA9F56 /* Shade */ = { - isa = PBXGroup; - children = ( - 0493E6D52C72981800BA9F56 /* ShadeApp.swift */, - 0493E6D72C72981800BA9F56 /* ContentView.swift */, - 0493E6D92C72981900BA9F56 /* Assets.xcassets */, - 0493E6DE2C72981900BA9F56 /* Shade.entitlements */, - 0493E6DB2C72981900BA9F56 /* Preview Content */, - ); - path = Shade; - sourceTree = ""; - }; - 0493E6DB2C72981900BA9F56 /* Preview Content */ = { - isa = PBXGroup; - children = ( - 0493E6DC2C72981900BA9F56 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; 0493E6E62C72981900BA9F56 /* ShadeTests */ = { isa = PBXGroup; children = ( @@ -144,6 +119,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + D0D331292D29937600CE9A9E /* Shade */, + ); name = Shade; productName = Shade; productReference = 0493E6D22C72981800BA9F56 /* Shade.app */; @@ -233,8 +211,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 0493E6DD2C72981900BA9F56 /* Preview Assets.xcassets in Resources */, - 0493E6DA2C72981900BA9F56 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -259,8 +235,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 0493E6D82C72981800BA9F56 /* ContentView.swift in Sources */, - 0493E6D62C72981800BA9F56 /* ShadeApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -421,10 +395,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Shade/Shade.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Shade/Preview Content\""; + DEVELOPMENT_TEAM = 7Z6V95G685; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -446,10 +422,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Shade/Shade.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Shade/Preview Content\""; + DEVELOPMENT_TEAM = 7Z6V95G685; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -468,7 +446,6 @@ 0493E6FB2C72981900BA9F56 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -486,7 +463,6 @@ 0493E6FC2C72981900BA9F56 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -504,7 +480,6 @@ 0493E6FE2C72981900BA9F56 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; @@ -520,7 +495,6 @@ 0493E6FF2C72981900BA9F56 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; diff --git a/Shade/Assets.xcassets/city.imageset/Contents.json b/Shade/Assets.xcassets/city.imageset/Contents.json new file mode 100644 index 0000000..52ff7bf --- /dev/null +++ b/Shade/Assets.xcassets/city.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "crowdedCity.jpeg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shade/Assets.xcassets/city.imageset/crowdedCity.jpeg b/Shade/Assets.xcassets/city.imageset/crowdedCity.jpeg new file mode 100644 index 0000000..c26aff5 Binary files /dev/null and b/Shade/Assets.xcassets/city.imageset/crowdedCity.jpeg differ diff --git a/Shade/Assets.xcassets/person.imageset/Contents.json b/Shade/Assets.xcassets/person.imageset/Contents.json new file mode 100644 index 0000000..ea94c84 --- /dev/null +++ b/Shade/Assets.xcassets/person.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "person.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shade/Assets.xcassets/person.imageset/person.png b/Shade/Assets.xcassets/person.imageset/person.png new file mode 100644 index 0000000..60bd2b3 Binary files /dev/null and b/Shade/Assets.xcassets/person.imageset/person.png differ diff --git a/Shade/Assets.xcassets/person1.imageset/Contents.json b/Shade/Assets.xcassets/person1.imageset/Contents.json new file mode 100644 index 0000000..b20be26 --- /dev/null +++ b/Shade/Assets.xcassets/person1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "person3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shade/Assets.xcassets/person1.imageset/person3.png b/Shade/Assets.xcassets/person1.imageset/person3.png new file mode 100644 index 0000000..6a023b0 Binary files /dev/null and b/Shade/Assets.xcassets/person1.imageset/person3.png differ diff --git a/Shade/Assets.xcassets/person2.imageset/Contents.json b/Shade/Assets.xcassets/person2.imageset/Contents.json new file mode 100644 index 0000000..18d96bb --- /dev/null +++ b/Shade/Assets.xcassets/person2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "person2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shade/Assets.xcassets/person2.imageset/person2.png b/Shade/Assets.xcassets/person2.imageset/person2.png new file mode 100644 index 0000000..1309d59 Binary files /dev/null and b/Shade/Assets.xcassets/person2.imageset/person2.png differ diff --git a/Shade/CoreML/ObjectDetection/ObjectDetection.swift b/Shade/CoreML/ObjectDetection/ObjectDetection.swift new file mode 100644 index 0000000..0315d10 --- /dev/null +++ b/Shade/CoreML/ObjectDetection/ObjectDetection.swift @@ -0,0 +1,98 @@ +// +// ObjectDetection.swift +// Shade +// +// Created by Ahmed Ragab on 23/10/2024. +// + +import Foundation +import SwiftUI +import CoreML +import Vision + +struct ObjectDetectionView: View { + @State private var detectedObjects: [VNRecognizedObjectObservation] = [] + var image = NSImage(resource: .city) + var body: some View { + VStack { + Text("Object Detection") + .font(.title) + .padding() + + // Displaying the image with detected bounding boxes + Image(nsImage: image) + .resizable() + .scaledToFit() + .overlay( + GeometryReader { geo in + ForEach(detectedObjects, id: \.self) { object in + let boundingBox = object.boundingBox + let rect = convertBoundingBox(boundingBox, in: geo.frame(in: .global).size) + + // Drawing the bounding box as a red rectangle + Rectangle() + .stroke(Color.red, lineWidth: 2) + .frame(width: rect.width, height: rect.height) + .position(x: rect.midX, y: rect.midY) + + // Optional: Displaying label and confidence + if let label = object.labels.first { + Text("\(label.identifier) \(String(format: "%.2f", label.confidence))") + .foregroundColor(.white) + .background(Color.black.opacity(0.7)) + .position(x: rect.midX, y: rect.minY - 10) + } + } + } + ) + .task { + Task { + performObjectDetection(image:image) + } + } + + } + + + + } + + func performObjectDetection(image: NSImage) { + + + guard let cgImage = image.CGImage else { return } + + // Load the Core ML YOLO model + let config = MLModelConfiguration() + config.computeUnits = .cpuAndGPU + guard let model = try? VNCoreMLModel(for: yolo11m_int8(configuration: config).model) else { + fatalError("Failed to load YOLO model") + } + + // Create a Vision request for object detection + let request = VNCoreMLRequest(model: model) { request, error in + if let results = request.results as? [VNRecognizedObjectObservation] { + DispatchQueue.main.async { + detectedObjects = results + } + } + } + + // Create an image request handler + let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) + try? handler.perform([request]) + } + + func convertBoundingBox(_ boundingBox: CGRect, in size: CGSize) -> CGRect { + // Convert the bounding box from normalized coordinates to view coordinates + let width = boundingBox.width * size.width + let height = boundingBox.height * size.height + let originX = boundingBox.minX * size.width + let originY = (1 - boundingBox.maxY) * size.height // Invert Y-axis for Vision + return CGRect(x: originX, y: originY, width: width, height: height) + } +} + +#Preview { + ObjectDetectionView() +} diff --git a/Shade/CoreML/ObjectDetection/yolo11m-int8.mlpackage/Data/com.apple.CoreML/model.mlmodel b/Shade/CoreML/ObjectDetection/yolo11m-int8.mlpackage/Data/com.apple.CoreML/model.mlmodel new file mode 100644 index 0000000..d0d75bf Binary files /dev/null and b/Shade/CoreML/ObjectDetection/yolo11m-int8.mlpackage/Data/com.apple.CoreML/model.mlmodel differ diff --git a/Shade/CoreML/ObjectDetection/yolo11m-int8.mlpackage/Data/com.apple.CoreML/weights/weight.bin b/Shade/CoreML/ObjectDetection/yolo11m-int8.mlpackage/Data/com.apple.CoreML/weights/weight.bin new file mode 100644 index 0000000..51b1dac Binary files /dev/null and b/Shade/CoreML/ObjectDetection/yolo11m-int8.mlpackage/Data/com.apple.CoreML/weights/weight.bin differ diff --git a/Shade/CoreML/ObjectDetection/yolo11m-int8.mlpackage/Manifest.json b/Shade/CoreML/ObjectDetection/yolo11m-int8.mlpackage/Manifest.json new file mode 100644 index 0000000..8cd7537 --- /dev/null +++ b/Shade/CoreML/ObjectDetection/yolo11m-int8.mlpackage/Manifest.json @@ -0,0 +1,18 @@ +{ + "fileFormatVersion": "1.0.0", + "itemInfoEntries": { + "096C55D7-931E-41B6-80C2-01AE33E06096": { + "author": "com.apple.CoreML", + "description": "CoreML Model Specification", + "name": "model.mlmodel", + "path": "com.apple.CoreML/model.mlmodel" + }, + "2F6C5D99-130A-4157-9DD6-0A8861AFDF74": { + "author": "com.apple.CoreML", + "description": "CoreML Model Weights", + "name": "weights", + "path": "com.apple.CoreML/weights" + } + }, + "rootModelIdentifier": "096C55D7-931E-41B6-80C2-01AE33E06096" +} diff --git a/Shade/CoreML/PersonsBackgroundsRemoval/DeepLabV3 model.mlmodel b/Shade/CoreML/PersonsBackgroundsRemoval/DeepLabV3 model.mlmodel new file mode 100644 index 0000000..6a2df3f Binary files /dev/null and b/Shade/CoreML/PersonsBackgroundsRemoval/DeepLabV3 model.mlmodel differ diff --git a/Shade/CoreML/PersonsBackgroundsRemoval/PersonsSegmentaionView.swift b/Shade/CoreML/PersonsBackgroundsRemoval/PersonsSegmentaionView.swift new file mode 100644 index 0000000..c7ac008 --- /dev/null +++ b/Shade/CoreML/PersonsBackgroundsRemoval/PersonsSegmentaionView.swift @@ -0,0 +1,205 @@ +// +// ContentView.swift +// CoreMLBackgroundChangeSwiftUI +// +// Created by Anupam Chugh on 27/05/21. +// + +import SwiftUI +import CoreML +import CoreMedia +import Vision + +extension NSImage { + class func imageFromColor(color: NSColor, scale: CGFloat) -> NSImage { + let baseSize = CGSize(width: 1, height: 1) + let scaledSize = CGSize(width: baseSize.width * scale, height: baseSize.height * scale) + + let image = NSImage(size: scaledSize) + image.lockFocus() + color.drawSwatch(in: NSRect(origin: .zero, size: scaledSize)) + image.unlockFocus() + + return image + } + /// Creates an `NSImage` filled with the given color and size. + class func imageFromColor(color: NSColor, size: NSSize) -> NSImage { + let image = NSImage(size: size) + image.lockFocus() + color.drawSwatch(in: NSRect(origin: .zero, size: size)) + image.unlockFocus() + return image + } + + func resizedImage(for newSize: CGSize) -> NSImage? { + guard let bitmapRep = NSBitmapImageRep( + bitmapDataPlanes: nil, + pixelsWide: Int(newSize.width), + pixelsHigh: Int(newSize.height), + bitsPerSample: 8, + samplesPerPixel: 4, + hasAlpha: true, + isPlanar: false, + colorSpaceName: .deviceRGB, + bytesPerRow: 0, + bitsPerPixel: 0) else { + return nil + } + + bitmapRep.size = newSize + + let resizedImage = NSImage(size: newSize) + resizedImage.addRepresentation(bitmapRep) + + resizedImage.lockFocus() + let context = NSGraphicsContext.current + context?.imageInterpolation = .high + self.draw(in: NSRect(origin: .zero, size: newSize), + from: NSRect(origin: .zero, size: self.size), + operation: .sourceOver, + fraction: 1.0) + resizedImage.unlockFocus() + + return resizedImage + } +} +#if canImport(UIKit) +extension UIImage { + class func imageFromColor(color: Color, size: CGSize=CGSize(width: 1, height: 1), scale: CGFloat) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(size, false, scale) + color.setFill() + UIRectFill(CGRect(origin: CGPoint.zero, size: size)) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } + + func resizedImage(for size: CGSize) -> UIImage? { + let image = self.cgImage + print(size) + let context = CGContext(data: nil, + width: Int(size.width), + height: Int(size.height), + bitsPerComponent: image!.bitsPerComponent, + bytesPerRow: Int(size.width), + space: image?.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!, + bitmapInfo: image!.bitmapInfo.rawValue) + context?.interpolationQuality = .high + context?.draw(image!, in: CGRect(origin: .zero, size: size)) + + guard let scaledImage = context?.makeImage() else { return nil } + + return UIImage(cgImage: scaledImage) + } + + + convenience init?(size: CGSize, gradientPoints: [GradientPoint], scale : CGFloat) { + UIGraphicsBeginImageContextWithOptions(size, false, scale) + + guard let context = UIGraphicsGetCurrentContext() else { return nil } // If the size is zero, the context will be nil. + guard let gradient = CGGradient(colorSpace: CGColorSpaceCreateDeviceRGB(), colorComponents: gradientPoints.compactMap { $0.color.cgColor.components }.flatMap { $0 }, locations: gradientPoints.map { $0.location }, count: gradientPoints.count) else { + return nil + } + + context.drawLinearGradient(gradient, start: CGPoint.zero, end: CGPoint(x: 0, y: size.height), options: CGGradientDrawingOptions()) + guard let image = UIGraphicsGetImageFromCurrentImageContext()?.cgImage else { return nil } + self.init(cgImage: image) + defer { UIGraphicsEndImageContext() } + } + +} + + + + +extension UIImage { + func withAlphaComponent(_ alpha: CGFloat) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(size, false, scale) + defer { UIGraphicsEndImageContext() } + + draw(at: .zero, blendMode: .normal, alpha: alpha) + return UIGraphicsGetImageFromCurrentImageContext() + } +} +#endif +struct PersonsSegmentaionView: View { + + @State var outputImage : NSImage = NSImage.imageFromColor(color: .red, scale: 1) + @State var inputImage : NSImage = NSImage(resource: .person) + + + var body: some View { + + + + + VStack{ + + HStack{ + + Image(nsImage: inputImage) + .resizable() + .aspectRatio(contentMode: .fit) + + Spacer() + Image(nsImage: outputImage) + .resizable() + .aspectRatio(contentMode: .fit) + + } + + Button(action: {runVisionRequest()}, label: { + Text("Run Image Segmentation") + }) + .padding() + + } + } + + func runVisionRequest() { + + guard let model = try? VNCoreMLModel(for: DeepLabV3_model(configuration: .init()).model) + else { return } + + let request = VNCoreMLRequest(model: model, completionHandler: visionRequestDidComplete) + request.imageCropAndScaleOption = .scaleFill + DispatchQueue.global().async { + + let handler = VNImageRequestHandler(cgImage: inputImage.CGImage!, options: [:]) + + do { + try handler.perform([request]) + }catch { + print(error) + } + } + } + + + func visionRequestDidComplete(request: VNRequest, error: Error?) { + DispatchQueue.main.async { + if let observations = request.results as? [VNCoreMLFeatureValueObservation], + let segmentationmap = observations.first?.featureValue.multiArrayValue { + + let segmentationMask = segmentationmap.image(min: 0, max: 1) + + self.outputImage = segmentationMask!.resizedImage(for: self.inputImage.size)! + + + + } + } + } +} + +struct GradientPoint { + var location: CGFloat + var color: NSColor +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + PersonsSegmentaionView() + .padding() + } +} diff --git a/Shade/CoreML/SuperResulotion/RealesrGAN 512.mlmodel b/Shade/CoreML/SuperResulotion/RealesrGAN 512.mlmodel new file mode 100644 index 0000000..bfa2678 Binary files /dev/null and b/Shade/CoreML/SuperResulotion/RealesrGAN 512.mlmodel differ diff --git a/Shade/CoreML/SuperResulotion/SuperResulotionView.swift b/Shade/CoreML/SuperResulotion/SuperResulotionView.swift new file mode 100644 index 0000000..7d75527 --- /dev/null +++ b/Shade/CoreML/SuperResulotion/SuperResulotionView.swift @@ -0,0 +1,96 @@ +// +// SuperResulotionView.swift +// Shade +// +// Created by Ahmed Ragab on 23/10/2024. +// + +import Foundation +import CoreML +import Accelerate +import Vision +import SwiftUI + +struct SuperResulotionView: View { + @State var inputImage: NSImage = NSImage(resource: .city) + @State var outImage: NSImage = NSImage(resource: .person2) + var body: some View { + VStack { + HStack(spacing:8) { + Image(nsImage: inputImage) + .resizable() + .scaledToFit() + + + Image(nsImage: outImage) + .resizable() + .scaledToFit() + } + Button { + superResulotion() + } label: { + Text("Super resulotion model") + } + + } + } + + + func superResulotion() { + Task { + let config = MLModelConfiguration() + config.computeUnits = .cpuAndGPU + guard let model = try? VNCoreMLModel(for: RealesrGAN_512(configuration: config).model) else { + fatalError("could not load ml model") + } + + + let request = VNCoreMLRequest(model: model) { request, error in + if let result = request.results as? [VNPixelBufferObservation] { + DispatchQueue.main.async { + let image = pixelBufferToCGImage(pixelBuffer:result.first!.pixelBuffer)! + outImage = NSImage(cgImage:image, size: NSSize(width: image.frame.width, height: image.frame.height)) + } + } else if let error = error { + print("Error during Core ML request: \(error.localizedDescription)") + } + } + + guard let inputImage = inputImage.CGImage else { + fatalError("could not load input image") + } + + let handler = VNImageRequestHandler(cgImage: inputImage,options: [:]) + + do { + try handler.perform([request]) + } catch { + print("Failed to perform Core ML request: \(error.localizedDescription)") + } + } + } + + func pixelBufferToCGImage(pixelBuffer: CVPixelBuffer) -> CGImage? { + // Create a CIImage from the CVPixelBuffer + let ciImage = CIImage(cvPixelBuffer: pixelBuffer) + + // Create a CIContext to render the CIImage + let ciContext = CIContext(options: nil) + + // Get the dimensions of the pixel buffer + let width = CVPixelBufferGetWidth(pixelBuffer) + let height = CVPixelBufferGetHeight(pixelBuffer) + + // Render the CIImage to a CGImage + let cgImage = ciContext.createCGImage(ciImage, from: CGRect(x: 0, y: 0, width: width, height: height)) + + return cgImage + } +} + + +#Preview { + SuperResulotionView() +} + + diff --git a/Shade/Extensions/CoreMLHelpers.swift b/Shade/Extensions/CoreMLHelpers.swift new file mode 100644 index 0000000..9e0dcc8 --- /dev/null +++ b/Shade/Extensions/CoreMLHelpers.swift @@ -0,0 +1,581 @@ +// +// CoreMLHelpers.swift +// Shade +// +// Created by Ahmed Ragab on 23/10/2024. +// + +import Foundation +import Accelerate +import CoreML + +#if canImport(UIKit) +import UIKit +typealias UniversalImage = UIImage +#elseif canImport(AppKit) +import AppKit +typealias UniversalImage = NSImage +#endif +// +// CoreMLHelpers.swift +// Inpating +// +// Created by 間嶋大輔 on 2023/01/12. +// + +import Accelerate +import CoreML + +public protocol MultiArrayType: Comparable { + static var multiArrayDataType: MLMultiArrayDataType { get } + static func +(lhs: Self, rhs: Self) -> Self + static func -(lhs: Self, rhs: Self) -> Self + static func *(lhs: Self, rhs: Self) -> Self + static func /(lhs: Self, rhs: Self) -> Self + init(_: Int) + var toUInt8: UInt8 { get } +} + +extension Double: MultiArrayType { + public static var multiArrayDataType: MLMultiArrayDataType { return .double } + public var toUInt8: UInt8 { return UInt8(self) } +} + +extension Float: MultiArrayType { + public static var multiArrayDataType: MLMultiArrayDataType { return .float32 } + public var toUInt8: UInt8 { return UInt8(self) } +} + +extension Int32: MultiArrayType { + public static var multiArrayDataType: MLMultiArrayDataType { return .int32 } + public var toUInt8: UInt8 { return UInt8(self) } +} + +extension MLMultiArray { + /** + Converts the multi-array to a CGImage. + The multi-array must have at least 2 dimensions for a grayscale image, or + at least 3 dimensions for a color image. + The default expected shape is (height, width) or (channels, height, width). + However, you can change this using the `axes` parameter. For example, if + the array shape is (1, height, width, channels), use `axes: (3, 1, 2)`. + If `channel` is not nil, only converts that channel to a grayscale image. + This lets you visualize individual channels from a multi-array with more + than 4 channels. + Otherwise, converts all channels. In this case, the number of channels in + the multi-array must be 1 for grayscale, 3 for RGB, or 4 for RGBA. + Use the `min` and `max` parameters to put the values from the array into + the range [0, 255], if not already: + - `min`: should be the smallest value in the data; this will be mapped to 0. + - `max`: should be the largest value in the data; will be mapped to 255. + For example, if the range of the data in the multi-array is [-1, 1], use + `min: -1, max: 1`. If the range is already [0, 255], then use the defaults. + */ + public func cgImage(min: Double = 0, + max: Double = 255, + channel: Int? = nil, + axes: (Int, Int, Int)? = nil) -> CGImage? { + switch self.dataType { + case .double: + return _image(min: min, max: max, channel: channel, axes: axes) + case .float32: + return _image(min: Float(min), max: Float(max), channel: channel, axes: axes) + case .int32: + return _image(min: Int32(min), max: Int32(max), channel: channel, axes: axes) + @unknown default: + fatalError("Unsupported data type \(dataType.rawValue)") + } + } + + /** + Helper function that allows us to use generics. The type of `min` and `max` + is also the dataType of the MLMultiArray. + */ + private func _image(min: T, + max: T, + channel: Int?, + axes: (Int, Int, Int)?) -> CGImage? { + if let (b, w, h, c) = toRawBytes(min: min, max: max, channel: channel, axes: axes) { + if c == 1 { + return CGImage.fromByteArrayGray(b, width: w, height: h) + } else { + return CGImage.fromByteArrayRGBA(b, width: w, height: h) + } + } + return nil + } + + /** + Converts the multi-array into an array of RGBA or grayscale pixels. + - Note: This is not particularly fast, but it is flexible. You can change + the loops to convert the multi-array whichever way you please. + - Note: The type of `min` and `max` must match the dataType of the + MLMultiArray object. + - Returns: tuple containing the RGBA bytes, the dimensions of the image, + and the number of channels in the image (1, 3, or 4). + */ + public func toRawBytes(min: T, + max: T, + channel: Int? = nil, + axes: (Int, Int, Int)? = nil) + -> (bytes: [UInt8], width: Int, height: Int, channels: Int)? { + // MLMultiArray with unsupported shape? + if shape.count < 2 { + print("Cannot convert MLMultiArray of shape \(shape) to image") + return nil + } + + // Figure out which dimensions to use for the channels, height, and width. + let channelAxis: Int + let heightAxis: Int + let widthAxis: Int + if let axes = axes { + channelAxis = axes.0 + heightAxis = axes.1 + widthAxis = axes.2 + guard channelAxis >= 0 && channelAxis < shape.count && + heightAxis >= 0 && heightAxis < shape.count && + widthAxis >= 0 && widthAxis < shape.count else { + print("Invalid axes \(axes) for shape \(shape)") + return nil + } + } else if shape.count == 2 { + // Expected shape for grayscale is (height, width) + heightAxis = 0 + widthAxis = 1 + channelAxis = -1 // Never be used + } else { + // Expected shape for color is (channels, height, width) + channelAxis = 0 + heightAxis = 1 + widthAxis = 2 + } + + let height = self.shape[heightAxis].intValue + let width = self.shape[widthAxis].intValue + let yStride = self.strides[heightAxis].intValue + let xStride = self.strides[widthAxis].intValue + + let channels: Int + let cStride: Int + let bytesPerPixel: Int + let channelOffset: Int + + // MLMultiArray with just two dimensions is always grayscale. (We ignore + // the value of channelAxis here.) + if shape.count == 2 { + channels = 1 + cStride = 0 + bytesPerPixel = 1 + channelOffset = 0 + + // MLMultiArray with more than two dimensions can be color or grayscale. + } else { + let channelDim = self.shape[channelAxis].intValue + if let channel = channel { + if channel < 0 || channel >= channelDim { + print("Channel must be -1, or between 0 and \(channelDim - 1)") + return nil + } + channels = 1 + bytesPerPixel = 1 + channelOffset = channel + } else if channelDim == 1 { + channels = 1 + bytesPerPixel = 1 + channelOffset = 0 + } else { + if channelDim != 3 && channelDim != 4 { + print("Expected channel dimension to have 1, 3, or 4 channels, got \(channelDim)") + return nil + } + channels = channelDim + bytesPerPixel = 4 + channelOffset = 0 + } + cStride = self.strides[channelAxis].intValue + } + + // Allocate storage for the RGBA or grayscale pixels. Set everything to + // 255 so that alpha channel is filled in if only 3 channels. + let count = height * width * bytesPerPixel + var pixels = [UInt8](repeating: 255, count: count) + + // Grab the pointer to MLMultiArray's memory. + var ptr = UnsafeMutablePointer(OpaquePointer(self.dataPointer)) + ptr = ptr.advanced(by: channelOffset * cStride) + + // Loop through all the pixels and all the channels and copy them over. + for c in 0.. CGImage? { + assert(features.dataType == .float32) + assert(features.shape.count == 3) + + let ptr = UnsafeMutablePointer(OpaquePointer(features.dataPointer)) + + let height = features.shape[1].intValue + let width = features.shape[2].intValue + let channelStride = features.strides[0].intValue + let rowStride = features.strides[1].intValue + let srcRowBytes = rowStride * MemoryLayout.stride + + var blueBuffer = vImage_Buffer(data: ptr, + height: vImagePixelCount(height), + width: vImagePixelCount(width), + rowBytes: srcRowBytes) + var greenBuffer = vImage_Buffer(data: ptr.advanced(by: channelStride), + height: vImagePixelCount(height), + width: vImagePixelCount(width), + rowBytes: srcRowBytes) + var redBuffer = vImage_Buffer(data: ptr.advanced(by: channelStride * 2), + height: vImagePixelCount(height), + width: vImagePixelCount(width), + rowBytes: srcRowBytes) + + let destRowBytes = width * 4 + var pixels = [UInt8](repeating: 0, count: height * destRowBytes) + var destBuffer = vImage_Buffer(data: &pixels, + height: vImagePixelCount(height), + width: vImagePixelCount(width), + rowBytes: destRowBytes) + + let error = vImageConvert_PlanarFToBGRX8888(&blueBuffer, + &greenBuffer, + &redBuffer, + Pixel_8(255), + &destBuffer, + [max, max, max], + [min, min, min], + vImage_Flags(0)) + if error == kvImageNoError { + return CGImage.fromByteArrayRGBA(pixels, width: width, height: height) + } else { + return nil + } +} + +extension MLMultiArray { + public func image(min: Double = 0, + max: Double = 255, + channel: Int? = nil, + axes: (Int, Int, Int)? = nil) -> NSImage? { + guard let cgImg = cgImage(min: min, max: max, channel: channel, axes: axes) else { + return nil + } + + let size = NSSize(width: cgImg.width, height: cgImg.height) + let nsImage = NSImage(cgImage: cgImg, size: size) + + return nsImage + } +} + +#if canImport(UIKit) + +import UIKit + +extension MLMultiArray { + public func image(min: Double = 0, + max: Double = 255, + channel: Int? = nil, + axes: (Int, Int, Int)? = nil) -> UIImage? { + let cgImg = cgImage(min: min, max: max, channel: channel, axes: axes) + return cgImg.map { UIImage(cgImage: $0) } + } +} + +public func createUIImage(fromFloatArray features: MLMultiArray, + min: Float = 0, + max: Float = 255) -> UIImage? { + let cgImg = createCGImage(fromFloatArray: features, min: min, max: max) + return cgImg.map { UIImage(cgImage: $0) } +} + +#endif + +public func clamp(_ x: T, min: T, max: T) -> T { + if x < min { return min } + if x > max { return max } + return x +} + +import CoreGraphics + +extension CGImage { + /** + Converts the image into an array of RGBA bytes. + */ + @nonobjc public func toByteArrayRGBA() -> [UInt8] { + var bytes = [UInt8](repeating: 0, count: width * height * 4) + bytes.withUnsafeMutableBytes { ptr in + if let colorSpace = colorSpace, + let context = CGContext( + data: ptr.baseAddress, + width: width, + height: height, + bitsPerComponent: bitsPerComponent, + bytesPerRow: bytesPerRow, + space: colorSpace, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) { + let rect = CGRect(x: 0, y: 0, width: width, height: height) + context.draw(self, in: rect) + } + } + return bytes + } + + /** + Creates a new CGImage from an array of RGBA bytes. + */ + @nonobjc public class func fromByteArrayRGBA(_ bytes: [UInt8], + width: Int, + height: Int) -> CGImage? { + return fromByteArray(bytes, width: width, height: height, + bytesPerRow: width * 4, + colorSpace: CGColorSpaceCreateDeviceRGB(), + alphaInfo: .premultipliedLast) + } + + /** + Creates a new CGImage from an array of grayscale bytes. + */ + @nonobjc public class func fromByteArrayGray(_ bytes: [UInt8], + width: Int, + height: Int) -> CGImage? { + return fromByteArray(bytes, width: width, height: height, + bytesPerRow: width, + colorSpace: CGColorSpaceCreateDeviceGray(), + alphaInfo: .none) + } + + @nonobjc class func fromByteArray(_ bytes: [UInt8], + width: Int, + height: Int, + bytesPerRow: Int, + colorSpace: CGColorSpace, + alphaInfo: CGImageAlphaInfo) -> CGImage? { + return bytes.withUnsafeBytes { ptr in + let context = CGContext(data: UnsafeMutableRawPointer(mutating: ptr.baseAddress!), + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: bytesPerRow, + space: colorSpace, + bitmapInfo: alphaInfo.rawValue) + return context?.makeImage() + } + } +} + + +extension CGImage { + var frame: CGRect { + return CGRect(x: 0, y: 0, width: self.width, height: self.height) + } + + func toBGR()->CGImage{ + let ciImage = CIImage(cgImage: self) + let kernelStr: String = """ + kernel vec4 swapRedAndGreenAmount(__sample s) { + return s.bgra; + } + """ + let ctx = CIContext(options: nil) + let swapKernel = CIColorKernel( source: + "kernel vec4 swapRedAndGreenAmount(__sample s) {" + + "return s.bgra;" + + "}" + ) + let ciOutput = swapKernel?.apply(extent: (ciImage.extent), arguments: [ciImage as Any]) + let cgOut:CGImage = ctx.createCGImage(ciOutput!, from: ciOutput!.extent)! + return cgOut + + } +} + +#if canImport(UIKIt) +extension UIImage { + func mlMultiArray(scale preprocessScale:Double=1/255, rBias preprocessRBias:Double=0, gBias preprocessGBias:Double=0, bBias preprocessBBias:Double=0) -> MLMultiArray { + let imagePixel = self.getPixelRgb(scale: preprocessScale, rBias: preprocessRBias, gBias: preprocessGBias, bBias: preprocessBBias) +// let size = self.size + let imagePointer : UnsafePointer = UnsafePointer(imagePixel) + let mlArray = try! MLMultiArray(shape: [1,3, NSNumber(value: Float(512)), NSNumber(value: Float(512))], dataType: MLMultiArrayDataType.double) + mlArray.dataPointer.initializeMemory(as: Double.self, from: imagePointer, count: imagePixel.count) + + return mlArray + } + + func mlMultiArrayGrayScale(scale preprocessScale:Double=1/255,bias preprocessBias:Double=0) -> MLMultiArray { + let imagePixel = self.getPixelGrayScale(scale: preprocessScale, bias: preprocessBias) +// let size = self.size + let imagePointer : UnsafePointer = UnsafePointer(imagePixel) + let mlArray = try! MLMultiArray(shape: [1,1, NSNumber(value: Float(512)), NSNumber(value: Float(512))], dataType: MLMultiArrayDataType.double) + mlArray.dataPointer.initializeMemory(as: Double.self, from: imagePointer, count: imagePixel.count) + return mlArray + } + + func mlMultiArrayComposite(outImage out:UIImage, inputImage input:UIImage, maskImage mask: UIImage, scale preprocessScale:Double=1/255, rBias preprocessRBias:Double=0, gBias preprocessGBias:Double=0, bBias preprocessBBias:Double=0) -> MLMultiArray { + let imagePixel = self.getMaskedPixelRgb(out: out, input: input, mask: mask) +// let size = self.size + let imagePointer : UnsafePointer = UnsafePointer(imagePixel) + let mlArray = try! MLMultiArray(shape: [1,3, NSNumber(value: Float(512)), NSNumber(value: Float(512))], dataType: MLMultiArrayDataType.double) + mlArray.dataPointer.initializeMemory(as: Double.self, from: imagePointer, count: imagePixel.count) + + return mlArray + } + + func getMaskedPixelRgb(out: UIImage,input: UIImage, mask:UIImage, scale preprocessScale:Double=1, rBias preprocessRBias:Double=0, gBias preprocessGBias:Double=0, bBias preprocessBBias:Double=0) -> [Double] + { + guard let outCGImage = out.cgImage?.resize(size: CGSize(width: 512, height: 512)) else { + return [] + } + let outbytesPerRow = outCGImage.bytesPerRow + let outwidth = outCGImage.width + let outheight = outCGImage.height + let outbytesPerPixel = 4 + let outpixelData = outCGImage.dataProvider!.data! as Data + + guard let inputCGImage = input.cgImage?.resize(size: CGSize(width: 512, height: 512)) else { + return [] + } + let inputpixelData = inputCGImage.dataProvider!.data! as Data + + guard let maskCgImage = mask.cgImage?.resize(size: CGSize(width: 512, height: 512)) else { + return [] + } + let maskBytesPerRow = maskCgImage.bytesPerRow + let maskBytesPerPixel = 4 + let maskPixelData = maskCgImage.dataProvider!.data! as Data + + var r_buf : [Double] = [] + var g_buf : [Double] = [] + var b_buf : [Double] = [] + + for j in 0.. 0 { + r_buf.append(Double(r*preprocessScale)+preprocessRBias) + g_buf.append(Double(g*preprocessScale)+preprocessGBias) + b_buf.append(Double(b*preprocessScale)+preprocessBBias) + } else { + r_buf.append(Double(bgr*preprocessScale)+preprocessRBias) + g_buf.append(Double(bgg*preprocessScale)+preprocessGBias) + b_buf.append(Double(bgb*preprocessScale)+preprocessBBias) + + } + } + } + + return ((r_buf + g_buf) + b_buf) + } + + func getPixelRgb(scale preprocessScale:Double=1/255, rBias preprocessRBias:Double=0, gBias preprocessGBias:Double=0, bBias preprocessBBias:Double=0) -> [Double] + { + guard let cgImage = self.cgImage?.resize(size: CGSize(width: 512, height: 512)) else { + return [] + } + let bytesPerRow = cgImage.bytesPerRow + let width = cgImage.width + let height = cgImage.height + let bytesPerPixel = 4 + let pixelData = cgImage.dataProvider!.data! as Data + + var r_buf : [Double] = [] + var g_buf : [Double] = [] + var b_buf : [Double] = [] + + for j in 0.. [Double] + { + guard let cgImage = self.cgImage?.resize(size: CGSize(width: 512, height: 512)) else { + return [] + } + let bytesPerRow = cgImage.bytesPerRow + let width = cgImage.width + let height = cgImage.height + let bytesPerPixel = 4 + let pixelData = cgImage.dataProvider!.data! as Data + + var buf : [Double] = [] + + for j in 0.. CGImage? { + let width: Int = Int(size.width) + let height: Int = Int(size.height) + + let bytesPerPixel = self.bitsPerPixel / self.bitsPerComponent + let destBytesPerRow = width * bytesPerPixel + + + guard let colorSpace = self.colorSpace else { return nil } + guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: self.bitsPerComponent, bytesPerRow: destBytesPerRow, space: colorSpace, bitmapInfo: self.alphaInfo.rawValue) else { return nil } + + context.interpolationQuality = .high + context.draw(self, in: CGRect(x: 0, y: 0, width: width, height: height)) + + return context.makeImage() + } +} + diff --git a/Shade/Extensions/Extensions.swift b/Shade/Extensions/Extensions.swift new file mode 100644 index 0000000..eb66ebd --- /dev/null +++ b/Shade/Extensions/Extensions.swift @@ -0,0 +1,192 @@ +// +// Extensions.swift +// Shade +// +// Created by Ahmed Ragab on 06/10/2024. +// + +import SwiftUI +import Foundation + +#if canImport(UIKit) +import UIKit +extension UIImage { + func pixelBuffer() -> CVPixelBuffer? { + let options: [String: Any] = [ + kCVPixelBufferCGImageCompatibilityKey as String: kCFBooleanTrue!, + kCVPixelBufferCGBitmapContextCompatibilityKey as String: kCFBooleanTrue! + ] + var pixelBuffer: CVPixelBuffer? + let width = Int(size.width) + let height = Int(size.height) + + // Create the pixel buffer + let status = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32ARGB, options as CFDictionary, &pixelBuffer) + guard status == kCVReturnSuccess, let buffer = pixelBuffer else { + print("Error: Unable to create CVPixelBuffer") + return nil + } + + // Lock the pixel buffer base address + CVPixelBufferLockBaseAddress(buffer, .init(rawValue: 0)) + + // Get the base address of the pixel buffer + guard let context = CGContext(data: CVPixelBufferGetBaseAddress(buffer), + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: CVPixelBufferGetBytesPerRow(buffer), + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue) else { + CVPixelBufferUnlockBaseAddress(buffer, .init(rawValue: 0)) + print("Error: Unable to create CGContext") + return nil + } + + // Draw the UIImage into the pixel buffer + UIGraphicsPushContext(context) + draw(in: CGRect(origin: .zero, size: size)) + UIGraphicsPopContext() + + // Unlock the pixel buffer base address + CVPixelBufferUnlockBaseAddress(buffer, .init(rawValue: 0)) + + return buffer + } +} +#endif + +#if canImport(AppKit) +import AppKit +extension NSImage { + func pixelBuffer() -> CVPixelBuffer? { + guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { + print("Error: Unable to convert NSImage to CGImage") + return nil + } + + let options: [String: Any] = [ + kCVPixelBufferCGImageCompatibilityKey as String: kCFBooleanTrue!, + kCVPixelBufferCGBitmapContextCompatibilityKey as String: kCFBooleanTrue! + ] + var pixelBuffer: CVPixelBuffer? + let width = Int(cgImage.width) + let height = Int(cgImage.height) + + // Create the pixel buffer + let status = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32ARGB, options as CFDictionary, &pixelBuffer) + guard status == kCVReturnSuccess, let buffer = pixelBuffer else { + print("Error: Unable to create CVPixelBuffer") + return nil + } + + // Lock the pixel buffer base address + CVPixelBufferLockBaseAddress(buffer, .init(rawValue: 0)) + + // Get the base address of the pixel buffer + guard let context = CGContext(data: CVPixelBufferGetBaseAddress(buffer), + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: CVPixelBufferGetBytesPerRow(buffer), + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue) else { + CVPixelBufferUnlockBaseAddress(buffer, .init(rawValue: 0)) + print("Error: Unable to create CGContext") + return nil + } + + // Draw the CGImage into the pixel buffer + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) + + // Unlock the pixel buffer base address + CVPixelBufferUnlockBaseAddress(buffer, .init(rawValue: 0)) + + return buffer + } +} +#endif + +extension Color { + func toSimdFloat4() -> SIMD4 { + // Convert SwiftUI Color to NSColor + let nsColor = NSColor(self) + + // Extract RGBA components + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + + // Ensure the color is in device RGB color space + if ((nsColor.usingColorSpace(.deviceRGB)?.getRed(&red, green: &green, blue: &blue, alpha: &alpha)) != nil) == true { + // Convert CGFloat (0-1) to Float (0-1) and create SIMD4 + return SIMD4(Float(red), Float(green), Float(blue), Float(alpha)) + } else { + // Fallback to transparent black if conversion fails + return SIMD4(0, 0, 0, 0) + } + } +} + + + +extension NSColor { + var float4: SIMD4 { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + return SIMD4(Float(red), Float(green), Float(blue), Float(alpha)) + } +} + +extension NSColor { + + convenience init(hex: String) { + let trimHex = hex.trimmingCharacters(in: .whitespacesAndNewlines) + let dropHash = String(trimHex.dropFirst()).trimmingCharacters(in: .whitespacesAndNewlines) + let hexString = trimHex.starts(with: "#") ? dropHash : trimHex + let ui64 = UInt64(hexString, radix: 16) + let value = ui64 != nil ? Int(ui64!) : 0 + // #RRGGBB + var components = ( + R: CGFloat((value >> 16) & 0xff) / 255, + G: CGFloat((value >> 08) & 0xff) / 255, + B: CGFloat((value >> 00) & 0xff) / 255, + a: CGFloat(1) + ) + if String(hexString).count == 8 { + // #RRGGBBAA + components = ( + R: CGFloat((value >> 24) & 0xff) / 255, + G: CGFloat((value >> 16) & 0xff) / 255, + B: CGFloat((value >> 08) & 0xff) / 255, + a: CGFloat((value >> 00) & 0xff) / 255 + ) + } + self.init(red: components.R, green: components.G, blue: components.B, alpha: components.a) +} + +func toHex(alpha: Bool = false) -> String? { + guard let components = cgColor.components, components.count >= 3 else { + return nil + } + + let r = Float(components[0]) + let g = Float(components[1]) + let b = Float(components[2]) + var a = Float(1.0) + + if components.count >= 4 { + a = Float(components[3]) + } + + if alpha { + return String(format: "%02lX%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255), lroundf(a * 255)) + } else { + return String(format: "%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255)) + } +} +} diff --git a/Shade/MetalFiles/ColorAdjustment/ChannelMixer.metal b/Shade/MetalFiles/ColorAdjustment/ChannelMixer.metal new file mode 100644 index 0000000..94d02d3 --- /dev/null +++ b/Shade/MetalFiles/ColorAdjustment/ChannelMixer.metal @@ -0,0 +1,46 @@ +// +// ChannelMixer.metal +// Shade +// +// Created by Ahmed Ragab on 06/10/2024. +// + +#include +using namespace metal; +struct VertexOut { + float4 postion [[position]]; + float2 textureCoordinate; +}; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 textureCoordinate [[attribute(1)]]; +}; + +// Vertex shader: transforms vertices and passes texture coordinates to the fragment shader +vertex VertexOut channelMixerVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + + // Pass through the position (already in clip space for a full-screen quad) + out.postion = in.position; + + // Pass the texture coordinates to the fragment shader + out.textureCoordinate = in.textureCoordinate; + + return out; +} + + +fragment float4 channelMixerFragment(VertexOut out [[stage_in]], + texture2d inTexture [[texture(0)]], + constant float3 &redMix [[buffer(0)]], + constant float3 &greenMix [[buffer(1)]], + constant float3 &blueMix [[buffer(2)]]) { + constexpr sampler textureSampler(mag_filter::linear, min_filter::linear); + float4 color = inTexture.sample(textureSampler,out.textureCoordinate); + //Mix coloer channels + float newRed = dot(float3(color.r,color.g,color.b), redMix); + float newGreen = dot(float3(color.r,color.g,color.b), greenMix); + float newBlue = dot(float3(color.r,color.g,color.b), blueMix); + return float4(newRed,newGreen,newBlue,color.a); +} diff --git a/Shade/MetalFiles/ColorAdjustment/ColorMonochrome.metal b/Shade/MetalFiles/ColorAdjustment/ColorMonochrome.metal new file mode 100644 index 0000000..cb85d98 --- /dev/null +++ b/Shade/MetalFiles/ColorAdjustment/ColorMonochrome.metal @@ -0,0 +1,47 @@ +// +// ColorMonochrome.metal +// Shade +// +// Created by Ahmed Ragab on 06/10/2024. +// + +#include +using namespace metal; +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoord [[attribute(1)]]; +}; + +struct VertexOut { + float4 position [[position]]; + float2 texCoord; +}; + +vertex VertexOut monochromeVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + out.position = in.position; + out.texCoord = in.texCoord; + return out; +} + +// Define a sampler with linear filtering +// The sampler is defined as a global variable +sampler textureSampler(mag_filter::linear, min_filter::linear); + +fragment float4 monochromeFragmentShader( + VertexOut in [[stage_in]], + texture2d texture [[texture(0)]], + constant float4 &color [[ buffer(0) ]], + constant float &intensity [[ buffer(1) ]]) { + // Sample the pixel color from the input texture using the defined sampler + float4 pixelColor = texture.sample(textureSampler, in.texCoord); + + // Convert to grayscale using luminance + float grayValue = dot(pixelColor.rgb, float3(0.299, 0.587, 0.114)); + + // Create a monochrome color by blending grayscale with the specified color + float4 monochromeColor = mix(float4(grayValue, grayValue, grayValue, pixelColor.a), color, intensity); + + // Return the final color + return monochromeColor; +} diff --git a/Shade/MetalFiles/ColorAdjustment/Grain.metal b/Shade/MetalFiles/ColorAdjustment/Grain.metal new file mode 100644 index 0000000..b1858f6 --- /dev/null +++ b/Shade/MetalFiles/ColorAdjustment/Grain.metal @@ -0,0 +1,35 @@ +// +// Grain.metal +// Shade +// +// Created by Ahmed Ragab on 06/10/2024. +// + +#include +using namespace metal; + + +struct VertexIn { + float4 postion [[attribute(0)]]; + float2 texCoord [[attribute(1)]]; +}; + +struct VertexOut { + float4 postion [[position]]; + float2 texCoord; +}; + + +vertex VertexOut vertexOut(VertexIn in [[stage_in]]) { + VertexOut out; + out.postion = in.postion; + out.texCoord = in.texCoord; + return out; +} + +fragment float4 GrainFragmentShader(VertexOut in [[stage_in]], + texture2d grainTexture [[texture(0)]], + constant float &grainIntensity [[buffer(1)]]) { + float4 grainColor = grainTexture.sample(sampler(mag_filter::linear, min_filter::linear), in.texCoord); + return float4(grainColor.rgb * grainIntensity, 1.0); +} diff --git a/Shade/MetalFiles/ColorAdjustment/ReplaceColor.metal b/Shade/MetalFiles/ColorAdjustment/ReplaceColor.metal new file mode 100644 index 0000000..0c4b98a --- /dev/null +++ b/Shade/MetalFiles/ColorAdjustment/ReplaceColor.metal @@ -0,0 +1,41 @@ +// +// ReplaceColor.metal +// Shade +// +// Created by Ahmed Ragab on 06/10/2024. +// + +#include +using namespace metal; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoord [[attribute(1)]]; +}; + +struct VertexOut { + float4 position [[position]]; + float2 texCoord; +}; + +vertex VertexOut colorReplaceVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + out.position = in.position; + out.texCoord = in.texCoord; + return out; +} + +fragment float4 colorReplaceFragmentShader(VertexOut in [[stage_in]], + texture2d inTexture [[texture(0)]], + sampler textureSampler [[sampler(0)]], + constant float4 &targetColor [[buffer(1)]], + constant float4 &replacementColor [[buffer(2)]]) { + + float4 color = inTexture.sample(textureSampler, in.texCoord); + + // Check if the color matches the target color + if (length(color.rgb - targetColor.rgb) < 0.1) { // Allow a tolerance for matching + return replacementColor; + } + return color; +} diff --git a/Shade/MetalFiles/ColorAdjustment/VignetteShader.metal b/Shade/MetalFiles/ColorAdjustment/VignetteShader.metal new file mode 100644 index 0000000..49af32a --- /dev/null +++ b/Shade/MetalFiles/ColorAdjustment/VignetteShader.metal @@ -0,0 +1,51 @@ +// +// VignetteShader.metal +// Shade +// +// Created by Ahmed Ragab on 06/10/2024. +// + +#include +using namespace metal; + +// Vertex structure +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoord [[attribute(1)]]; +}; + +// Output structure for the vertex shader +struct VertexOut { + float4 position [[position]]; + float2 texCoord; +}; + +// Vertex shader: Pass through vertex positions and texture coordinates +vertex VertexOut vertex_passthrough(VertexIn in [[stage_in]]) { + VertexOut out; + out.position = in.position; + out.texCoord = in.texCoord; + return out; +} + +// Fragment shader: Apply the vignette effect +// Fragment shader: Apply the vignette effect +fragment float4 vignetteShader(VertexOut in [[stage_in]], + texture2d inTexture [[texture(0)]], + constant float &radius [[buffer(0)]], + constant float &softness [[buffer(1)]]) { + + // Get the color of the current pixel + float4 color = inTexture.sample(sampler(filter::linear), in.texCoord); + + // Calculate the distance from the center of the image + float dist = distance(in.texCoord, float2(0.5, 0.5)); + + // Calculate the vignette factor + float vignette = smoothstep(radius + softness, radius, dist); + + // Apply the vignette effect + color.rgb *= vignette; + + return color; // Return the modified color +} diff --git a/Shade/MetalFiles/Filters/Blur/BokehBlur.metal b/Shade/MetalFiles/Filters/Blur/BokehBlur.metal new file mode 100644 index 0000000..a3f2ea7 --- /dev/null +++ b/Shade/MetalFiles/Filters/Blur/BokehBlur.metal @@ -0,0 +1,67 @@ +// +// BokehBlur.metal +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +#include +using namespace metal; + + +struct VertexOut { + float4 postion [[position]]; + float2 texCoords; +}; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoords [[attribute(1)]]; +}; + + +// Vertex shader: transforms vertices and passes texture coordinates to the fragment shader +vertex VertexOut bokehBlurVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + + // Pass through the position (already in clip space for a full-screen quad) + out.postion = in.position; + + // Pass the texture coordinates to the fragment shader + out.texCoords = in.texCoords; + + return out; +} + +fragment float4 bokehBlurFragment(VertexOut in [[ stage_in ]], + texture2d inTexture [[ texture(0) ]], + constant float &radius [[ buffer(0) ]], + constant float &ringSize [[ buffer(1) ]], + constant float &ringAmount [[ buffer(2) ]]) { + constexpr int sampleCount = 20; // Number of samples for bokeh effect + float2 texCoord = in.texCoords; + const float PI = 3.14159265358979323846; + + // Initialize the color accumulation + float4 color = float4(0.0); + float totalWeight = 0.0; + + // Sample in a circular pattern around the original pixel + for (int i = 0; i < sampleCount; i++) { + // Random angle for sample point + float angle = (float(i) / float(sampleCount)) * 2.0 * PI; + float r = radius * (1.0 + (sin(angle * ringAmount) * ringSize)); + float2 offset = float2(cos(angle), sin(angle)) * r; + float2 sampleCoord = texCoord + offset; + + // Ensure the sample is within bounds + sampleCoord = clamp(sampleCoord, float2(0.0), float2(1.0)); + + // Accumulate color + color += inTexture.sample(sampler(mag_filter::linear, min_filter::linear), sampleCoord); + totalWeight += 1.0; + } + + // Average the accumulated color + return color / totalWeight; +} diff --git a/Shade/MetalFiles/Filters/Blur/BoxBlur.metal b/Shade/MetalFiles/Filters/Blur/BoxBlur.metal new file mode 100644 index 0000000..310ab9b --- /dev/null +++ b/Shade/MetalFiles/Filters/Blur/BoxBlur.metal @@ -0,0 +1,62 @@ +// +// BoxBlur.metal +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +#include +using namespace metal; + + +struct VertexOut { + float4 postion [[position]]; + float2 texCoords; +}; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoords [[attribute(1)]]; +}; + +// Vertex shader: transforms vertices and passes texture coordinates to the fragment shader +vertex VertexOut boxBlurVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + + // Pass through the position (already in clip space for a full-screen quad) + out.postion = in.position; + + // Pass the texture coordinates to the fragment shader + out.texCoords = in.texCoords; + + return out; +} + + + +fragment float4 boxBlurFragment(VertexOut in [[stage_in]], + texture2d inTexture [[texture(0)]], + sampler s [[sampler(0)]], + constant float &blurRadius [[buffer(0)]]) { + + float4 color = float4(0.0); + float2 texCoords = in.texCoords; + + int radius = int(blurRadius); + float sampleCount = float((2 * radius + 1) * (2 * radius + 1)); + + // Iterate through neighboring pixels in the box defined by blur radius + for (int x = -radius; x <= radius; x++) { + for (int y = -radius; y <= radius; y++) { + float2 offset = float2(x, y) / float2(inTexture.get_width(), inTexture.get_height()); + color += inTexture.sample(s, texCoords + offset); + } + } + + // Average the color values + color /= sampleCount; + + return color; +} + + diff --git a/Shade/MetalFiles/Filters/Blur/GaussianBlur.metal b/Shade/MetalFiles/Filters/Blur/GaussianBlur.metal new file mode 100644 index 0000000..5ff7bc0 --- /dev/null +++ b/Shade/MetalFiles/Filters/Blur/GaussianBlur.metal @@ -0,0 +1,58 @@ +// +// GaussianBlur.metal +// Shade +// +// Created by Ahmed Ragab on 11/10/2024. +// + +#include +using namespace metal; +struct VertexOut { + float4 postion [[position]]; + float2 texCoords; +}; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoords [[attribute(1)]]; +}; + +// Vertex shader: transforms vertices and passes texture coordinates to the fragment shader +vertex VertexOut gaussianBlurVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + + // Pass through the position (already in clip space for a full-screen quad) + out.postion = in.position; + + // Pass the texture coordinates to the fragment shader + out.texCoords = in.texCoords; + + return out; +} + + + +fragment float4 gaussianBlurFragment(VertexOut in [[stage_in]], + texture2d inTexture [[texture(0)]], + sampler s [[sampler(0)]], + constant float &blurRadius [[buffer(0)]]) { + + float4 color = float4(0.0); + + float2 texCoords = in.texCoords; + float blurWeights[5] = {0.227027, 0.194594, 0.121621, 0.054054, 0.016216}; + + // Initial color from the center + color += inTexture.sample(s, texCoords) * blurWeights[0]; + + // Apply Gaussian blur horizontally and vertically + for (int i = 1; i < 5; i++) { + color += inTexture.sample(s, texCoords + float2(blurRadius * i, 0)) * blurWeights[i]; + color += inTexture.sample(s, texCoords - float2(blurRadius * i, 0)) * blurWeights[i]; + color += inTexture.sample(s, texCoords + float2(0, blurRadius * i)) * blurWeights[i]; + color += inTexture.sample(s, texCoords - float2(0, blurRadius * i)) * blurWeights[i]; + } + + return color; +} + diff --git a/Shade/MetalFiles/Filters/Blur/MotionBlur.metal b/Shade/MetalFiles/Filters/Blur/MotionBlur.metal new file mode 100644 index 0000000..b80ccc8 --- /dev/null +++ b/Shade/MetalFiles/Filters/Blur/MotionBlur.metal @@ -0,0 +1,64 @@ +// +// MotionBlur.metal +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +#include +using namespace metal; +struct VertexOut { + float4 postion [[position]]; + float2 texCoords; +}; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoords [[attribute(1)]]; +}; + + +// Vertex shader: transforms vertices and passes texture coordinates to the fragment shader +vertex VertexOut motionBlurVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + + // Pass through the position (already in clip space for a full-screen quad) + out.postion = in.position; + + // Pass the texture coordinates to the fragment shader + out.texCoords = in.texCoords; + + return out; +} + +fragment float4 motionBlurFragment(VertexOut in [[stage_in]], + texture2d inTexture [[texture(0)]], + sampler s [[sampler(0)]], + constant float &blurRadius [[buffer(0)]], + constant float &blurAngle [[buffer(1)]]) { + float4 color = float4(0.0); + float2 texCoords = in.texCoords; + + // Convert the blur angle to radians + float radians = blurAngle * (M_PI_F / 180.0); + + // Calculate the direction of the blur (x and y offset) + float2 direction = float2(cos(radians), sin(radians)); + + // Determine how many samples to take based on the blur radius + int radius = int(blurRadius); + float sampleCount = float(2 * radius + 1); + + // Accumulate samples along the motion blur direction + for (int i = -radius; i <= radius; i++) { + float2 offset = float2(i) * direction / float2(inTexture.get_width(), inTexture.get_height()); + color += inTexture.sample(s, texCoords + offset); + } + + // Average the color values to create the blur effect + color /= sampleCount; + + return color; +} + + diff --git a/Shade/MetalFiles/Filters/Colors/ColorControls.metal b/Shade/MetalFiles/Filters/Colors/ColorControls.metal new file mode 100644 index 0000000..6232bfb --- /dev/null +++ b/Shade/MetalFiles/Filters/Colors/ColorControls.metal @@ -0,0 +1,64 @@ +// +// ColorControls.metal +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +#include +using namespace metal; + +struct VertexOut { + float4 postion [[position]]; + float2 texCoords; +}; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoords [[attribute(1)]]; +}; + + +// Vertex shader: transforms vertices and passes texture coordinates to the fragment shader +vertex VertexOut colorControlsVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + + // Pass through the position (already in clip space for a full-screen quad) + out.postion = in.position; + + // Pass the texture coordinates to the fragment shader + out.texCoords = in.texCoords; + + return out; +} + + +fragment float4 colorControlsFragment( + VertexOut in [[stage_in]], + texture2d inTexture [[ texture(0) ]], + sampler inSampler [[ sampler(0) ]], + constant float3 &colorAdjustments [[ buffer(0) ]]) // {saturation, contrast, brightness} +{ + // Sample the original color from the texture + float4 color = inTexture.sample(inSampler, in.texCoords); + + // Extract color components + float3 rgb = color.rgb; + + // Adjust Brightness (0 to 3% range) + rgb += (colorAdjustments.z - 1.0) * 2.0; // Scale brightness (1.0 = 0% adjustment) + + // Convert to grayscale to compute saturation adjustment + float gray = dot(rgb, float3(0.299, 0.587, 0.114)); + + // Adjust Saturation (0 to 3) + rgb = mix(float3(gray), rgb, colorAdjustments.x); // Saturation adjustment (0-3) + + // Adjust Contrast (0 to 3) + rgb = ((rgb - 0.5) * (colorAdjustments.y * 2.0)) + 0.5; // Scale contrast (1.0 = 0% adjustment) + + // Clamp the color values to [0, 1] range + rgb = clamp(rgb, 0.0, 1.0); + + return float4(rgb, color.a); // Return the adjusted color +} diff --git a/Shade/MetalFiles/Filters/Colors/ExposureAdjust.metal b/Shade/MetalFiles/Filters/Colors/ExposureAdjust.metal new file mode 100644 index 0000000..0a4ccb2 --- /dev/null +++ b/Shade/MetalFiles/Filters/Colors/ExposureAdjust.metal @@ -0,0 +1,54 @@ +// +// ExposureAdjust.metal +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +#include +using namespace metal; + +struct VertexOut { + float4 postion [[position]]; + float2 texCoords; +}; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoords [[attribute(1)]]; +}; + + +// Vertex shader: transforms vertices and passes texture coordinates to the fragment shader +vertex VertexOut exposureAdjustVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + + // Pass through the position (already in clip space for a full-screen quad) + out.postion = in.position; + + // Pass the texture coordinates to the fragment shader + out.texCoords = in.texCoords; + + return out; +} + +fragment float4 exposureAdjustFragment( + VertexOut in [[stage_in]], + texture2d inTexture [[ texture(0) ]], + sampler inSampler [[ sampler(0) ]], + constant float &exposureEV [[ buffer(0) ]]) +{ + // Sample the original color from the texture + float4 color = inTexture.sample(inSampler, in.texCoords); + + // Convert the EV percentage to a scaling factor + float scalingFactor = pow(2.0, exposureEV / 100.0); // EV 100% corresponds to doubling the light + + // Adjust the color by the scaling factor + color.rgb *= scalingFactor; + + // Clamp the color values to [0, 1] range + color.rgb = clamp(color.rgb, 0.0, 1.0); + + return color; // Return the adjusted color +} diff --git a/Shade/MetalFiles/Filters/Colors/HueAdjust.metal b/Shade/MetalFiles/Filters/Colors/HueAdjust.metal new file mode 100644 index 0000000..cca8615 --- /dev/null +++ b/Shade/MetalFiles/Filters/Colors/HueAdjust.metal @@ -0,0 +1,112 @@ +// +// HueAdjust.metal +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +#include +using namespace metal; +struct VertexOut { + float4 postion [[position]]; + float2 texCoords; +}; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoords [[attribute(1)]]; +}; + + +// Vertex shader: transforms vertices and passes texture coordinates to the fragment shader +vertex VertexOut hueAdjustVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + + // Pass through the position (already in clip space for a full-screen quad) + out.postion = in.position; + + // Pass the texture coordinates to the fragment shader + out.texCoords = in.texCoords; + + return out; +} + +fragment float4 hueAdjustFragment( + VertexOut in [[stage_in]], + texture2d inTexture [[ texture(0) ]], + sampler inSampler [[ sampler(0) ]], + constant float &angle [[ buffer(0) ]] // Angle in degrees + ) +{ + // Sample the original color from the texture + float4 color = inTexture.sample(inSampler, in.texCoords); + + + // Convert RGB to HSL + float3 rgb = color.rgb; + float maxVal = max(max(rgb.r, rgb.g), rgb.b); + float minVal = min(min(rgb.r, rgb.g), rgb.b); + float delta = maxVal - minVal; + + float hue, saturation, lightness; + + // Calculate lightness + lightness = (maxVal + minVal) / 2.0; + + // Calculate saturation + if (delta == 0.0) { + hue = 0.0; // achromatic + saturation = 0.0; + } else { + saturation = lightness < 0.5 ? delta / (maxVal + minVal) : delta / (2.0 - maxVal - minVal); + + // Calculate hue + if (maxVal == rgb.r) { + hue = ((rgb.g - rgb.b) / delta); + } else if (maxVal == rgb.g) { + hue = 2.0 + (rgb.b - rgb.r) / delta; + } else { + hue = 4.0 + (rgb.r - rgb.g) / delta; + } + hue = hue * 60.0; // Convert to degrees + if (hue < 0.0) { + hue += 360.0; // Adjust hue to be in [0, 360] + } + } + + // Adjust the hue + hue += angle; // Add the angle adjustment + if (hue >= 360.0) { + hue -= 360.0; // Wrap around + } else if (hue < 0.0) { + hue += 360.0; // Wrap around + } + + // Convert HSL back to RGB + float c = (1.0 - abs(2.0 * lightness - 1.0)) * saturation; + float x = c * (1.0 - abs(fmod(hue / 60.0, 2.0) - 1.0)); + float m = lightness - c / 2.0; + + float3 rgbAdjusted; + + if (hue < 60.0) { + rgbAdjusted = float3(c, x, 0.0); + } else if (hue < 120.0) { + rgbAdjusted = float3(x, c, 0.0); + } else if (hue < 180.0) { + rgbAdjusted = float3(0.0, c, x); + } else if (hue < 240.0) { + rgbAdjusted = float3(0.0, x, c); + } else if (hue < 300.0) { + rgbAdjusted = float3(x, 0.0, c); + } else { + rgbAdjusted = float3(c, 0.0, x); + } + + rgbAdjusted += m; // Apply lightness shift + + // Clamp the color values to [0, 1] range + rgbAdjusted = clamp(rgbAdjusted, 0.0, 1.0); + + return float4(rgbAdjusted, color.a); // Return the adjusted color +} diff --git a/Shade/MetalFiles/Filters/Colors/InvertColor.metal b/Shade/MetalFiles/Filters/Colors/InvertColor.metal new file mode 100644 index 0000000..1ece929 --- /dev/null +++ b/Shade/MetalFiles/Filters/Colors/InvertColor.metal @@ -0,0 +1,46 @@ +// +// InvertColor.metal +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +#include +using namespace metal; +struct VertexOut { + float4 postion [[position]]; + float2 texCoords; +}; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoords [[attribute(1)]]; +}; + + +// Vertex shader: transforms vertices and passes texture coordinates to the fragment shader +vertex VertexOut invertedColorVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + + // Pass through the position (already in clip space for a full-screen quad) + out.postion = in.position; + + // Pass the texture coordinates to the fragment shader + out.texCoords = in.texCoords; + + return out; +} + +fragment float4 invertColorFragment( + VertexOut in [[stage_in]], + texture2d inTexture [[ texture(0) ]], + sampler inSampler [[ sampler(0) ]]) +{ + // Sample the original color from the texture + float4 color = inTexture.sample(inSampler, in.texCoords); + + // Invert the color + float4 invertedColor = float4(1.0 - color.rgb, color.a); + + return invertedColor; // Return the inverted color +} diff --git a/Shade/MetalFiles/Filters/Colors/ThresholdColor.metal b/Shade/MetalFiles/Filters/Colors/ThresholdColor.metal new file mode 100644 index 0000000..ad87251 --- /dev/null +++ b/Shade/MetalFiles/Filters/Colors/ThresholdColor.metal @@ -0,0 +1,53 @@ +// +// ThresholdColor.metal +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +#include +using namespace metal; + +struct VertexOut { + float4 postion [[position]]; + float2 texCoords; +}; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoords [[attribute(1)]]; +}; + + +// Vertex shader: transforms vertices and passes texture coordinates to the fragment shader +vertex VertexOut thresholdColorVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + + // Pass through the position (already in clip space for a full-screen quad) + out.postion = in.position; + + // Pass the texture coordinates to the fragment shader + out.texCoords = in.texCoords; + + return out; +} + +fragment float4 thresholdColorFragment( + VertexOut in [[stage_in]], + texture2d inTexture [[ texture(0) ]], + sampler inSampler [[ sampler(0) ]], + constant float &threshold [[ buffer(0) ]]) +{ + // Sample the original color from the texture + float4 color = inTexture.sample(inSampler, in.texCoords); + + // Calculate brightness using the luminance formula + float brightness = dot(color.rgb, float3(0.299, 0.587, 0.114)); + + // Apply the threshold + if (brightness > threshold) { + return float4(1.0, 1.0, 1.0, color.a); // White + } else { + return float4(0.0, 0.0, 0.0, color.a); // Black + } +} diff --git a/Shade/MetalFiles/Filters/Distortion/Bump.metal b/Shade/MetalFiles/Filters/Distortion/Bump.metal new file mode 100644 index 0000000..319571d --- /dev/null +++ b/Shade/MetalFiles/Filters/Distortion/Bump.metal @@ -0,0 +1,58 @@ +// +// Bump.metal +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +#include +using namespace metal; + +struct VertexOut { + float4 postion [[position]]; + float2 texCoords; +}; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoords [[attribute(1)]]; +}; + + +// Vertex shader: transforms vertices and passes texture coordinates to the fragment shader +vertex VertexOut bumpEffectVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + + // Pass through the position (already in clip space for a full-screen quad) + out.postion = in.position; + + // Pass the texture coordinates to the fragment shader + out.texCoords = in.texCoords; + + return out; +} + +fragment float4 bumpEffectFragment(VertexOut in [[stage_in]], + texture2d texture [[texture(0)]], + sampler textureSampler [[sampler(0)]], + constant float2 &bumpCenter [[buffer(0)]], + constant float &radius [[buffer(1)]], + constant float &scale [[buffer(2)]]) { + + // Convert fragment position to UV coordinates (0 to 1 range) + float2 uv = in.texCoords; + + // Calculate the distance from the bump center + float2 delta = uv - bumpCenter; + float distance = length(delta); + + // Apply bump effect if within the radius + if (distance < radius) { + float distortion = (1.0 - distance / radius) * scale; + uv += normalize(delta) * distortion; + } + + // Sample the texture using modified UV coordinates + return texture.sample(textureSampler, uv); +} + diff --git a/Shade/MetalFiles/Filters/Distortion/CircleSplash.metal b/Shade/MetalFiles/Filters/Distortion/CircleSplash.metal new file mode 100644 index 0000000..4537f2c --- /dev/null +++ b/Shade/MetalFiles/Filters/Distortion/CircleSplash.metal @@ -0,0 +1,61 @@ +// +// CircleSplash.metal +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +#include +using namespace metal; +struct VertexOut { + float4 postion [[position]]; + float2 texCoords; +}; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoords [[attribute(1)]]; +}; + + +// Vertex shader: transforms vertices and passes texture coordinates to the fragment shader +vertex VertexOut splashVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + + // Pass through the position (already in clip space for a full-screen quad) + out.postion = in.position; + + // Pass the texture coordinates to the fragment shader + out.texCoords = in.texCoords; + + return out; +} + +fragment float4 splashEffectFragment(VertexOut in [[stage_in]], + texture2d texture [[texture(0)]], + sampler textureSampler [[sampler(0)]], + constant float2 &splashCenter [[buffer(0)]], + constant float &radius [[buffer(1)]], + constant float &intensity [[buffer(2)]] + ) { + + // Convert fragment position to UV coordinates (0 to 1 range) + float2 uv = in.texCoords; + + // Calculate the distance from the splash center + float2 delta = uv - splashCenter; + float distance = length(delta); + + // If the pixel is outside the splash area, apply the effect + if (distance > radius) { + // Calculate splash distortion based on distance + float splashDistortion = (distance - radius) * intensity; + // Create a radial effect by displacing UV coordinates + uv += normalize(delta) * splashDistortion; + } + + // Sample the texture using modified UV coordinates + return texture.sample(textureSampler, uv); +} + + diff --git a/Shade/MetalFiles/Filters/Distortion/pinch.metal b/Shade/MetalFiles/Filters/Distortion/pinch.metal new file mode 100644 index 0000000..b49d8f0 --- /dev/null +++ b/Shade/MetalFiles/Filters/Distortion/pinch.metal @@ -0,0 +1,59 @@ +// +// pinch.metal +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +#include +using namespace metal; + +struct VertexOut { + float4 postion [[position]]; + float2 texCoords; +}; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoords [[attribute(1)]]; +}; + + +// Vertex shader: transforms vertices and passes texture coordinates to the fragment shader +vertex VertexOut pinchEffectVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + + // Pass through the position (already in clip space for a full-screen quad) + out.postion = in.position; + + // Pass the texture coordinates to the fragment shader + out.texCoords = in.texCoords; + + return out; +} + +fragment float4 pinchEffectFragment(VertexOut in [[stage_in]], + texture2d texture [[texture(0)]], + sampler textureSampler [[sampler(0)]], + constant float2 &pinchCenter [[buffer(0)]], + constant float &radius [[buffer(1)]], + constant float &scaleFactor [[buffer(2)]]) { + + // Convert fragment position to UV coordinates (0 to 1 range) + float2 uv = in.texCoords; + + // Calculate the distance from the pinch center + float2 delta = uv - pinchCenter; + float distance = length(delta); + + // If the pixel is within the pinch radius, apply the scale effect + if (distance > radius) { + // Calculate the scaling effect + float scale = mix(1.0, scaleFactor, 1.0 - (distance / radius)); + uv = pinchCenter + delta * scale; // Scale UV coordinates + } + + // Sample the texture using modified UV coordinates + return texture.sample(textureSampler, uv); +} + diff --git a/Shade/MetalFiles/Filters/Distortion/warpingLoupe.metal b/Shade/MetalFiles/Filters/Distortion/warpingLoupe.metal new file mode 100644 index 0000000..932c2af --- /dev/null +++ b/Shade/MetalFiles/Filters/Distortion/warpingLoupe.metal @@ -0,0 +1,75 @@ +// +// warpingLoupe.metal +// Shade +// +// Created by Ahmed Ragab on 12/10/2024. +// + +#include +using namespace metal; +struct VertexOut { + float4 postion [[position]]; + float2 texCoords; +}; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoords [[attribute(1)]]; +}; + + +// Vertex shader: transforms vertices and passes texture coordinates to the fragment shader +vertex VertexOut warpingLoupeVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + + // Pass through the position (already in clip space for a full-screen quad) + out.postion = in.position; + + // Pass the texture coordinates to the fragment shader + out.texCoords = in.texCoords; + + return out; +} + + + +fragment float4 warpingLoupe(VertexOut in [[stage_in]], + texture2d inputTexture [[texture(0)]], + constant float2 &size [[buffer(0)]], + constant float2 &touch [[buffer(1)]], + constant float &maxDistance [[buffer(2)]], + constant float &zoomFactor [[buffer(3)]]) { + constexpr sampler textureSampler (mag_filter::linear, min_filter::linear); + + // UV space coordinates (0 to 1 range) + float2 uv = in.texCoords; + + // Convert the touch point to UV space (normalized coordinates) + float2 center = touch / size; + + // Calculate the distance of this pixel from the touch point + float2 delta = uv - center; + + // Adjust for aspect ratio to keep the zoom effect circular + float aspectRatio = size.x / size.y; + delta.y *= aspectRatio; + + // Compute squared distance from the touch point + float distance = dot(delta, delta); + + // Initialize total zoom to 1.0 (no zoom) + float totalZoom = 1.0; + + // Apply zoom if within the defined radius + if (distance < maxDistance * maxDistance) { + // Apply zoom factor inside the loupe area + totalZoom = mix(1.0, 1.0 / zoomFactor, smoothstep(0.0, maxDistance, sqrt(distance))); + } + + // Calculate the new texture coordinate, applying the zoom + float2 newUV = delta * totalZoom + center; + + // Sample the texture using the modified coordinates + return inputTexture.sample(textureSampler, distance < maxDistance * maxDistance ? newUV : uv); +} + diff --git a/Shade/MetalFiles/Filters/Tile/Brickwork.metal b/Shade/MetalFiles/Filters/Tile/Brickwork.metal new file mode 100644 index 0000000..1617fb1 --- /dev/null +++ b/Shade/MetalFiles/Filters/Tile/Brickwork.metal @@ -0,0 +1,65 @@ +// +// Brickwork.metal +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +#include +using namespace metal; + + +struct VertexOut { + float4 postion [[position]]; + float2 texCoords; +}; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoords [[attribute(1)]]; +}; + + +// Vertex shader: transforms vertices and passes texture coordinates to the fragment shader +vertex VertexOut brickworkVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + + // Pass through the position (already in clip space for a full-screen quad) + out.postion = in.position; + + // Pass the texture coordinates to the fragment shader + out.texCoords = in.texCoords; + + return out; +} + +fragment float4 brickworkFragment( + VertexOut in [[stage_in]], + texture2d texture [[texture(0)]], + constant float2 ¢er [[buffer(0)]], // Center point for the effect + constant float &radius [[buffer(1)]], // Radius of the effect + constant float &angle [[buffer(2)]], // Angle for brickwork + constant float &width [[buffer(3)]], // Width of the bricks + sampler textureSampler [[sampler(0)]]) +{ + // Calculate the distance from the current fragment to the center point + float2 coords = in.texCoords * float2(texture.get_width(), texture.get_height()); // Convert UV to pixel coordinates + float dist = distance(coords, center); + + // If the distance is greater than the radius, keep the original color + if (dist > radius) { + return texture.sample(textureSampler, in.texCoords); + } + + // Brickwork pattern logic + float pattern = (sin(coords.y * (2.0 * 3.14159 / width) + angle) + 1.0) * 0.5; // Adjust brick height + pattern = floor(pattern * 2.0); // Make it a binary pattern + + // Sample the original texture color + float4 color = texture.sample(textureSampler, in.texCoords); + if (pattern < 1.0) { + color.rgb *= 0.5; // Darken the brick + } + + return color; +} diff --git a/Shade/MetalFiles/Filters/Tile/Kaleidoscope.metal b/Shade/MetalFiles/Filters/Tile/Kaleidoscope.metal new file mode 100644 index 0000000..f88bf67 --- /dev/null +++ b/Shade/MetalFiles/Filters/Tile/Kaleidoscope.metal @@ -0,0 +1,150 @@ +// +// Kaleidoscope.metal +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +#include +using namespace metal; + +struct VertexOut { + float4 postion [[position]]; + float2 texCoords; +}; + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texCoords [[attribute(1)]]; +}; + +constant float PI = 3.14159265358979323846; + +// Vertex shader: transforms vertices and passes texture coordinates to the fragment shader +vertex VertexOut kaleidoscopeVertexShader(VertexIn in [[stage_in]]) { + VertexOut out; + + // Pass through the position (already in clip space for a full-screen quad) + out.postion = in.position; + + // Pass the texture coordinates to the fragment shader + out.texCoords = in.texCoords; + + return out; +} + +float2 rotate(float2 coord, float angle) { + float cosAngle = cos(angle); + float sinAngle = sin(angle); + return float2( + coord.x * cosAngle - coord.y * sinAngle, + coord.x * sinAngle + coord.y * cosAngle + ); +} + + +fragment float4 kaleidoscopeFragment(VertexIn in [[stage_in]], + texture2d texture [[texture(0)]], + sampler textureSampler [[sampler(0)]], + constant float2 ¢er [[buffer(0)]], + constant float &angle [[buffer(1)]], + constant int &count [[buffer(2)]], + constant float &radius [[buffer(3)]]) { + float2 uv = in.texCoords; + + + // Calculate delta and distance from center + float2 delta = uv - center; + float distanceFromCenter = length(delta); + + // If the distance is greater than the radius, return the original texture color + if (distanceFromCenter > radius) { + return texture.sample(textureSampler, uv); + } + + // Calculate angle step per slice + float sliceAngle = 2.0 * PI / float(count); + + // Calculate the angle from the center to the current point + float theta = atan2(delta.y, delta.x); + + // Normalize the angle into one slice + theta = fmod(theta, sliceAngle); + + // If the angle is negative, normalize it to the positive range + if (theta < 0.0) { + theta += sliceAngle; + } + + // Apply user-specified rotation angle + theta += angle; + + // Reflect the angle to create the mirroring effect + if (theta > sliceAngle / 2.0) { + theta = sliceAngle - theta; + } + + // Rotate the delta based on the calculated angle + float2 rotatedDelta = rotate(delta, theta); + + // Add the rotated delta back to the center + float2 rotatedUV = rotatedDelta + center; + + // Sample the texture at the modified coordinates + return texture.sample(textureSampler, rotatedUV); +} + + +fragment float4 triangleKaleidoscopeFragment(VertexOut in [[stage_in]], + texture2d texture [[texture(0)]], + sampler textureSampler [[sampler(0)]], + constant float2 ¢er [[buffer(0)]], + constant float &angle [[buffer(1)]], + constant float &size [[buffer(2)]], + constant float &decay [[buffer(3)]], + constant float &radius [[buffer(4)]]) { + float2 uv = in.texCoords; + + // Calculate delta and distance from center + float2 delta = uv - center; + float distanceFromCenter = length(delta); + + // If the distance is greater than the radius, keep the original image + if (distanceFromCenter < radius) { + return texture.sample(textureSampler, uv); + } + + // Calculate the effect's decay based on the distance and decay percentage + float decayFactor = max(1.0 - (distanceFromCenter / decay), 0.0); + + // Calculate the angle from the center to the current point + float theta = atan2(delta.y, delta.x); + + // Normalize the angle into the triangle's section + float sliceAngle = 2.0 * PI / 3.0; // Triangles (360 degrees / 3 slices) + theta = fmod(theta, sliceAngle); + + if (theta < 0.0) { + theta += sliceAngle; + } + + // Rotate the angle by the user-defined rotation + theta += angle; + + // Reflect the angle to create the mirroring effect + if (theta > sliceAngle / 2.0) { + theta = sliceAngle - theta; + } + + // Apply the size parameter to determine how large the triangles are + float2 rotatedDelta = rotate(delta, theta) / size; + + // Add the rotated delta back to the center + float2 rotatedUV = rotatedDelta + center; + + // Sample the texture at the modified coordinates + float4 color = texture.sample(textureSampler, rotatedUV); + + // Apply decay to the color (fade out the effect based on distance) + return float4(color.rgb * decayFactor, color.a); +} diff --git a/Shade/MetalViews/BlendModesView.swift b/Shade/MetalViews/BlendModesView.swift new file mode 100644 index 0000000..06c7fae --- /dev/null +++ b/Shade/MetalViews/BlendModesView.swift @@ -0,0 +1,94 @@ +// +// BlendModesView.swift +// Shade +// +// Created by Ahmed Ragab on 04/01/2025. +// + + +import SwiftUI +import Metal + +struct BlendModesView: View { + @State private var selectedMode: BlendMode = .normal + + let blendModes: [(name: String, mode: BlendMode)] = [ + ("Normal", .normal), + ("Color Dodge", .colorDodge), + ("Hard Mix", .hardLight), + ("Darken", .darken), + ("Linear Dodge", .plusLighter), + ("Difference", .difference), + ("Multiply", .multiply), + ("Lighter Color", .lighten), + ("Exclusion", .exclusion), + ("Color Burn", .colorBurn), + ("Overlay", .overlay), + ("Subtract", .difference), + ("Linear Burn", .colorBurn), + ("Soft Light", .softLight), + ("Divide", .screen), + ("Darker", .darken), + ("Hard Light", .hardLight), + ("Hue", .hue), + ("Color", .color), + ("Vivid Light", .hardLight), + ("Saturation", .saturation), + ("Lighten", .lighten), + ("Linear Light", .plusLighter), + ("Screen", .screen), + ("Pin Light", .hardLight) + ] + + var body: some View { + VStack(spacing: 16) { + // Preview Area + ZStack { + Image(.person) + + Image(.city) + + .blendMode(selectedMode) + } + .frame(height: 200) + .clipShape(RoundedRectangle(cornerRadius: 15)) + .shadow(radius: 10) + .padding(.horizontal) + + // Current Selection Display + Text("Selected: \(blendModes.first(where: { $0.mode == selectedMode })?.name ?? "Normal")") + .font(.headline) + .padding(.vertical, 8) + + // Blend Mode Picker using List + List { + ForEach(blendModes, id: \.name) { blendMode in + HStack { + Text(blendMode.name) + .font(.system(.body)) + Spacer() + if selectedMode == blendMode.mode { + Image(systemName: "checkmark") + .foregroundColor(.blue) + } + } + .contentShape(Rectangle()) + .onTapGesture { + withAnimation { + selectedMode = blendMode.mode + } + } + .padding(.vertical, 4) + } + } + .listStyle(PlainListStyle()) + } + } +} + +// Preview Provider +struct BlendModesView_Previews: PreviewProvider { + static var previews: some View { + BlendModesView() + } +} diff --git a/Shade/MetalViews/ColorAdjustment/ChannelMixer/ChannelMixerMetalRenderer.swift b/Shade/MetalViews/ColorAdjustment/ChannelMixer/ChannelMixerMetalRenderer.swift new file mode 100644 index 0000000..706257e --- /dev/null +++ b/Shade/MetalViews/ColorAdjustment/ChannelMixer/ChannelMixerMetalRenderer.swift @@ -0,0 +1,155 @@ +// +// ChannelMixerMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 06/10/2024. +// +// + +import MetalKit +import SwiftUI +struct Vertex { + var position: SIMD2 + var textureCoordinate: SIMD2 +} + +class ChannelMixerMetalRenderer: NSObject, MTKViewDelegate { + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + + var redMix = SIMD3(1.0, 0.0, 0.0) + var greenMix = SIMD3(0.0, 1.0, 0.0) + var blueMix = SIMD3(0.0, 0.0, 1.0) + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + // Load image and create Metal texture from UIImage + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + // Setup vertex buffer with full-screen quad vertices + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + // Setup pipeline, including vertex and fragment shaders + private func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "channelMixerVertexShader") + let fragmentFunction = library.makeFunction(name: "channelMixerFragment") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float2 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float2 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + // Create pipeline state + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + // MARK: - MTKViewDelegate + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { + // Handle window resize if needed + } + + func draw(in view: MTKView) { + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + + // Set the pipeline state + renderEncoder.setRenderPipelineState(pipelineState) + + // Set vertex buffer + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Set texture and channel mixer values in the fragment shader + renderEncoder.setFragmentTexture(texture, index: 0) + renderEncoder.setFragmentBytes(&redMix, length: MemoryLayout>.stride, index: 0) + renderEncoder.setFragmentBytes(&greenMix, length: MemoryLayout>.stride, index: 1) + renderEncoder.setFragmentBytes(&blueMix, length: MemoryLayout>.stride, index: 2) + + // Draw the full-screen quad (4 vertices) + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) + + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } +} + +extension NSImage { + var CGImage: CGImage? { + get { + let imageData = self.tiffRepresentation + let source = CGImageSourceCreateWithData(imageData! as CFData, nil).unsafelyUnwrapped + let maskRef = CGImageSourceCreateImageAtIndex(source, Int(0), nil) + return maskRef.unsafelyUnwrapped + } + } +} +extension NSImage { + /// Generates a CIImage for this NSImage. + /// - Returns: A CIImage optional. + func ciImage() -> CIImage? { + guard let data = self.tiffRepresentation, + let bitmap = NSBitmapImageRep(data: data) else { + return nil + } + let ci = CIImage(bitmapImageRep: bitmap) + return ci + } + + /// Generates an NSImage from a CIImage. + /// - Parameter ciImage: The CIImage + /// - Returns: An NSImage optional. + static func fromCIImage(_ ciImage: CIImage) -> NSImage { + let rep = NSCIImageRep(ciImage: ciImage) + let nsImage = NSImage(size: rep.size) + nsImage.addRepresentation(rep) + return nsImage + } +} diff --git a/Shade/MetalViews/ColorAdjustment/ChannelMixer/ChannelMixerView.swift b/Shade/MetalViews/ColorAdjustment/ChannelMixer/ChannelMixerView.swift new file mode 100644 index 0000000..79718a1 --- /dev/null +++ b/Shade/MetalViews/ColorAdjustment/ChannelMixer/ChannelMixerView.swift @@ -0,0 +1,85 @@ +// +// ChannelMixerView.swift +// Shade +// +// Created by Ahmed Ragab on 06/10/2024. +// + +import Foundation +import SwiftUI +import MetalKit + +struct ChannelMixerMetalView: NSViewRepresentable { + var image: NSImage + var redMix: SIMD3 + var greenMix: SIMD3 + var blueMix: SIMD3 + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator + mtkView.enableSetNeedsDisplay = true + mtkView.isPaused = true + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ mtkView: MTKView, context: Context) { + context.coordinator.updateMixers(red: redMix, green: greenMix, blue: blueMix) + mtkView.setNeedsDisplay(mtkView.frame) + } + + class Coordinator: NSObject, MTKViewDelegate { + var parent: ChannelMixerMetalView + var renderer: ChannelMixerMetalRenderer + + init(_ parent: ChannelMixerMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = ChannelMixerMetalRenderer(device: device, image: parent.image) + } + + func updateMixers(red: SIMD3, green: SIMD3, blue: SIMD3) { + renderer.redMix = red + renderer.greenMix = green + renderer.blueMix = blue + } + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} + } +} + + + +struct ChannelMixerView: View { + @State private var redMix = SIMD3(1.0, 0.0, 0.0) + @State private var greenMix = SIMD3(0.0, 1.0, 0.0) + @State private var blueMix = SIMD3(0.0, 0.0, 1.0) + + var body: some View { + VStack { + ChannelMixerMetalView(image: NSImage(resource: .person2), + redMix: redMix, greenMix: greenMix, blueMix: blueMix) + .frame(height: 300) + + VStack { + Slider(value: $redMix.x, in: 0...1, label: { Text("Red Mix") }) + Slider(value: $greenMix.y, in: 0...1, label: { Text("Green Mix") }) + Slider(value: $blueMix.z, in: 0...1, label: { Text("Blue Mix") }) + }.padding() + } + } +} + +#Preview { + ChannelMixerView() +} diff --git a/Shade/MetalViews/ColorAdjustment/ColorMonochrome/ColorMonochromeMetalRenderer.swift b/Shade/MetalViews/ColorAdjustment/ColorMonochrome/ColorMonochromeMetalRenderer.swift new file mode 100644 index 0000000..6007aa8 --- /dev/null +++ b/Shade/MetalViews/ColorAdjustment/ColorMonochrome/ColorMonochromeMetalRenderer.swift @@ -0,0 +1,108 @@ +// +// ColorMonochromeMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 06/10/2024. +// + +import Foundation +import SwiftUI +import MetalKit + +class ColorMonochromeMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + + + var intensity: Float = 0.5 + var color = SIMD4(0.0,0.0,0.0,0.0) + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "monochromeVertexShader") + let fragmentFunction = library.makeFunction(name: "monochromeFragmentShader") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + renderEncoder.setFragmentBytes(&color, length: MemoryLayout>.size, index: 0) + renderEncoder.setFragmentBytes(&intensity, length: MemoryLayout.size, index: 1) + + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} diff --git a/Shade/MetalViews/ColorAdjustment/ColorMonochrome/ColorMonochromeView.swift b/Shade/MetalViews/ColorAdjustment/ColorMonochrome/ColorMonochromeView.swift new file mode 100644 index 0000000..c95a630 --- /dev/null +++ b/Shade/MetalViews/ColorAdjustment/ColorMonochrome/ColorMonochromeView.swift @@ -0,0 +1,86 @@ +// +// ColorMonochromeView.swift +// Shade +// +// Created by Ahmed Ragab on 06/10/2024. +// + +import Foundation +import SwiftUI +import MetalKit + +struct ColorMonochromeMetalView: NSViewRepresentable { + var image: NSImage + var color: Color + var intensity: Float + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.updateColor(color: color, intensity: intensity) + uiView.setNeedsDisplay(uiView.frame) + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + func updateColor(color: Color, intensity: Float) { + renderer.intensity = intensity + renderer.color = color.toSimdFloat4() + } + + var parent: ColorMonochromeMetalView + var renderer: ColorMonochromeMetalRenderer + + init(_ parent: ColorMonochromeMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = ColorMonochromeMetalRenderer(device: device, image: parent.image) + } + } +} + + + +struct ColorMonochromeView: View { + @State private var color: Color = .red + @State private var intensity: Float = 0.5 + var image = NSImage(resource: .person) + var body: some View { + VStack { + ColorMonochromeMetalView(image:image, color: color, intensity: intensity) + .frame(width: 300, height: 300) + + + // Add controls to change color and intensity + HStack { + + ColorPicker("Select Color", selection: $color) + + Slider(value: $intensity, in: 0...1, label: { + Text("Intensity") + }) + } + .padding() + } + } +} + + + +#Preview { + ColorMonochromeView() +} diff --git a/Shade/MetalViews/ColorAdjustment/Grain/GrainMetalRenderer.swift b/Shade/MetalViews/ColorAdjustment/Grain/GrainMetalRenderer.swift new file mode 100644 index 0000000..11b0a34 --- /dev/null +++ b/Shade/MetalViews/ColorAdjustment/Grain/GrainMetalRenderer.swift @@ -0,0 +1,107 @@ +// +// GrainMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 06/10/2024. +// + +import Foundation +import SwiftUI +import MetalKit + +class GrainMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + + + var grainIntensity: Float = 0.5 + + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "vertexOut") + let fragmentFunction = library.makeFunction(name: "GrainFragmentShader") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + renderEncoder.setFragmentBytes(&grainIntensity, length: MemoryLayout.size, index: 1) + + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} diff --git a/Shade/MetalViews/ColorAdjustment/Grain/GrainView.swift b/Shade/MetalViews/ColorAdjustment/Grain/GrainView.swift new file mode 100644 index 0000000..4324e79 --- /dev/null +++ b/Shade/MetalViews/ColorAdjustment/Grain/GrainView.swift @@ -0,0 +1,76 @@ +// +// GrainView.swift +// Shade +// +// Created by Ahmed Ragab on 06/10/2024. +// + +import Foundation +import MetalKit +import SwiftUI + +struct GrainMetalView: NSViewRepresentable { + var image: NSImage + var grainIntensity: Float + + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.updateGrain(grainIntensity: grainIntensity) + uiView.setNeedsDisplay(uiView.frame) + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + func updateGrain(grainIntensity: Float) { + renderer.grainIntensity = grainIntensity + } + + var parent: GrainMetalView + var renderer: GrainMetalRenderer + + init(_ parent: GrainMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = GrainMetalRenderer(device: device, image: parent.image) + } + } +} + +struct GrainEffectView: View { + @State private var grainIntensity: Float = 0.5 // Adjust grain intensity + var image = NSImage(resource: .person) + var body: some View { + VStack { + GrainMetalView(image: image, + grainIntensity: grainIntensity) + .frame(width: 300, height: 300) + .cornerRadius(12) + + Slider(value: $grainIntensity, in: 0.0...1.0, step: 0.01) { + Text("Grain Intensity") + } + .padding() + } + } +} + + +#Preview { + GrainEffectView() +} diff --git a/Shade/MetalViews/ColorAdjustment/ReplaceColor/ReplaceColorView.swift b/Shade/MetalViews/ColorAdjustment/ReplaceColor/ReplaceColorView.swift new file mode 100644 index 0000000..41787b1 --- /dev/null +++ b/Shade/MetalViews/ColorAdjustment/ReplaceColor/ReplaceColorView.swift @@ -0,0 +1,83 @@ +// +// ReplaceColorView.swift +// Shade +// +// Created by Ahmed Ragab on 06/10/2024. +// + +import Foundation +import SwiftUI +import MetalKit + +struct ReplaceColorMetalView: NSViewRepresentable { + + var image: NSImage + var replacmentColor: Color + var targetColor: Color + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.enableSetNeedsDisplay = true + mtkView.isPaused = true + mtkView.autoResizeDrawable = true + return mtkView + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func updateNSView(_ mtkView: MTKView, context: Context) { + context.coordinator.updateColors(targetColor: targetColor, replacmentColor: replacmentColor) + mtkView.setNeedsDisplay(mtkView.frame) + } + + class Coordinator: NSObject { + var parent: ReplaceColorMetalView + var renderer: ReplaceColortMetalRenderer + + init(_ parent: ReplaceColorMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = ReplaceColortMetalRenderer(device: device, image: parent.image) + } + + func updateColors(targetColor: Color, replacmentColor: Color) { + renderer.replacmentColor = replacmentColor.toSimdFloat4() + renderer.targetColor = targetColor.toSimdFloat4() + } + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + } + +} + + + +struct ReplaceColorView: View { + @State var targetColor = Color.red // Target color to replace + + @State var replacementColor = Color.blue + + private let image = NSImage(resource: .person1) + + var body: some View { + VStack { + ReplaceColorMetalView(image:image, + replacmentColor: replacementColor, + targetColor: targetColor) + + ColorPicker("target Color", selection: $targetColor) + ColorPicker("replacment Color", selection: $replacementColor) + } + } +} +#Preview { + ReplaceColorView() +} + + diff --git a/Shade/MetalViews/ColorAdjustment/ReplaceColor/ReplaceColortMetalRenderer.swift b/Shade/MetalViews/ColorAdjustment/ReplaceColor/ReplaceColortMetalRenderer.swift new file mode 100644 index 0000000..4ade487 --- /dev/null +++ b/Shade/MetalViews/ColorAdjustment/ReplaceColor/ReplaceColortMetalRenderer.swift @@ -0,0 +1,119 @@ +// +// ReplaceColortMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 06/10/2024. +// + +import Foundation +import MetalKit +import SwiftUI + +class ReplaceColortMetalRenderer: NSObject, MTKViewDelegate { + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + var samplerState: MTLSamplerState! + + var targetColor = SIMD4(0.0,0.0,0.0,0.0) + var replacmentColor = SIMD4(1.0, 1.0, 1.0, 1.0) + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + setupsamplerDescriptor() + loadImageAsTexture(image: image) + } + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + private func setupsamplerDescriptor() { + let samplerDescriptor = MTLSamplerDescriptor() + samplerDescriptor.minFilter = .linear + samplerDescriptor.magFilter = .linear + samplerDescriptor.mipFilter = .nearest + samplerState = device.makeSamplerState(descriptor: samplerDescriptor) + } + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "colorReplaceVertexShader") + let fragmentFunction = library.makeFunction(name: "colorReplaceFragmentShader") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float2 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float2 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + // Create pipeline state + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { + // Handle window resize if needed + } + + func draw(in view: MTKView) { + guard let drawable = view.currentDrawable, + let rendererPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: rendererPassDescriptor)! + + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + renderEncoder.setFragmentSamplerState(samplerState, index: 0) + +// renderEncoder.setFragmentTexture(drawable.texture, index: 1) + + renderEncoder.setFragmentBytes(&targetColor, length: MemoryLayout>.stride, index: 1) + renderEncoder.setFragmentBytes(&replacmentColor, length: MemoryLayout>.stride, index: 2) + + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) + + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } +} diff --git a/Shade/MetalViews/ColorAdjustment/Vignette/VignetteMetalRenderer.swift b/Shade/MetalViews/ColorAdjustment/Vignette/VignetteMetalRenderer.swift new file mode 100644 index 0000000..be6471c --- /dev/null +++ b/Shade/MetalViews/ColorAdjustment/Vignette/VignetteMetalRenderer.swift @@ -0,0 +1,109 @@ +// +// VignetteMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 06/10/2024. +// + +import Foundation +import SwiftUI +import MetalKit + +class VignetteMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + + + var radius: Float = 0.0 + var softness: Float = 0.0 + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "vertex_passthrough") + let fragmentFunction = library.makeFunction(name: "vignetteShader") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + renderEncoder.setFragmentBytes(&radius, length: MemoryLayout.size, index: 0) + renderEncoder.setFragmentBytes(&softness, length: MemoryLayout.size, index: 1) + + + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} diff --git a/Shade/MetalViews/ColorAdjustment/Vignette/VignetteView.swift b/Shade/MetalViews/ColorAdjustment/Vignette/VignetteView.swift new file mode 100644 index 0000000..3231472 --- /dev/null +++ b/Shade/MetalViews/ColorAdjustment/Vignette/VignetteView.swift @@ -0,0 +1,95 @@ +// +// VignetteView.swift +// Shade +// +// Created by Ahmed Ragab on 06/10/2024. +// + +import Foundation +import SwiftUI +import MetalKit + +struct VignetteViewMetalView : NSViewRepresentable { + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + var image: NSImage + var radius: Float + var softness: Float + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.enableSetNeedsDisplay = true + mtkView.isPaused = true +// mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.updateVignette(radius: radius, softness: softness) + uiView.setNeedsDisplay(uiView.frame) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + func updateVignette(radius: Float, softness: Float) { + renderer.radius = radius + renderer.softness = softness + } + + var parent: VignetteViewMetalView + var renderer: VignetteMetalRenderer + + init(_ parent: VignetteViewMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = VignetteMetalRenderer(device: device, image: parent.image) + } + } +} + + +struct VignetteView: View { + // State properties for radius and softness + @State private var radius: Float = 0.5 // Default value + @State private var softness: Float = 0.5 // Default value + + var body: some View { + VStack { + // Vignette view with the image and the effect applied + VignetteViewMetalView(image: NSImage(resource: .person), + radius: radius, + softness: softness) + .frame(width: 300, height: 300) + .border(Color.red, width: 1) + + // Sliders for adjusting radius and softness + VStack { + Text("Radius: \(String(format: "%.2f", radius))") + .padding(.top) + + Slider(value: $radius, in: 0...1, step: 0.01) // Adjust range as needed + .padding() + + Text("Softness: \(String(format: "%.2f", softness))") + .padding(.top) + + Slider(value: $softness, in: 0...1, step: 0.01) // Adjust range as needed + .padding() + } + .padding(.top) + } + .padding() + } +} + +#Preview { + VignetteView() +} diff --git a/Shade/MetalViews/Filters/Blur/BokehBlurMetalRenderer.swift b/Shade/MetalViews/Filters/Blur/BokehBlurMetalRenderer.swift new file mode 100644 index 0000000..d60582e --- /dev/null +++ b/Shade/MetalViews/Filters/Blur/BokehBlurMetalRenderer.swift @@ -0,0 +1,218 @@ +// +// BokehBlurMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +import MetalKit +import SwiftUI + +class BokehBlurMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + var samplerState: MTLSamplerState! + + + + + var radius: Float = 0.0 + var ringAmount: Float = 0.0 + var ringSize: Float = 0.0 + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), // Bottom-left + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left + + // Second triangle + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left (repeated) + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right (repeated) + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "bokehBlurVertexShader") + let fragmentFunction = library.makeFunction(name: "bokehBlurFragment") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + + let samplerDescriptor = MTLSamplerDescriptor() + samplerDescriptor.minFilter = .linear + samplerDescriptor.magFilter = .linear + samplerDescriptor.mipFilter = .linear + samplerDescriptor.sAddressMode = .clampToEdge + samplerDescriptor.tAddressMode = .clampToEdge + samplerState = device.makeSamplerState(descriptor: samplerDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + + + + renderEncoder.setFragmentBytes(&radius, length: MemoryLayout.size, index: 0) + renderEncoder.setFragmentBytes(&ringAmount, length: MemoryLayout.size, index: 1) + renderEncoder.setFragmentBytes(&ringSize, length: MemoryLayout.size, index: 2) + + + + renderEncoder.setFragmentSamplerState(samplerState, index: 0) + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 6) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} + + +struct BokehBlurMetalView: NSViewRepresentable { + var image: NSImage + var radius: Float + var ringAmount: Float + var ringSize: Float + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.preferredFramesPerSecond = 60 + + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.renderer.radius = radius + context.coordinator.renderer.ringAmount = ringAmount + context.coordinator.renderer.ringSize = ringSize + uiView.setNeedsDisplay(uiView.frame) + } + func updateTouchLocation(_ location: CGPoint) -> SIMD2 { + return SIMD2(Float(location.x), Float(location.y)) + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + var parent: BokehBlurMetalView + var renderer: BokehBlurMetalRenderer + + init(_ parent: BokehBlurMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = BokehBlurMetalRenderer(device: device, image: parent.image) + } + } +} + + +struct BokehBlurView: View { + @State private var radius: Float = 0.1 + @State private var ringSize: Float = 0.1 + @State private var ringAmount: Float = 1.0 + var image = NSImage(resource: .person) + + var body: some View { + VStack { + BokehBlurMetalView(image: image,radius: radius, ringAmount: ringAmount, ringSize: ringSize) + .frame(width: 300, height: 300) + .border(Color.black) + + VStack { + Text("Radius: \(radius, specifier: "%.2f")") + Slider(value: $radius, in: 0...1) { + Text("Radius") + } + + Text("Ring Size: \(ringSize, specifier: "%.2f")") + Slider(value: $ringSize, in: 0...1) { + Text("Ring Size") + } + + Text("Ring Amount: \(ringAmount, specifier: "%.2f")") + Slider(value: $ringAmount, in: 1...10) { + Text("Ring Amount") + } + } + .padding() + } + } + } +#Preview { + BokehBlurView() +} + + + diff --git a/Shade/MetalViews/Filters/Blur/BoxBlurMetalRenderer.swift b/Shade/MetalViews/Filters/Blur/BoxBlurMetalRenderer.swift new file mode 100644 index 0000000..8b93513 --- /dev/null +++ b/Shade/MetalViews/Filters/Blur/BoxBlurMetalRenderer.swift @@ -0,0 +1,187 @@ +// +// BoxBlurMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 12/10/2024. +// + +import Foundation +import MetalKit +import SwiftUI + +class BoxBlurMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + + + + + var radius: Float = 0.5 + + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), // Bottom-left + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left + + // Second triangle + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left (repeated) + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right (repeated) + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "boxBlurVertexShader") + let fragmentFunction = library.makeFunction(name: "boxBlurFragment") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + + renderEncoder.setFragmentBytes(&radius, length: MemoryLayout>.size, index: 0) + + + renderEncoder.setFragmentTexture(texture, index: 0) + + + + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 6) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} + + +struct BoxBlurMetalView: NSViewRepresentable { + var image: NSImage + var radius: Float = 0.5 + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.preferredFramesPerSecond = 60 + + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.renderer.radius = radius + uiView.setNeedsDisplay(uiView.frame) + } + func updateTouchLocation(_ location: CGPoint) -> SIMD2 { + return SIMD2(Float(location.x), Float(location.y)) + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + var parent: BoxBlurMetalView + var renderer: BoxBlurMetalRenderer + + init(_ parent: BoxBlurMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = BoxBlurMetalRenderer(device: device, image: parent.image) + } + } +} + +struct BoxBlurView: View { + var image = NSImage(resource: .person) + @State private var blurCenter: CGPoint = .zero + @State private var blurRadius: Float = 3.0 + + + + + + var body: some View { + VStack { + BoxBlurMetalView(image: image, radius: blurRadius) + .frame(width: 300, height: 300) + + Slider(value: $blurRadius, in: 0...20) + .padding() + } + } +} + +#Preview { + BoxBlurView() +} diff --git a/Shade/MetalViews/Filters/Blur/GaussianBlurEffectMetalRenderer.swift b/Shade/MetalViews/Filters/Blur/GaussianBlurEffectMetalRenderer.swift new file mode 100644 index 0000000..49da742 --- /dev/null +++ b/Shade/MetalViews/Filters/Blur/GaussianBlurEffectMetalRenderer.swift @@ -0,0 +1,190 @@ +// +// SaturateEffectMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 12/10/2024. +// + +import Foundation +import MetalKit +import SwiftUI + +class GaussianBlurEffectMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + + + + + var radius: Float = 0.5 + + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), // Bottom-left + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left + + // Second triangle + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left (repeated) + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right (repeated) + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "gaussianBlurVertexShader") + let fragmentFunction = library.makeFunction(name: "gaussianBlurFragment") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + + + + renderEncoder.setFragmentBytes(&radius, length: MemoryLayout>.size, index: 0) + + + renderEncoder.setFragmentTexture(texture, index: 0) + + + + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 6) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} + + +struct GaussianBlurEffectMetalView: NSViewRepresentable { + var image: NSImage + var radius: Float = 0.5 + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.preferredFramesPerSecond = 60 + + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.renderer.radius = radius + uiView.setNeedsDisplay(uiView.frame) + } + func updateTouchLocation(_ location: CGPoint) -> SIMD2 { + return SIMD2(Float(location.x), Float(location.y)) + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + var parent: GaussianBlurEffectMetalView + var renderer: GaussianBlurEffectMetalRenderer + + init(_ parent: GaussianBlurEffectMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = GaussianBlurEffectMetalRenderer(device: device, image: parent.image) + } + } +} + +struct GaussianBlurEffectView: View { + var image = NSImage(resource: .person) + @State private var blurCenter: CGPoint = .zero + @State private var blurRadius: Float = 0.5 + + + + + + var body: some View { + VStack { + GaussianBlurEffectMetalView(image: image, radius: blurRadius) + .frame(width: 300, height: 300) + + Slider(value: $blurRadius, in: 0...1) + .padding() + } + } +} + +#Preview { + GaussianBlurEffectView() +} + diff --git a/Shade/MetalViews/Filters/Blur/MotionBlurMetalRenderer.swift b/Shade/MetalViews/Filters/Blur/MotionBlurMetalRenderer.swift new file mode 100644 index 0000000..c4b9156 --- /dev/null +++ b/Shade/MetalViews/Filters/Blur/MotionBlurMetalRenderer.swift @@ -0,0 +1,196 @@ +// +// MotionBlurMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 12/10/2024. +// + +import Foundation +import MetalKit +import SwiftUI + +class MotionBlurMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + + + + + var radius: Float = 0.5 + var blurAngle: Float = 45.0 + + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), // Bottom-left + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left + + // Second triangle + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left (repeated) + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right (repeated) + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "motionBlurVertexShader") + let fragmentFunction = library.makeFunction(name: "motionBlurFragment") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + + + + + renderEncoder.setFragmentBytes(&radius, length: MemoryLayout>.size, index: 0) + renderEncoder.setFragmentBytes(&blurAngle, length: MemoryLayout>.size, index: 1) + + + renderEncoder.setFragmentTexture(texture, index: 0) + + + + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 6) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} + + +struct MotionBlurMetalView: NSViewRepresentable { + var image: NSImage + var radius: Float = 0.5 + var blurAngle: Float = 45.0 + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.preferredFramesPerSecond = 60 + + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.renderer.radius = radius + context.coordinator.renderer.blurAngle = blurAngle + uiView.setNeedsDisplay(uiView.frame) + } + func updateTouchLocation(_ location: CGPoint) -> SIMD2 { + return SIMD2(Float(location.x), Float(location.y)) + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + var parent: MotionBlurMetalView + var renderer: MotionBlurMetalRenderer + + init(_ parent: MotionBlurMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = MotionBlurMetalRenderer(device: device, image: parent.image) + } + } +} + +struct MotionBlurView: View { + var image: NSImage = NSImage(resource: .person2) + @State private var blurRadius: Float = 5.0 + @State private var blurAngle: Float = 45.0 + + var body: some View { + VStack { + MotionBlurMetalView(image: image, radius: blurRadius, blurAngle: blurAngle) + .frame(width: 300, height: 300) + + VStack { + Text("Blur Radius") + Slider(value: $blurRadius, in: 1...20) + .padding() + Text("Blur Angle") + Slider(value: $blurAngle, in: 0...360) + .padding() + } + } + } +} + +#Preview { + MotionBlurView() +} diff --git a/Shade/MetalViews/Filters/Colors/ColorControlMetalRenderer.swift b/Shade/MetalViews/Filters/Colors/ColorControlMetalRenderer.swift new file mode 100644 index 0000000..0f874c6 --- /dev/null +++ b/Shade/MetalViews/Filters/Colors/ColorControlMetalRenderer.swift @@ -0,0 +1,208 @@ +// +// ColorControlMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +import Foundation +import MetalKit +import SwiftUI + +class ColorControlMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + var samplerState: MTLSamplerState! + + var saturation: Float = 0.0 + var contrast: Float = 0.0 + var brightness: Float = 0.0 + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), // Bottom-left + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left + + // Second triangle + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left (repeated) + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right (repeated) + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "colorControlsVertexShader") + let fragmentFunction = library.makeFunction(name: "colorControlsFragment") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + + let samplerDescriptor = MTLSamplerDescriptor() + samplerDescriptor.minFilter = .linear + samplerDescriptor.magFilter = .linear + samplerDescriptor.mipFilter = .linear + samplerDescriptor.sAddressMode = .clampToEdge + samplerDescriptor.tAddressMode = .clampToEdge + samplerState = device.makeSamplerState(descriptor: samplerDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + + + var adjustments = SIMD3(saturation, contrast, brightness) + + renderEncoder.setFragmentBytes(&adjustments, length: MemoryLayout>.size, index: 0) + + + + + renderEncoder.setFragmentSamplerState(samplerState, index: 0) + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 6) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} + + +struct ColorControlMetalView: NSViewRepresentable { + var image: NSImage + var saturation: Float + var contrast: Float + var brightness: Float + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.preferredFramesPerSecond = 60 + + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.renderer.brightness = brightness + context.coordinator.renderer.saturation = saturation + context.coordinator.renderer.contrast = contrast + + + uiView.setNeedsDisplay(uiView.frame) + } + + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + var parent: ColorControlMetalView + var renderer: ColorControlMetalRenderer + + init(_ parent: ColorControlMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = ColorControlMetalRenderer(device: device, image: parent.image) + } + } +} + + + +struct ColorControlView: View { + var image = NSImage(resource: .person) + @State private var saturation: Float = 1.0 // Saturation (1.0 = 100%) + @State private var contrast: Float = 1.0 // Contrast (1.0 = 100%) + @State private var brightness: Float = 1.0 // Brightness (1.0 = 100%) + + var body: some View { + VStack { + ColorControlMetalView(image:image,saturation: saturation, contrast: contrast, brightness: brightness) + .frame(width: 300, height: 300) + + Slider(value: $saturation, in: -1...2, step: 0.1, label: { + Text("Saturation: \(Int(saturation * 100))%") + }) + Slider(value: $contrast, in: 0...2, step: 0.1, label: { + Text("Contrast: \(Int(contrast * 100))%") + }) + Slider(value: $brightness, in: 0...2, step: 0.1, label: { + Text("Brightness: \(Int(brightness * 100))%") + }) + } + } + } +#Preview { + ColorControlView() +} + + + diff --git a/Shade/MetalViews/Filters/Colors/ExposureAdjustMetalRenderer.swift b/Shade/MetalViews/Filters/Colors/ExposureAdjustMetalRenderer.swift new file mode 100644 index 0000000..56c5603 --- /dev/null +++ b/Shade/MetalViews/Filters/Colors/ExposureAdjustMetalRenderer.swift @@ -0,0 +1,191 @@ +// +// ExposureAdjustMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +import Foundation +import MetalKit +import SwiftUI + +class ExposureAdjustMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + var samplerState: MTLSamplerState! + + var exposureEV: Float = 0.0 + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), // Bottom-left + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left + + // Second triangle + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left (repeated) + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right (repeated) + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "exposureAdjustVertexShader") + let fragmentFunction = library.makeFunction(name: "exposureAdjustFragment") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + + let samplerDescriptor = MTLSamplerDescriptor() + samplerDescriptor.minFilter = .linear + samplerDescriptor.magFilter = .linear + samplerDescriptor.mipFilter = .linear + samplerDescriptor.sAddressMode = .clampToEdge + samplerDescriptor.tAddressMode = .clampToEdge + samplerState = device.makeSamplerState(descriptor: samplerDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + + + + renderEncoder.setFragmentBytes(&exposureEV, length: MemoryLayout.size, index: 0) + + + + + renderEncoder.setFragmentSamplerState(samplerState, index: 0) + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 6) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} + + +struct ExposureAdjustMetalView: NSViewRepresentable { + var image: NSImage + var exposureEV: Float = 0.0 + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.preferredFramesPerSecond = 60 + + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.renderer.exposureEV = exposureEV + + uiView.setNeedsDisplay(uiView.frame) + } + + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + var parent: ExposureAdjustMetalView + var renderer: ExposureAdjustMetalRenderer + + init(_ parent: ExposureAdjustMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = ExposureAdjustMetalRenderer(device: device, image: parent.image) + } + } +} + + + +struct ExposureAdjustView: View { + @State private var exposureEV: Float = 0.0 // Exposure Value in percentage + var image = NSImage(resource: .person) + var body: some View { + VStack { + ExposureAdjustMetalView(image:image,exposureEV: exposureEV) + .frame(width: 300, height: 300) + + Slider(value: $exposureEV, in: -100...100, label: { + Text("Exposure EV") + }) + } + } +} +#Preview { + ExposureAdjustView() +} + + + diff --git a/Shade/MetalViews/Filters/Colors/HueAdjustMetalRenderer.swift b/Shade/MetalViews/Filters/Colors/HueAdjustMetalRenderer.swift new file mode 100644 index 0000000..ad15f64 --- /dev/null +++ b/Shade/MetalViews/Filters/Colors/HueAdjustMetalRenderer.swift @@ -0,0 +1,191 @@ +// +// HueAdjustMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// +import Foundation +import MetalKit +import SwiftUI + +class HueAdjustMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + var samplerState: MTLSamplerState! + + var hueAngle: Float = 0.0 + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), // Bottom-left + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left + + // Second triangle + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left (repeated) + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right (repeated) + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "hueAdjustVertexShader") + let fragmentFunction = library.makeFunction(name: "hueAdjustFragment") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + + let samplerDescriptor = MTLSamplerDescriptor() + samplerDescriptor.minFilter = .linear + samplerDescriptor.magFilter = .linear + samplerDescriptor.mipFilter = .linear + samplerDescriptor.sAddressMode = .clampToEdge + samplerDescriptor.tAddressMode = .clampToEdge + samplerState = device.makeSamplerState(descriptor: samplerDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + + + + renderEncoder.setFragmentBytes(&hueAngle, length: MemoryLayout.size, index: 0) + + + + + renderEncoder.setFragmentSamplerState(samplerState, index: 0) + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 6) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} + + +struct HueAdjustMetalView: NSViewRepresentable { + var image: NSImage + var hueAngle: Float + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.preferredFramesPerSecond = 60 + + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.renderer.hueAngle = hueAngle + + uiView.setNeedsDisplay(uiView.frame) + } + + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + var parent: HueAdjustMetalView + var renderer: HueAdjustMetalRenderer + + init(_ parent: HueAdjustMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = HueAdjustMetalRenderer(device: device, image: parent.image) + } + } +} + + + +struct HueAdjustView: View { + @State private var hueAngle: Float = 0.0 // Angle for hue adjustment + var image = NSImage(resource: .person) + var body: some View { + VStack { + HueAdjustMetalView(image:image, hueAngle:hueAngle) + .frame(width: 300, height: 300) + + + Slider(value: $hueAngle, in: -180...180, step: 1.0, label: { + Text("Hue Angle: \(Int(hueAngle))°") + }) + } + } +} +#Preview { + HueAdjustView() +} + + + diff --git a/Shade/MetalViews/Filters/Colors/InvertColorMetalRenderer.swift b/Shade/MetalViews/Filters/Colors/InvertColorMetalRenderer.swift new file mode 100644 index 0000000..29b0196 --- /dev/null +++ b/Shade/MetalViews/Filters/Colors/InvertColorMetalRenderer.swift @@ -0,0 +1,178 @@ +// +// InvertColorMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +import Foundation +import MetalKit +import SwiftUI + +class InvertColorMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + var samplerState: MTLSamplerState! + + var hueAngle: Float = 0.0 + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), // Bottom-left + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left + + // Second triangle + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left (repeated) + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right (repeated) + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "invertedColorVertexShader") + let fragmentFunction = library.makeFunction(name: "invertColorFragment") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + + let samplerDescriptor = MTLSamplerDescriptor() + samplerDescriptor.minFilter = .linear + samplerDescriptor.magFilter = .linear + samplerDescriptor.mipFilter = .linear + samplerDescriptor.sAddressMode = .clampToEdge + samplerDescriptor.tAddressMode = .clampToEdge + samplerState = device.makeSamplerState(descriptor: samplerDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + + renderEncoder.setFragmentSamplerState(samplerState, index: 0) + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 6) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} + + +struct InvertColorMetalView: NSViewRepresentable { + var image: NSImage + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.preferredFramesPerSecond = 60 + + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + + uiView.setNeedsDisplay(uiView.frame) + } + + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + var parent: InvertColorMetalView + var renderer: InvertColorMetalRenderer + + init(_ parent: InvertColorMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = InvertColorMetalRenderer(device: device, image: parent.image) + } + } +} + + + +struct InvertColorView: View { + var image = NSImage(resource: .person) + var body: some View { + VStack { + InvertColorMetalView(image:image) + .frame(width: 300, height: 300) + } + } +} +#Preview { + InvertColorView() + .padding() +} + + + diff --git a/Shade/MetalViews/Filters/Colors/ThresholdColorMetalRenderer.swift b/Shade/MetalViews/Filters/Colors/ThresholdColorMetalRenderer.swift new file mode 100644 index 0000000..ba9395c --- /dev/null +++ b/Shade/MetalViews/Filters/Colors/ThresholdColorMetalRenderer.swift @@ -0,0 +1,191 @@ +// +// ThresholdColorMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +import Foundation +import MetalKit +import SwiftUI + +class ThresholdColorMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + var samplerState: MTLSamplerState! + + var threshold: Float = 0.0 + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), // Bottom-left + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left + + // Second triangle + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left (repeated) + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right (repeated) + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "thresholdColorVertexShader") + let fragmentFunction = library.makeFunction(name: "thresholdColorFragment") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + + let samplerDescriptor = MTLSamplerDescriptor() + samplerDescriptor.minFilter = .linear + samplerDescriptor.magFilter = .linear + samplerDescriptor.mipFilter = .linear + samplerDescriptor.sAddressMode = .clampToEdge + samplerDescriptor.tAddressMode = .clampToEdge + samplerState = device.makeSamplerState(descriptor: samplerDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + + + + renderEncoder.setFragmentBytes(&threshold, length: MemoryLayout.size, index: 0) + + + + + renderEncoder.setFragmentSamplerState(samplerState, index: 0) + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 6) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} + + +struct ThresholdColorMetalView: NSViewRepresentable { + var image: NSImage + var threshold: Float + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.preferredFramesPerSecond = 60 + + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.renderer.threshold = threshold + + uiView.setNeedsDisplay(uiView.frame) + } + + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + var parent: ThresholdColorMetalView + var renderer: ThresholdColorMetalRenderer + + init(_ parent: ThresholdColorMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = ThresholdColorMetalRenderer(device: device, image: parent.image) + } + } +} + + + +struct ThresholdColorView: View { + @State private var threshold: Float = 0.5 // Default threshold value + var image = NSImage(resource: .person) + var body: some View { + VStack { + ThresholdColorMetalView(image:image, threshold:threshold) + .frame(width: 300, height: 300) + + + Slider(value: $threshold, in: 0.0...1.0, step: 0.01) + .padding() + .accentColor(.blue) + } + } +} +#Preview { + ThresholdColorView() +} + + + diff --git a/Shade/MetalViews/Filters/Distortion/BumpTouchEffectMetalRendrer.swift b/Shade/MetalViews/Filters/Distortion/BumpTouchEffectMetalRendrer.swift new file mode 100644 index 0000000..b545b9c --- /dev/null +++ b/Shade/MetalViews/Filters/Distortion/BumpTouchEffectMetalRendrer.swift @@ -0,0 +1,208 @@ +// +// BumpTouchEffect.swift +// Shade +// +// Created by Ahmed Ragab on 12/10/2024. +// + +import Foundation +import MetalKit +import SwiftUI + +class BumpTouchEffectMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + + + + var touchLocation = SIMD2(0, 0) + var radius: Float = 2.0 + var scale: Float = 0.25 + + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), // Bottom-left + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left + + // Second triangle + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left (repeated) + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right (repeated) + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "bumpEffectVertexShader") + let fragmentFunction = library.makeFunction(name: "bumpEffectFragment") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + + var bumpCenter = SIMD2(Float(touchLocation.x / Float(view.bounds.width)), Float(1.0 - touchLocation.y / Float(view.bounds.height))) + var radius = Float(radius / Float(view.bounds.width)) + + + + renderEncoder.setFragmentBytes(&bumpCenter, length: MemoryLayout>.size, index: 0) + renderEncoder.setFragmentBytes(&radius, length: MemoryLayout.size, index: 1) + renderEncoder.setFragmentBytes(&scale, length: MemoryLayout.size, index: 2) + + + + + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 6) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} + + +struct BumpTouchEffectMetalView: NSViewRepresentable { + var image: NSImage + @Binding var touchLocation: CGPoint + var radius: Float = 100.0 + var scale: Float = 0.2 + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.preferredFramesPerSecond = 60 + + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.renderer.radius = radius + context.coordinator.renderer.scale = scale + context.coordinator.renderer.touchLocation = updateTouchLocation($touchLocation.wrappedValue) + uiView.setNeedsDisplay(uiView.frame) + } + func updateTouchLocation(_ location: CGPoint) -> SIMD2 { + return SIMD2(Float(location.x), Float(location.y)) + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + var parent: BumpTouchEffectMetalView + var renderer: BumpTouchEffectMetalRenderer + + init(_ parent: BumpTouchEffectMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = BumpTouchEffectMetalRenderer(device: device, image: parent.image) + } + } +} + + +struct BumpTouchEffectView: View { + @State private var touchLocation: CGPoint = .zero + @State private var radius: Float = 100.0 + @State private var scale: Float = 0.2 + var image = NSImage(resource: .person) + var body: some View { + VStack { + BumpTouchEffectMetalView ( + image: image, + touchLocation: $touchLocation, + radius: radius, + scale: scale) + .gesture(DragGesture(minimumDistance: 0) + .onChanged { value in + touchLocation = value.location + }) + + + Text("Radius: \(Int(radius))") + Slider(value: $radius, in: 50...200) + + Text("Scale: \(scale, specifier: "%.2f")") + Slider(value: $scale, in: 0.1...0.5) + } + .padding() + } +} +#Preview { + BumpTouchEffectView() + .padding() +} + + diff --git a/Shade/MetalViews/Filters/Distortion/PinchEffectMetalRenderer.swift b/Shade/MetalViews/Filters/Distortion/PinchEffectMetalRenderer.swift new file mode 100644 index 0000000..c6d0a7d --- /dev/null +++ b/Shade/MetalViews/Filters/Distortion/PinchEffectMetalRenderer.swift @@ -0,0 +1,209 @@ +// +// PinchEffectMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 12/10/2024. +// + +import MetalKit +import SwiftUI + +class PinchEffectMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + + + + var touchLocation = SIMD2(0, 0) + var radius: Float = 2.0 + var scale: Float = 0.25 + + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), // Bottom-left + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left + + // Second triangle + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left (repeated) + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right (repeated) + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "pinchEffectVertexShader") + let fragmentFunction = library.makeFunction(name: "pinchEffectFragment") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + + + + var bumpCenter = SIMD2(Float(touchLocation.x / Float(view.bounds.width)), Float(1.0 - touchLocation.y / Float(view.bounds.height))) + var radius = Float(radius / Float(view.bounds.width)) + + + + renderEncoder.setFragmentBytes(&bumpCenter, length: MemoryLayout>.size, index: 0) + renderEncoder.setFragmentBytes(&radius, length: MemoryLayout.size, index: 1) + renderEncoder.setFragmentBytes(&scale, length: MemoryLayout.size, index: 2) + + + + + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 6) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} + + +struct PinchEffectMetalView: NSViewRepresentable { + var image: NSImage + @Binding var touchLocation: CGPoint + var radius: Float = 100.0 + var scale: Float = 0.2 + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.preferredFramesPerSecond = 60 + + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.renderer.radius = radius + context.coordinator.renderer.scale = scale + context.coordinator.renderer.touchLocation = updateTouchLocation($touchLocation.wrappedValue) + uiView.setNeedsDisplay(uiView.frame) + } + func updateTouchLocation(_ location: CGPoint) -> SIMD2 { + return SIMD2(Float(location.x), Float(location.y)) + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + var parent: PinchEffectMetalView + var renderer: PinchEffectMetalRenderer + + init(_ parent: PinchEffectMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = PinchEffectMetalRenderer(device: device, image: parent.image) + } + } +} + + +struct PinchEffectView: View { + @State private var touchLocation: CGPoint = .zero + @State private var radius: Float = 100.0 + @State private var scale: Float = 0.2 + var image = NSImage(resource: .person) + var body: some View { + VStack { + BumpTouchEffectMetalView ( + image: image, + touchLocation: $touchLocation, + radius: radius, + scale: scale) + .gesture(DragGesture(minimumDistance: 0) + .onChanged { value in + touchLocation = value.location + }) + + + Text("Radius: \(Int(radius))") + Slider(value: $radius, in: 50...200) + + Text("Scale: \(scale, specifier: "%.2f")") + Slider(value: $scale, in: 0.1...0.5) + } + .padding() + } + } +#Preview { + PinchEffectView() +} + + diff --git a/Shade/MetalViews/Filters/Distortion/SplashEffectMetalRenderer.swift b/Shade/MetalViews/Filters/Distortion/SplashEffectMetalRenderer.swift new file mode 100644 index 0000000..98db394 --- /dev/null +++ b/Shade/MetalViews/Filters/Distortion/SplashEffectMetalRenderer.swift @@ -0,0 +1,212 @@ +// +// SplashEffectMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 12/10/2024. +// + +import MetalKit +import SwiftUI + +class SplashEffectMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + + + + var touchLocation = SIMD2(0, 0) + var radius: Float = 0.0 + var intensity: Float = 0.25 + var isOutside: Bool = true + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), // Bottom-left + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left + + // Second triangle + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left (repeated) + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right (repeated) + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "splashVertexShader") + let fragmentFunction = library.makeFunction(name: "splashEffectFragment") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + + + + var splashCenter = SIMD2(Float(touchLocation.x / Float(view.bounds.width)), Float(1.0 - touchLocation.y / Float(view.bounds.height))) + var radius = Float(radius / Float(view.bounds.width)) + + renderEncoder.setFragmentBytes(&splashCenter, length: MemoryLayout>.stride, index: 0) + renderEncoder.setFragmentBytes(&radius, length: MemoryLayout.stride, index: 1) + renderEncoder.setFragmentBytes(&intensity, length: MemoryLayout.stride, index: 2) + renderEncoder.setFragmentBytes(&isOutside, length: MemoryLayout.stride, index: 3) + renderEncoder.setFragmentTexture(texture, index: 0) + + + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 6) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} + + +struct SplashEffectMetalView: NSViewRepresentable { + var image: NSImage + @Binding var touchLocation: CGPoint + var radius: Float = 0.0 + var intensity: Float = 0.0 + var isOutside: Bool = true + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.preferredFramesPerSecond = 60 + + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.renderer.radius = radius + context.coordinator.renderer.intensity = intensity + context.coordinator.renderer.isOutside = isOutside + context.coordinator.renderer.touchLocation = updateTouchLocation($touchLocation.wrappedValue) + uiView.setNeedsDisplay(uiView.frame) + } + func updateTouchLocation(_ location: CGPoint) -> SIMD2 { + return SIMD2(Float(location.x), Float(location.y)) + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + var parent: SplashEffectMetalView + var renderer: SplashEffectMetalRenderer + + init(_ parent: SplashEffectMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = SplashEffectMetalRenderer(device: device, image: parent.image) + } + } +} + +struct SplashEffectView: View { + @State private var touchLocation: CGPoint = .zero + @State private var radius: Float = 100.0 + @State private var intensity: Float = 0.2 + @State var isOutSide: Bool = true + var image = NSImage(resource: .person) + var body: some View { + VStack { + SplashEffectMetalView( + image: image, + touchLocation: $touchLocation, + radius: radius, + intensity: intensity) + .gesture(DragGesture(minimumDistance: 0) + .onChanged { value in + touchLocation = value.location + }) + + + Text("Radius: \(Int(radius))") + Slider(value: $radius, in: 50...200) + + Text("Intensity: \(intensity, specifier: "%.2f")") + Slider(value: $intensity, in: 0.0...2.0) + Toggle("Is OutSide", isOn: $isOutSide) + + } + .padding() + } +} + +#Preview { + SplashEffectView() +} diff --git a/Shade/MetalViews/Filters/Distortion/WarpingLoupeMetalRenderer.swift b/Shade/MetalViews/Filters/Distortion/WarpingLoupeMetalRenderer.swift new file mode 100644 index 0000000..ddb04c9 --- /dev/null +++ b/Shade/MetalViews/Filters/Distortion/WarpingLoupeMetalRenderer.swift @@ -0,0 +1,205 @@ +// +// WrapingLoupeMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 12/10/2024. +// + +import Foundation +import SwiftUI +import MetalKit + +class WarpingLoupeMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + + + + var touchLocation = SIMD2(0, 0) + var zoomFactor: Float = 2.0 + var maxDistance: Float = 0.25 + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), // Bottom-left + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left + + // Second triangle + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left (repeated) + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right (repeated) + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "warpingLoupeVertexShader") + let fragmentFunction = library.makeFunction(name: "warpingLoupe") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + + var size = SIMD2(Float(view.drawableSize.width), Float(view.drawableSize.height)) + + + renderEncoder.setFragmentBytes(&size, length: MemoryLayout>.stride, index: 0) + renderEncoder.setFragmentBytes(&touchLocation, length: MemoryLayout>.stride, index: 1) + renderEncoder.setFragmentBytes(&maxDistance, length: MemoryLayout.stride, index: 2) + renderEncoder.setFragmentBytes(&zoomFactor, length: MemoryLayout.stride, index: 3) + + + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 6) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} + + +struct WarpingLoupeMetalView: NSViewRepresentable { + var image: NSImage + @Binding var touchLocation: CGPoint + var maxDistance: Float = 0.25 // Loupe radius (in normalized UV space) + var zoomFactor: Float = 2.0 + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.preferredFramesPerSecond = 60 + + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.renderer.maxDistance = maxDistance + context.coordinator.renderer.zoomFactor = zoomFactor + + context.coordinator.renderer.touchLocation = updateTouchLocation($touchLocation.wrappedValue) + uiView.setNeedsDisplay(uiView.frame) + } + func updateTouchLocation(_ location: CGPoint) -> SIMD2 { + return SIMD2(Float(location.x), Float(location.y)) + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + var parent: WarpingLoupeMetalView + var renderer: WarpingLoupeMetalRenderer + + init(_ parent: WarpingLoupeMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = WarpingLoupeMetalRenderer(device: device, image: parent.image) + } + } +} + +struct WarpingLoupeView: View { + @State private var touchLocation: CGPoint = .zero + @State private var maxDistance: Float = 0.25 // Loupe radius (in normalized UV space) + @State private var zoomFactor: Float = 0.5 // Zoom factor + var image = NSImage(resource: .person) + var body: some View { + VStack { + + + WarpingLoupeMetalView(image: image, touchLocation: $touchLocation,maxDistance: maxDistance,zoomFactor: zoomFactor) + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { value in + // Convert touch location to Metal view coordinates + touchLocation = value.location + } + ) + .aspectRatio(contentMode: .fit) + + Slider(value: $zoomFactor, in: 0.5...5.0) { + Text("Zoom Factor") + }.padding() + + Slider(value: $maxDistance, in: 0.1...0.5) { + Text("Loupe Radius") + }.padding() + } + } +} + +#Preview { + WarpingLoupeView() +} diff --git a/Shade/MetalViews/Filters/Tile/BrickworkMetalRenderer.swift b/Shade/MetalViews/Filters/Tile/BrickworkMetalRenderer.swift new file mode 100644 index 0000000..83daa97 --- /dev/null +++ b/Shade/MetalViews/Filters/Tile/BrickworkMetalRenderer.swift @@ -0,0 +1,225 @@ +// +// BrickworkMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +import Foundation +import SwiftUI +import MetalKit + +class BrickworkMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + + + + var touchLocation = SIMD2(0, 0) + var radius: Float = 50.0 // Default radius + var angle: Float = 0.0 // Default angle + var width: Float = 50.0 + + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), // Bottom-left + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left + + // Second triangle + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left (repeated) + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right (repeated) + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "brickworkVertexShader") + let fragmentFunction = library.makeFunction(name: "brickworkFragment") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + + + + var center = SIMD2(Float(touchLocation.x / Float(view.bounds.width)), Float(1.0 - touchLocation.y / Float(view.bounds.height))) + renderEncoder.setFragmentBytes(&touchLocation, length: MemoryLayout>.stride, index: 0) + renderEncoder.setFragmentBytes(&radius, length: MemoryLayout.stride, index: 1) + renderEncoder.setFragmentBytes(&angle, length: MemoryLayout.stride, index: 2) + renderEncoder.setFragmentBytes(&width, length: MemoryLayout.stride, index: 3) + + renderEncoder.setFragmentTexture(texture, index: 0) + + + + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 6) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} + + +struct BrickworkMetalView: NSViewRepresentable { + var image: NSImage + @Binding var touchLocation: CGPoint + var radius: Float = 50.0 // Default radius + var angle: Float = 0.0 // Default angle + var width: Float = 50.0 + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.preferredFramesPerSecond = 60 + + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.renderer.angle = angle + context.coordinator.renderer.radius = radius + context.coordinator.renderer.width = width + context.coordinator.renderer.touchLocation = updateTouchLocation($touchLocation.wrappedValue) + uiView.setNeedsDisplay(uiView.frame) + } + func updateTouchLocation(_ location: CGPoint) -> SIMD2 { + return SIMD2(Float(location.x), Float(location.y)) + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + var parent: BrickworkMetalView + var renderer: BrickworkMetalRenderer + + init(_ parent: BrickworkMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = BrickworkMetalRenderer(device: device, image: parent.image) + } + } +} + +struct BrickworkView: View { + @State private var center: CGPoint = .zero + @State private var radius: Float = 50.0 + @State private var angle: Float = 0.0 + @State private var width: Float = 50.0 + + + var body: some View { + VStack { + BrickworkMetalView(image: NSImage(resource: .person), + touchLocation: $center, + radius:radius, + angle: angle, + width: width ) + .frame(width: 300, height: 300) + .gesture( + DragGesture() + .onChanged { value in + center = value.location + } + ) + + HStack { + Text("Brick Width (px)") + Slider(value: $width, in: 10...2000) + } + .padding() + + HStack { + Text("Rotation Angle") + Slider(value: $angle, in: 0.0...Float.pi) + } + .padding() + + HStack { + Text("Radius") + Slider(value: $radius, in: 10...150) + } + .padding() + } + } +} + + +#Preview { + BrickworkView() +} diff --git a/Shade/MetalViews/Filters/Tile/KaleidoscopeMetalRenderer.swift b/Shade/MetalViews/Filters/Tile/KaleidoscopeMetalRenderer.swift new file mode 100644 index 0000000..8924b45 --- /dev/null +++ b/Shade/MetalViews/Filters/Tile/KaleidoscopeMetalRenderer.swift @@ -0,0 +1,228 @@ +// +// kaleidoscopeMetalRenderer.swift +// Shade +// +// Created by Ahmed Ragab on 13/10/2024. +// + +import Foundation +import SwiftUI +import MetalKit + +class kaleidoscopeMetalRenderer: NSObject,MTKViewDelegate { + + + private var device: MTLDevice! + private var commandQueue: MTLCommandQueue! + private var pipelineState: MTLRenderPipelineState! + private var vertexBuffer: MTLBuffer! + private var texture: MTLTexture? + + + + var touchLocation = SIMD2(0, 0) + var count: Int = 6 + var angle: Float = 0.0 + var radius: Float = 0.0 + + + init(device: MTLDevice, image: NSImage) { + self.device = device + self.commandQueue = device.makeCommandQueue() + super.init() + setupPipeline() + setupVertexBuffer() + loadImageAsTexture(image: image) + } + + + private func setupVertexBuffer() { + let quadVertices: [Vertex] = [ + Vertex(position: [-1.0, -1.0], textureCoordinate: [0.0, 1.0]), // Bottom-left + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left + + // Second triangle + Vertex(position: [-1.0, 1.0], textureCoordinate: [0.0, 0.0]), // Top-left (repeated) + Vertex(position: [ 1.0, -1.0], textureCoordinate: [1.0, 1.0]), // Bottom-right (repeated) + Vertex(position: [ 1.0, 1.0], textureCoordinate: [1.0, 0.0]) + ] + + vertexBuffer = device.makeBuffer(bytes: quadVertices, + length: MemoryLayout.stride * quadVertices.count, + options: []) + } + + func setupPipeline() { + let library = device.makeDefaultLibrary()! + let vertexFunction = library.makeFunction(name: "kaleidoscopeVertexShader") + let fragmentFunction = library.makeFunction(name: "kaleidoscopeFragment") + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = vertexFunction + pipelineDescriptor.fragmentFunction = fragmentFunction + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + // Define the vertex descriptor to describe vertex attributes + let vertexDescriptor = MTLVertexDescriptor() + + vertexDescriptor.attributes[0].format = .float3 // Position + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + + vertexDescriptor.attributes[1].format = .float3 // Texture Coordinates + vertexDescriptor.attributes[1].offset = MemoryLayout>.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + + vertexDescriptor.layouts[0].stride = MemoryLayout.stride + vertexDescriptor.layouts[0].stepFunction = .perVertex + + pipelineDescriptor.vertexDescriptor = vertexDescriptor + + pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + func draw(in view: MTKView) { + + guard let drawable = view.currentDrawable, + let renderPassDescriptor = view.currentRenderPassDescriptor, + let texture = texture else { return } + + let commandBuffer = commandQueue.makeCommandBuffer()! + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderEncoder.setRenderPipelineState(pipelineState) + + renderEncoder.setFragmentTexture(texture, index: 0) + + + + var center = SIMD2(Float(touchLocation.x / Float(view.bounds.width)), Float(1.0 - touchLocation.y / Float(view.bounds.height))) + renderEncoder.setFragmentBytes(¢er, length: MemoryLayout>.stride, index: 0) + renderEncoder.setFragmentBytes(&angle, length: MemoryLayout.stride, index: 1) + renderEncoder.setFragmentBytes(&count, length: MemoryLayout.stride, index: 2) + renderEncoder.setFragmentBytes(&radius, length: MemoryLayout.size, index: 3) + + + + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + // Draw the quad + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 6) + renderEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + + + private func loadImageAsTexture(image: NSImage) { + let textureLoader = MTKTextureLoader(device: device) + if let cgImage = image.CGImage { + texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} +} + + +struct kaleidoscopeMetalView: NSViewRepresentable { + var image: NSImage + @Binding var touchLocation: CGPoint + var angle: Float = 0.0 // Loupe radius (in normalized UV space) + var count: Int = 0 + var radius: Float = 0.5 + + func makeNSView(context: Context) -> MTKView { + let device = MTLCreateSystemDefaultDevice()! + let mtkView = MTKView(frame: .zero, device: device) + mtkView.delegate = context.coordinator.renderer + mtkView.preferredFramesPerSecond = 60 + + mtkView.autoResizeDrawable = true + return mtkView + } + + func updateNSView(_ uiView: MTKView, context: Context) { + context.coordinator.renderer.angle = angle + context.coordinator.renderer.count = count + context.coordinator.renderer.radius = radius + context.coordinator.renderer.touchLocation = updateTouchLocation($touchLocation.wrappedValue) + uiView.setNeedsDisplay(uiView.frame) + } + func updateTouchLocation(_ location: CGPoint) -> SIMD2 { + return SIMD2(Float(location.x), Float(location.y)) + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + + func draw(in view: MTKView) { + renderer.draw(in: view) + } + + var parent: kaleidoscopeMetalView + var renderer: kaleidoscopeMetalRenderer + + init(_ parent: kaleidoscopeMetalView) { + self.parent = parent + let device = MTLCreateSystemDefaultDevice()! + self.renderer = kaleidoscopeMetalRenderer(device: device, image: parent.image) + } + } +} + +struct kaleidoscopeView: View { + @State private var count: Int = 6 + @State private var angle: Float = 0.0 + @State private var center: CGPoint = CGPoint(x: 0.5, y: 0.5) + @State private var radius: Float = 0.5 + + var body: some View { + VStack { + kaleidoscopeMetalView( + image: NSImage(resource: .person), touchLocation: $center, + angle: angle, + count: count, + radius: radius + ) + .frame(width: 300, height: 300) + .gesture( + DragGesture() + .onChanged { value in + // Update center of the kaleidoscope effect based on drag location +// let x = Float(value.location.x / 300) +// let y = Float(value.location.y / 300) + center = value.location +// CGPoint(x: CGFloat(x), y: CGFloat(y)) + } + ) + + HStack { + Text("Slices Count") + Slider(value: Binding( + get: { Double(count) }, + set: { count = Int($0) } + ), in: 3...12, step: 1) + } + .padding() + + HStack { + Text("Angle") + Slider(value: $angle, in: 0.0...Float.pi) + } + .padding() + + HStack { + Text("Radius") + Slider(value: $radius, in: 0.0...1.0) + } + .padding() + } + } + } +#Preview { + kaleidoscopeView() +} diff --git a/Shade/Shade.entitlements b/Shade/Shade.entitlements index 18aff0c..40b639e 100644 --- a/Shade/Shade.entitlements +++ b/Shade/Shade.entitlements @@ -6,5 +6,9 @@ com.apple.security.files.user-selected.read-only + com.apple.security.network.client + + com.apple.security.network.server + diff --git a/Shaders.metal b/Shaders.metal new file mode 100644 index 0000000..41e59a1 --- /dev/null +++ b/Shaders.metal @@ -0,0 +1,41 @@ +#include +using namespace metal; + +struct VertexIn { + float2 position [[attribute(0)]]; + float2 textureCoordinate [[attribute(1)]]; +}; + +struct VertexOut { + float4 position [[position]]; + float2 textureCoordinate; +}; + +vertex VertexOut vertexShader(const device VertexIn* vertex_array [[ buffer(0) ]], + unsigned int vid [[ vertex_id ]]) { + VertexOut out; + out.position = float4(vertex_array[vid].position, 0.0, 1.0); + out.textureCoordinate = vertex_array[vid].textureCoordinate; + return out; +} + +fragment float4 blendFragmentShader(VertexOut in [[stage_in]], + texture2d baseTexture [[texture(0)]], + texture2d blendTexture [[texture(1)]], + sampler textureSampler [[sampler(0)]], + constant int& blendMode [[buffer(0)]]) { + float4 baseColor = baseTexture.sample(textureSampler, in.textureCoordinate); + float4 blendColor = blendTexture.sample(textureSampler, in.textureCoordinate); + + // Implement blend mode calculations here based on blendMode parameter + // This is a simplified example + switch(blendMode) { + case 0: // Normal + return mix(baseColor, blendColor, blendColor.a); + case 1: // Multiply + return baseColor * blendColor; + // Add more blend mode implementations as needed + default: + return baseColor; + } +} diff --git a/ViewController.swift b/ViewController.swift new file mode 100644 index 0000000..05fa306 --- /dev/null +++ b/ViewController.swift @@ -0,0 +1,23 @@ +class ViewController: UIViewController { + private let blendView = BlendLayerView() + + override func viewDidLoad() { + super.viewDidLoad() + setupBlendView() + } + + private func setupBlendView() { + view.addSubview(blendView) + blendView.frame = view.bounds + + // Set base and blend images + if let baseImage = UIImage(named: "baseImage"), + let blendImage = UIImage(named: "blendImage") { + blendView.setBaseImage(baseImage) + blendView.setBlendImage(blendImage) + } + + // Set blend mode + blendView.setBlendMode(.overlay) + } +}