Skip to content

Instantly share code, notes, and snippets.

@khanlou
Last active September 24, 2025 14:08
Show Gist options
  • Save khanlou/c0452b26b31dd554d04bc3b62f76133c to your computer and use it in GitHub Desktop.
Save khanlou/c0452b26b31dd554d04bc3b62f76133c to your computer and use it in GitHub Desktop.
enum APCA {
static func gamma(for channel: CGFloat) -> Double {
pow(channel, 2.4)
}
static func luminance(for color: RGBColor) -> Double {
let r = gamma(for: color.red)
let g = gamma(for: color.green)
let b = gamma(for: color.blue)
var y = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b
if (y < 0.022) {
y += pow(0.022 - y, 1.414)
}
return y
}
static func contrast(foreground: RGBColor, background: RGBColor) -> Int {
let c = if (luminance(for: background) > luminance(for: foreground)) {
pow(luminance(for: background), 0.56) - pow(luminance(for: foreground), 0.57)
} else {
pow(luminance(for: background), 0.65) - pow(luminance(for: foreground), 0.62)
}
return Int(abs(1.14 * c * 100) - 2.7)
}
}
enum WCAG2 {
static func gamma(for channel: CGFloat) -> CGFloat {
if channel <= 0.03928 {
channel / 12.92
} else {
pow((channel + 0.055)/1.055, 2.4)
}
}
static func luminance(for color: RGBColor) -> CGFloat {
0.2126 * gamma(for: color.red)
+ 0.7152 * gamma(for: color.green)
+ 0.0722 * gamma(for: color.blue)
}
static func contrast(foreground: RGBColor, background: RGBColor) -> CGFloat {
let brighter = max(luminance(for: foreground), luminance(for: background))
let darker = min(luminance(for: foreground), luminance(for: background))
return (brighter + 0.05) / (darker + 0.05)
}
}
extension APCA {
static let fontSizeToMinimumLcByFontWeight: [(fontSize: Int, weights: [Int])] = [
(12, [999, 999, 999, 999, 999, 999, 999, 999, 999]),
(14, [999, 999, 999, 100, 100, 90, 75, 999, 999]),
(15, [999, 999, 999, 100, 90, 75, 70, 999, 999]),
(16, [999, 999, 999, 90, 75, 70, 60, 60, 0]),
(18, [999, 999, 100, 75, 70, 60, 55, 55, 55]),
(21, [999, 999, 90, 70, 60, 55, 50, 50, 50]),
(24, [999, 999, 75, 60, 55, 50, 45, 45, 45]),
(28, [999, 100, 70, 55, 50, 45, 43, 43, 43]),
(32, [999, 90, 65, 50, 45, 43, 40, 40, 40]),
(36, [999, 75, 60, 45, 43, 40, 38, 38, 38]),
(42, [100, 70, 55, 43, 40, 38, 35, 35, 35]),
(48, [90, 60, 50, 40, 38, 35, 33, 33, 33]),
(60, [75, 55, 45, 38, 35, 33, 30, 30, 30]),
(72, [60, 50, 40, 35, 33, 30, 30, 30, 30]),
(96, [50, 45, 35, 33, 30, 30, 30, 30, 30]),
]
private static let weights: [SwiftUI.Font.Weight] = [.ultraLight, .thin, .light, .regular, .medium, .semibold, .bold, .heavy, .black]
static func passes(foreground: RGBColor, background: RGBColor, fontSize: CGFloat, fontWeight: SwiftUI.Font.Weight) -> Bool {
guard let fontWeightIndex = weights.firstIndex(of: fontWeight) else { return false }
guard let row = self.fontSizeToMinimumLcByFontWeight.last(where: { $0.fontSize <= Int(fontSize) }) else { return false }
let minimumLc = row.weights[fontWeightIndex]
let score = self.contrast(foreground: foreground, background: background)
return score > minimumLc
}
static func minimumFontWeight(foreground: RGBColor, background: RGBColor, fontSize: CGFloat) -> SwiftUI.Font.Weight {
let score = self.contrast(foreground: foreground, background: background)
guard let row = self.fontSizeToMinimumLcByFontWeight.last(where: { $0.fontSize <= Int(fontSize) }) else { return .black }
guard let index = row.weights.firstIndex(where: { $0 < score}) else { return .black }
return weights[index]
}
static func minimumFontSize(foreground: RGBColor, background: RGBColor, fontWeight: SwiftUI.Font.Weight) -> CGFloat? {
let score = self.contrast(foreground: foreground, background: background)
guard let index = weights.firstIndex(of: fontWeight) else { return nil }
let column = fontSizeToMinimumLcByFontWeight.map({ (fontSize: $0.fontSize, score: $0.weights[index] )})
return column.first(where: { $0.score < score }).map({ CGFloat($0.fontSize) })
}
}
struct RGBColor: Hashable {
var red: CGFloat
var green: CGFloat
var blue: CGFloat
var alpha: CGFloat = 1
init (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat = 1) {
self.red = red
self.green = green
self.blue = blue
self.alpha = alpha
}
}
extension RGBColor {
init(hex: Int) {
self.init(
red: Double(((hex >> 16) & 0xFF)) / 255,
green: Double(((hex >> 8) & 0xFF)) / 255,
blue: Double(((hex >> 0) & 0xFF)) / 255
)
}
var hexString: String {
let red = Int(abs(self.red * 255)) << 16
let green = Int(abs(self.green * 255)) << 8
let blue = Int(abs(self.blue * 255))
return String(format: "#%06X", red | green | blue)
}
}
extension RGBColor {
static let red: Self = .init(red: 1, green: 0, blue: 0)
static let green: Self = .init(red: 0, green: 1, blue: 0)
static let blue: Self = .init(red: 0, green: 0, blue: 1)
static let black: Self = .init(red: 0, green: 0, blue: 0)
static let white: Self = .init(red: 1, green: 1, blue: 1)
static let yellow: Self = .init(red: 1, green: 1, blue: 0)
static let cyan: Self = .init(red: 0, green: 1, blue: 1)
static let magenta: Self = .init(red: 1, green: 0, blue: 1)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment