Skip to content

Instantly share code, notes, and snippets.

@rjchatfield
Created October 15, 2019 03:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rjchatfield/4936b1c5c34492f0c5d3aa22e2b6a3cb to your computer and use it in GitHub Desktop.
Save rjchatfield/4936b1c5c34492f0c5d3aa22e2b6a3cb to your computer and use it in GitHub Desktop.
UIColour contrast and luminance
/// Rip off of the JavaScript implementation of:
/// https://github.com/LeaVerou/contrast-ratio/blob/gh-pages/color.js
extension UIColor {
typealias Contrast = (ratio: CGFloat, error: CGFloat, min: CGFloat, max: CGFloat)
/// Range: 1 ... 21
func contrast(_ other: UIColor) -> Contrast {
var other = other
// Formula: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
guard alpha < 1 else {
if other.alpha < 1 {
other = other.overlayOn(self)
}
let l1 = self.luminance + 0.05
let l2 = other.luminance + 0.05
var ratio = l1/l2
if l2 > l1 {
ratio = 1 / ratio
}
return (
ratio: ratio,
error: 0,
min: ratio,
max: ratio
)
}
// If we’re here, it means we have a semi-transparent background
// The text color may or may not be semi-transparent, but that doesn't matter
let onBlack = self.overlayOn(.black)
let onWhite = self.overlayOn(.white)
let contrastOnBlack = onBlack.contrast(other).ratio
let contrastOnWhite = onWhite.contrast(other).ratio
let minContrast: CGFloat
if onBlack.luminance > other.luminance {
minContrast = contrastOnBlack
} else if onWhite.luminance < other.luminance {
minContrast = contrastOnWhite
} else {
minContrast = 1
}
let maxContrast = Swift.max(contrastOnBlack, contrastOnWhite)
return (
ratio: (minContrast + maxContrast) / 2.0,
error: (maxContrast - minContrast) / 2.0,
min: minContrast,
max: maxContrast
)
}
/// Computed property that returns the RGBA components of this UIColor instance.
var rgbaComponents: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
var redRational: CGFloat = 0.0
var greenRational: CGFloat = 0.0
var blueRational: CGFloat = 0.0
var alphaRational: CGFloat = 0.0
getRed(&redRational, green: &greenRational, blue: &blueRational, alpha: &alphaRational)
return (redRational, greenRational, blueRational, alphaRational)
}
/// Range: 0 ... 1
var luminance: CGFloat {
func magic(_ v: CGFloat) -> CGFloat {
assert(v <= 1.0, "Invalid number")
return v <= 0.03928
? v / 12.92
: pow((v + 0.055) / 1.055, 2.4)
}
let components = rgbaComponents
return magic(components.red) * 0.2126
+ magic(components.green) * 0.7152
+ magic(components.blue) * 0.0722
}
var alpha: CGFloat { self.cgColor.alpha }
func overlayOn(_ other: UIColor) -> UIColor {
guard alpha < 1 else { return self }
let this = self.rgbaComponents
let other = other.rgbaComponents
return UIColor(
red: (this.red * this.alpha) + (other.red * other.alpha * (1 - this.alpha)),
green: (this.green * this.alpha) + (other.green * other.alpha * (1 - this.alpha)),
blue: (this.blue * this.alpha) + (other.blue * other.alpha * (1 - this.alpha)),
alpha: this.alpha + (other.alpha * (1 - this.alpha))
)
}
}
// Example
UIColor(red: 1, green: 1, blue: 1, alpha: 1).contrast(UIColor(red: 1, green: 1, blue: 0, alpha: 1)) // 1.074 for yellow
UIColor(red: 1, green: 1, blue: 1, alpha: 1).contrast(UIColor(red: 0, green: 0, blue: 1, alpha: 1)) // 8.592 for blue
// Reversed
UIColor(red: 1, green: 1, blue: 0, alpha: 1).contrast(UIColor(red: 1, green: 1, blue: 1, alpha: 1)) // 1.074 for yellow
UIColor(red: 0, green: 0, blue: 1, alpha: 1).contrast(UIColor(red: 1, green: 1, blue: 1, alpha: 1)) // 8.592 for blue
// Alpha
UIColor(red: 0, green: 0, blue: 1, alpha: 1).contrast(UIColor(red: 1, green: 1, blue: 1, alpha: 0.5)) // 2.625 for blue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment