Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support generate palette title/body colors. #2

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
4 changes: 4 additions & 0 deletions Framework/Palette.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
0FE76B0322FCD1CA00F23EE6 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FE76AF922FCD1CA00F23EE6 /* Color.swift */; };
0FE76B0422FCD1CA00F23EE6 /* PaletteBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FE76AFA22FCD1CA00F23EE6 /* PaletteBuilder.swift */; };
0FE76B0622FCD2C100F23EE6 /* UIImage+Palette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FE76B0522FCD2C100F23EE6 /* UIImage+Palette.swift */; };
DB2CF5182807C19B00AAF1C7 /* ColorUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2CF5172807C19B00AAF1C7 /* ColorUtils.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -40,6 +41,7 @@
0FE76AF922FCD1CA00F23EE6 /* Color.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; };
0FE76AFA22FCD1CA00F23EE6 /* PaletteBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaletteBuilder.swift; sourceTree = "<group>"; };
0FE76B0522FCD2C100F23EE6 /* UIImage+Palette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Palette.swift"; sourceTree = "<group>"; };
DB2CF5172807C19B00AAF1C7 /* ColorUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorUtils.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -92,6 +94,7 @@
0FE76AF922FCD1CA00F23EE6 /* Color.swift */,
0F53B15922FF643700039D92 /* ColorConverter.swift */,
0FE76B0522FCD2C100F23EE6 /* UIImage+Palette.swift */,
DB2CF5172807C19B00AAF1C7 /* ColorUtils.swift */,
0FE76AF122FCD1CA00F23EE6 /* Data Structures */,
);
name = Source;
Expand Down Expand Up @@ -187,6 +190,7 @@
buildActionMask = 2147483647;
files = (
0F53B15A22FF643700039D92 /* ColorConverter.swift in Sources */,
DB2CF5182807C19B00AAF1C7 /* ColorUtils.swift in Sources */,
0FE76B0222FCD1CA00F23EE6 /* ColorCutQuantizer.swift in Sources */,
0FE76B0022FCD1CA00F23EE6 /* Target.swift in Sources */,
0FE76AFD22FCD1CA00F23EE6 /* Heap.swift in Sources */,
Expand Down
145 changes: 145 additions & 0 deletions Source/ColorUtils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//
// ColorUtils.swift
// Palette
//
// Copyright © 2022 Egor Snitsar. All rights reserved.
//

import Foundation
import UIKit
import CoreGraphics

private let MinAlphaSearchMaxIterations = 10
private let MinAlphaSearchPrecision: CGFloat = 1.0/255

extension UIColor {
var coreImageColor: CIColor {
return CIColor(color: self)
}
var hex: UInt {
let alpha = UInt(coreImageColor.alpha * 255 + 0.5)
let red = UInt(coreImageColor.red * 255 + 0.5)
let green = UInt(coreImageColor.green * 255 + 0.5)
let blue = UInt(coreImageColor.blue * 255 + 0.5)
return (alpha << 24) | (red << 16) | (green << 8) | blue
}
}

final class ColorUtils {

private class func compositeColors(forgroundColor: UIColor, backgroundColor: UIColor) -> UIColor {

let fgColor = forgroundColor.coreImageColor
let bgColor = backgroundColor.coreImageColor

let fgAlpha = fgColor.alpha
let bgAlpha = bgColor.alpha

let compositeAlpha = compositeAlpha(foregroundAlpha: fgAlpha, backgroundAlpha: bgAlpha)

let r = compositeComponent(fgChannel: fgColor.red, fgAlpha: fgAlpha, bgChannel: bgColor.red, bgAlpha: bgAlpha, compositeAlpha: compositeAlpha)
let g = compositeComponent(fgChannel: fgColor.green, fgAlpha: fgAlpha, bgChannel: bgColor.green, bgAlpha: bgAlpha, compositeAlpha: compositeAlpha)
let b = compositeComponent(fgChannel: fgColor.blue, fgAlpha: fgAlpha, bgChannel: bgColor.blue, bgAlpha: bgAlpha, compositeAlpha: compositeAlpha)

return UIColor(red: r, green: g, blue: b, alpha: compositeAlpha)
}

private class func compositeAlpha(foregroundAlpha:CGFloat, backgroundAlpha:CGFloat) -> CGFloat {
// 0xFF - (((0xFF - bgAlpha) * (0xFF - fgAlpha)) / 0xFF)
return 1.0 - ((1.0 - backgroundAlpha) * (1.0 - foregroundAlpha))
}


private class func compositeComponent(fgChannel:CGFloat, fgAlpha:CGFloat, bgChannel:CGFloat, bgAlpha:CGFloat, compositeAlpha:CGFloat) -> CGFloat {
if (compositeAlpha == 0) {
return 0.0
}
// ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF)
return ((fgChannel * fgAlpha) + (bgChannel * bgAlpha * (1.0 - fgAlpha)))/compositeAlpha
}

private class func calculateLuminance(color: UIColor) -> CGFloat {

let r = color.coreImageColor.red
let g = color.coreImageColor.green
let b = color.coreImageColor.blue

let sr = r < 0.04045 ? r / 12.92 : pow((r + 0.055) / 1.055, 2.4)
let sg = g < 0.04045 ? g / 12.92 : pow((g + 0.055) / 1.055, 2.4)
let sb = b < 0.04045 ? b / 12.92 : pow((b + 0.055) / 1.055, 2.4)

//outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805)
//outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722)
//outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505)
return sr * 0.2126 + sg * 0.7152 + sb * 0.0722
}

private class func calculateContrast(foreground: UIColor, background: UIColor) -> CGFloat {
// background can not be translucent
if (background.coreImageColor.alpha != 1.0){
return -1
}

let newForegroundColor:UIColor
if(foreground.coreImageColor.alpha < 1.0) {
// If the foreground is translucent, composite the foreground over the background
newForegroundColor = compositeColors(forgroundColor : foreground, backgroundColor : background)
}
else {
newForegroundColor = foreground
}

let luminance1 = calculateLuminance(color: newForegroundColor) + 0.05
let luminance2 = calculateLuminance(color: background) + 0.05

// Now return the lighter luminance divided by the darker luminance
return max(luminance1, luminance2) / min(luminance1, luminance2)
}

class func calculateMinimumAlpha(foreground: UIColor, background: UIColor, minContrastRatio: CGFloat) -> CGFloat {
let alpha = background.coreImageColor.alpha

if (alpha != 1.0) {
// background can not be translucent
return -1
}

// First lets check that a fully opaque foreground has sufficient contrast
var testForeground = modifyAlpha(color: foreground, alpha: 1.0)
var testRatio = calculateContrast(foreground: testForeground, background: background)
if (testRatio < minContrastRatio) {
// Fully opaque foreground does not have sufficient contrast, return error
return -1
}

// Binary search to find a value with the minimum value which provides sufficient contrast
var numIterations = 0
var minAlpha: CGFloat = 0
var maxAlpha: CGFloat = 1.0

while (numIterations <= MinAlphaSearchMaxIterations &&
(maxAlpha - minAlpha) > MinAlphaSearchPrecision) {

let testAlpha = (minAlpha + maxAlpha) / 2.0

testForeground = modifyAlpha(color: foreground, alpha: testAlpha)
testRatio = calculateContrast(foreground: testForeground, background: background)

if (testRatio < minContrastRatio) {
minAlpha = testAlpha
}
else {
maxAlpha = testAlpha
}

numIterations += 1
}

// Conservatively return the max of the range of possible alphas, which is known to pass.
return maxAlpha
}

class func modifyAlpha(color: UIColor, alpha: CGFloat) -> UIColor {
return color.withAlphaComponent(alpha)
}
}
81 changes: 79 additions & 2 deletions Source/PaletteSwatch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ import UIKit
public typealias RGB = (r: Int, g: Int, b: Int)
public typealias HSL = (h: CGFloat, s: CGFloat, l: CGFloat)

extension Palette {
private let MinContrastTitleText: CGFloat = 3.0
private let MinContrastBodyText: CGFloat = 4.5
private let MinAlphaSearchMaxIterations = 10
private let MinAlphaSearchPrecision: CGFloat = 0.05

extension Palette {
public final class Swatch: CustomDebugStringConvertible {

public private(set) lazy var color = UIColor(_color)
Expand All @@ -21,7 +25,8 @@ extension Palette {
public private(set) lazy var rgb: RGB = _color.rgb

public let population: Int

public var calculateColors: Bool = false

public var debugDescription: String {
return """

Expand All @@ -36,5 +41,77 @@ extension Palette {
}

internal let _color: Color

private var titleTextColor: UIColor!
private var bodyTextColor: UIColor!


public func getTitleTextColor() -> UIColor{
ensureTextColorsGenerated()
return titleTextColor
}

public func getBodyTextColor() -> UIColor{
ensureTextColorsGenerated()
return bodyTextColor
}

private func ensureTextColorsGenerated() -> () {
if (!calculateColors) {

var bodyColor:UIColor
var titleColor:UIColor

// First check white, as most colors will be dark
let lightBodyAlpha = ColorUtils.calculateMinimumAlpha(foreground: UIColor.white, background: color, minContrastRatio: MinContrastBodyText)
let lightTitleAlpha = ColorUtils.calculateMinimumAlpha(foreground: UIColor.white, background: color, minContrastRatio: MinContrastTitleText)


if (lightBodyAlpha != -1 && lightTitleAlpha != -1) {
// If we found valid light values, use them and return
bodyColor = ColorUtils.modifyAlpha(color: UIColor.white, alpha: lightBodyAlpha)
titleColor = ColorUtils.modifyAlpha(color: UIColor.white, alpha: lightTitleAlpha)
bodyTextColor = bodyColor
titleTextColor = titleColor
calculateColors = true
return
}

let darkBodyAlpha = ColorUtils.calculateMinimumAlpha(foreground: UIColor.black, background: color, minContrastRatio: MinContrastBodyText)
let darkTitleAlpha = ColorUtils.calculateMinimumAlpha(foreground: UIColor.black, background: color, minContrastRatio: MinContrastTitleText)

if (darkBodyAlpha != -1 && darkTitleAlpha != -1) {
// If we found valid dark values, use them and return
bodyColor = ColorUtils.modifyAlpha(color: UIColor.black, alpha: darkBodyAlpha)
titleColor = ColorUtils.modifyAlpha(color: UIColor.black, alpha: darkTitleAlpha)
bodyTextColor = bodyColor
titleTextColor = titleColor
calculateColors = true
return
}

// If we reach here then we can not find title and body values which use the same
// lightness, we need to use mismatched values
if(lightBodyAlpha != -1){
bodyColor = ColorUtils.modifyAlpha(color: UIColor.white, alpha: lightBodyAlpha)
}
else {
bodyColor = ColorUtils.modifyAlpha(color: UIColor.black, alpha: lightBodyAlpha)
}


if(lightTitleAlpha != -1){
titleColor = ColorUtils.modifyAlpha(color: UIColor.white, alpha: lightTitleAlpha)
}
else {
titleColor = ColorUtils.modifyAlpha(color: UIColor.black, alpha: lightTitleAlpha)
}

bodyTextColor = bodyColor
titleTextColor = titleColor
calculateColors = true
}
}

}
}