Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions BlendApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import SwiftUI

@main
struct BlendApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.windowStyle(HiddenTitleBarWindowStyle())
.commands {
CommandGroup(replacing: .newItem) { }
}
}
}
97 changes: 97 additions & 0 deletions BlendLayerMetalRenderer.swift
Original file line number Diff line number Diff line change
@@ -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<Float>(-1, -1), textureCoordinate: SIMD2<Float>(0, 1)),
Vertex(position: SIMD2<Float>(1, -1), textureCoordinate: SIMD2<Float>(1, 1)),
Vertex(position: SIMD2<Float>(-1, 1), textureCoordinate: SIMD2<Float>(0, 0)),
Vertex(position: SIMD2<Float>(1, 1), textureCoordinate: SIMD2<Float>(1, 0))
]

vertexBuffer = device.makeBuffer(bytes: vertices,
length: vertices.count * MemoryLayout<Vertex>.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<Int32>.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) {}
}
103 changes: 103 additions & 0 deletions BlendLayerView.swift
Original file line number Diff line number Diff line change
@@ -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()
}
70 changes: 70 additions & 0 deletions BlendMode.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
97 changes: 97 additions & 0 deletions ContentView.swift
Original file line number Diff line number Diff line change
@@ -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<ContentView, Binding<NSImage?>>) {
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()
}
}
Loading