diff --git a/Sources/iDither/ContentView.swift b/Sources/iDither/ContentView.swift index db8e6dc..d1d6565 100644 --- a/Sources/iDither/ContentView.swift +++ b/Sources/iDither/ContentView.swift @@ -20,6 +20,7 @@ struct ContentView: View { .onChange(of: viewModel.brightness) { _, _ in viewModel.processImage() } .onChange(of: viewModel.contrast) { _, _ in viewModel.processImage() } .onChange(of: viewModel.pixelScale) { _, _ in viewModel.processImage() } + .onChange(of: viewModel.colorDepth) { _, _ in viewModel.processImage() } .onChange(of: viewModel.selectedAlgorithm) { _, _ in viewModel.processImage() } .onChange(of: viewModel.isGrayscale) { _, _ in viewModel.processImage() } // File Importer at the very top level @@ -143,6 +144,17 @@ struct SidebarView: View { } Slider(value: $viewModel.pixelScale, in: 1.0...20.0, step: 1.0) } + + VStack(alignment: .leading) { + HStack { + Text("Color Depth") + Spacer() + Text("\(Int(viewModel.colorDepth))") + .monospacedDigit() + .foregroundStyle(.secondary) + } + Slider(value: $viewModel.colorDepth, in: 1.0...32.0, step: 1.0) + } } } .formStyle(.grouped) diff --git a/Sources/iDither/Renderer/MetalImageRenderer.swift b/Sources/iDither/Renderer/MetalImageRenderer.swift index 8d4351d..f1975aa 100644 --- a/Sources/iDither/Renderer/MetalImageRenderer.swift +++ b/Sources/iDither/Renderer/MetalImageRenderer.swift @@ -6,6 +6,7 @@ struct RenderParameters { var brightness: Float var contrast: Float var pixelScale: Float + var colorDepth: Float // New parameter var algorithm: Int32 // 0: None, 1: Bayer 8x8, 2: Bayer 4x4 var isGrayscale: Int32 // 0: false, 1: true } diff --git a/Sources/iDither/Resources/Shaders.metal b/Sources/iDither/Resources/Shaders.metal index 838ebee..517bedf 100644 --- a/Sources/iDither/Resources/Shaders.metal +++ b/Sources/iDither/Resources/Shaders.metal @@ -5,6 +5,7 @@ struct RenderParameters { float brightness; float contrast; float pixelScale; + float colorDepth; // New parameter: 1.0 to 32.0 (Levels) int algorithm; // 0: None, 1: Bayer 2x2, 2: Bayer 4x4, 3: Bayer 8x8, 4: Cluster 4x4, 5: Cluster 8x8, 6: Blue Noise int isGrayscale; }; @@ -67,6 +68,16 @@ constant float blueNoise8x8[8][8] = { {35.0/64.0, 24.0/64.0, 0.0/64.0, 41.0/64.0, 15.0/64.0, 52.0/64.0, 20.0/64.0, 37.0/64.0} }; +float ditherChannel(float value, float threshold, float limit) { + // Quantization Formula + // value: 0.0 to 1.0 + // threshold: 0.0 to 1.0 (from matrix) + // limit: colorDepth (e.g. 4.0) + + float ditheredValue = value + (threshold - 0.5) * (1.0 / (limit - 1.0)); + return floor(ditheredValue * (limit - 1.0) + 0.5) / (limit - 1.0); +} + kernel void ditherShader(texture2d inputTexture [[texture(0)]], texture2d outputTexture [[texture(1)]], constant RenderParameters ¶ms [[buffer(0)]], @@ -104,6 +115,7 @@ kernel void ditherShader(texture2d inputTexture [[texture(0 if (shouldDither) { uint x, y; + // Fetch threshold from matrix switch (params.algorithm) { case 1: // Bayer 2x2 x = uint(sourceCoord.x / scale) % 2; @@ -139,7 +151,18 @@ kernel void ditherShader(texture2d inputTexture [[texture(0 break; } - rgb = (luma > threshold) ? float3(1.0) : float3(0.0); + // Apply Quantized Dithering + if (params.isGrayscale > 0) { + // Apply only to luma (which is already in rgb) + rgb.r = ditherChannel(rgb.r, threshold, params.colorDepth); + rgb.g = rgb.r; + rgb.b = rgb.r; + } else { + // Apply to each channel + rgb.r = ditherChannel(rgb.r, threshold, params.colorDepth); + rgb.g = ditherChannel(rgb.g, threshold, params.colorDepth); + rgb.b = ditherChannel(rgb.b, threshold, params.colorDepth); + } } outputTexture.write(float4(rgb, color.a), gid); diff --git a/Sources/iDither/ViewModel/DitherViewModel.swift b/Sources/iDither/ViewModel/DitherViewModel.swift index f57b4d0..66f0cbd 100644 --- a/Sources/iDither/ViewModel/DitherViewModel.swift +++ b/Sources/iDither/ViewModel/DitherViewModel.swift @@ -37,7 +37,8 @@ class DitherViewModel { var brightness: Double = 0.0 var contrast: Double = 1.0 var pixelScale: Double = 4.0 - var selectedAlgorithm: DitherAlgorithm = .bayer4x4 // Default to Balanced + var colorDepth: Double = 4.0 // Default to 4 levels + var selectedAlgorithm: DitherAlgorithm = .bayer4x4 var isGrayscale: Bool = false private let renderer = MetalImageRenderer() @@ -67,6 +68,7 @@ class DitherViewModel { brightness: Float(brightness), contrast: Float(contrast), pixelScale: Float(pixelScale), + colorDepth: Float(colorDepth), algorithm: Int32(selectedAlgorithm.rawValue), isGrayscale: isGrayscale ? 1 : 0 )