From 2acd7276d1c0e6489c06736226281ddc03cf2df4 Mon Sep 17 00:00:00 2001 From: Kami Date: Sat, 6 Jul 2024 21:52:26 +1000 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20Add=20Luminance=20&=20Quantized?= =?UTF-8?q?=20methods=20+=20Live=20toggle=20switch=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Extensions/Color+Extensions.swift | 28 ++++++++++ .../Theming/AccentColorConfiguration.swift | 3 +- Loop/Luminare/Theming/WallpaperColors.swift | 55 ++++++++++--------- 3 files changed, 60 insertions(+), 26 deletions(-) diff --git a/Loop/Extensions/Color+Extensions.swift b/Loop/Extensions/Color+Extensions.swift index 1739a124..947654fd 100644 --- a/Loop/Extensions/Color+Extensions.swift +++ b/Loop/Extensions/Color+Extensions.swift @@ -8,6 +8,8 @@ import Defaults import SwiftUI +// MARK: - Loop theming + extension Color { enum LoopAccentTone { case normal @@ -49,6 +51,17 @@ extension NSColor { Int(rgbColor.blueComponent * 0xFF)) } + /// Calculates the brightness of the color based on luminance. + /// Brightness is calculated using the luminance formula, which considers the different contributions + /// of the red, green, and blue components of the color. This property can be used to determine + /// how light or dark a color is perceived to be. + var brightness: CGFloat { + // Ensure the color is in the sRGB color space for accurate luminance calculation. + guard let rgbColor = usingColorSpace(.sRGB) else { return 0 } + // Calculate brightness using the luminance formula. + return 0.299 * rgbColor.redComponent + 0.587 * rgbColor.greenComponent + 0.114 * rgbColor.blueComponent + } + /// Determines if two colors are similar based on a threshold. /// - Parameters: /// - color: The color to compare with the receiver. @@ -63,4 +76,19 @@ extension NSColor { abs(color1.greenComponent - color2.greenComponent) < threshold && abs(color1.blueComponent - color2.blueComponent) < threshold } + + /// Quantizes the color to a limited set of values. + /// This process reduces the color's precision, effectively snapping it to a grid + /// in the color space defined by the quantization level. This simplification can + /// be beneficial for analyzing colors in smaller images by reducing the color palette's complexity. + /// - Returns: A quantized NSColor. + func quantized(levels: Double = 512.0) -> NSColor { + guard let sRGBColor = usingColorSpace(.sRGB) else { return self } + let divisionFactor = levels - 1 + let red = round(sRGBColor.redComponent * divisionFactor) / divisionFactor + let green = round(sRGBColor.greenComponent * divisionFactor) / divisionFactor + let blue = round(sRGBColor.blueComponent * divisionFactor) / divisionFactor + let alpha = round(sRGBColor.alphaComponent * divisionFactor) / divisionFactor + return NSColor(srgbRed: red, green: green, blue: blue, alpha: alpha) + } } diff --git a/Loop/Luminare/Theming/AccentColorConfiguration.swift b/Loop/Luminare/Theming/AccentColorConfiguration.swift index c059a112..526968d0 100644 --- a/Loop/Luminare/Theming/AccentColorConfiguration.swift +++ b/Loop/Luminare/Theming/AccentColorConfiguration.swift @@ -81,7 +81,8 @@ struct AccentColorConfigurationView: View { } if model.isCustom || model.isWallpaper { - LuminareToggle("Gradient", isOn: $model.useGradient.animation(LuminareSettingsWindow.animation)) + LuminareToggle("Gradient", isOn: $model.useGradient) + .animation(LuminareSettingsWindow.animation, value: model.useGradient) } if model.processWallpaper { diff --git a/Loop/Luminare/Theming/WallpaperColors.swift b/Loop/Luminare/Theming/WallpaperColors.swift index 45eced3c..26ecf45e 100644 --- a/Loop/Luminare/Theming/WallpaperColors.swift +++ b/Loop/Luminare/Theming/WallpaperColors.swift @@ -14,12 +14,16 @@ import SwiftUI extension NSImage { /// Calculates the dominant colors of the image asynchronously. /// - Returns: An array of NSColor representing the dominant colors, or nil if an error occurs. + /// Resizing the image to a smaller size improves performance by reducing the number of pixels that need to be analyzed. + /// NOTE: This function tends to return darker colors, which can be problematic with darker wallpapers. To address this, + /// a brightness threshold is applied to filter out excessively dark colors. Additionally, the function filters out colors + /// that are very similar to each other, such as #000000 and #010101, to ensure a more diverse and representative color palette. func calculateDominantColors() async -> [NSColor]? { // Resize the image to a smaller size to improve performance of color calculation. let aspectRatio = size.width / size.height - let resizedImage = resized(to: NSSize(width: 100 * aspectRatio, height: 100)) + let resizedImage = resized(to: NSSize(width: 200 * aspectRatio, height: 200)) - // Ensure we can get the CGImage and its data provider. + // Ensure we can get the CGImage and its data provider from the resized image. guard let resizedCGImage = resizedImage?.cgImage(forProposedRect: nil, context: nil, hints: nil), let dataProvider = resizedCGImage.dataProvider, @@ -29,7 +33,7 @@ extension NSImage { return nil } - // Calculate the number of bytes per pixel and per row. + // Calculate the number of bytes per pixel and per row to access pixel data correctly. let bytesPerPixel = resizedCGImage.bitsPerPixel / 8 let bytesPerRow = resizedCGImage.bytesPerRow let width = resizedCGImage.width @@ -40,55 +44,56 @@ extension NSImage { for y in 0 ..< height { for x in 0 ..< width { let pixelData = Int(y * bytesPerRow + x * bytesPerPixel) - // Determine the alpha value based on the presence of an alpha channel. let alpha = (bytesPerPixel == 4) ? CGFloat(data[pixelData + 3]) / 255.0 : 1.0 - // Create an NSColor instance for the current pixel. - let color = NSColor( + // Create an NSColor instance for the current pixel using RGBA values. + var color = NSColor( red: CGFloat(data[pixelData]) / 255.0, green: CGFloat(data[pixelData + 1]) / 255.0, blue: CGFloat(data[pixelData + 2]) / 255.0, alpha: alpha ) - // Increment the count for this color. + // Apply a quantization method to the color to reduce the color space complexity. + color = color.quantized() + // Increment the count for this color in the map. colorCountMap[color, default: 0] += 1 } } + // Filter out very dark colors based on a brightness threshold to avoid dominance of dark shades. + let brightnessThreshold: CGFloat = 0.2 // Filtered threshold. + let filteredByBrightness = colorCountMap.filter { $0.key.brightness > brightnessThreshold } + + // If all colors are dark and the filtered map is empty, fallback to the original map. + let finalColors = filteredByBrightness.isEmpty ? colorCountMap : filteredByBrightness + // Sort the colors by occurrence to find the most dominant colors. - let sortedColors = colorCountMap.sorted { $0.value > $1.value }.map(\.key) + let sortedColors = finalColors.sorted { $0.value > $1.value }.map(\.key) - // Filter out colors that are too similar to each other. - let filteredColors = filterSimilarColors(colors: sortedColors) + // Further filter out colors that are too similar to each other to ensure a diverse color palette. + let distinctColors = filterSimilarColors(colors: sortedColors) - return filteredColors + return distinctColors } /// Helper function to resize the image to a new size. /// - Parameter newSize: The target size for the resized image. /// - Returns: The resized NSImage or nil if the operation fails. func resized(to newSize: NSSize) -> NSImage? { - // Create a new bitmap representation with the specified size. - 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 { + 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 { NSLog("Error: Unable to create NSBitmapImageRep for new size.") return nil } - - // Draw the current image onto the new bitmap representation. bitmapRep.size = newSize NSGraphicsContext.saveGraphicsState() NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: bitmapRep) draw(in: NSRect(x: 0, y: 0, width: newSize.width, height: newSize.height), - from: NSRect.zero, operation: .copy, fraction: 1.0) + from: NSRect.zero, operation: .copy, fraction: 1.0, respectFlipped: true, hints: [NSImageRep.HintKey.interpolation: NSNumber(value: NSImageInterpolation.high.rawValue)]) NSGraphicsContext.restoreGraphicsState() - - // Create a new NSImage from the bitmap representation. let resizedImage = NSImage(size: newSize) resizedImage.addRepresentation(bitmapRep) return resizedImage