Search code examples
swiftuicolorcolor-schemecolor-wheel

Computing complementary, triadic, tetradic, and analagous colors


I have created swift functions, where I send color value to and want to return triadic and tetrads values. It sort of works, but I am not happy about the color results. Can anyone help me to fine-tune the formula please?

I was following few sources, but the returned colours were too bright or saturated in comparison to several online web based color schemes. I know it's a matter of preference as well and I kinda like the results of the code below, but in some instances of colors the result of one color returned is way too close to the original one, so it's barely visible. It applies only to a few colors...

I was using the formula from here:

enter image description here

my code:

func getTriadColor(color: UIColor) -> (UIColor, UIColor){

    var hue : CGFloat = 0
    var saturation : CGFloat = 0
    var brightness : CGFloat = 0
    var alpha : CGFloat = 0

    let triadHue = CGFloat(color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha))

    let triadColor1 = UIColor(hue: (triadHue + 0.33) - 1.0, saturation: saturation, brightness: brightness, alpha: alpha)
    let triadColor2 = UIColor(hue: (triadHue + 0.66) - 1.0, saturation: saturation, brightness: brightness, alpha: alpha)


    return (triadColor1, triadColor2)

}

func getTetradColor(color: UIColor) -> (UIColor, UIColor, UIColor){

    var hue : CGFloat = 0
    var saturation : CGFloat = 0
    var brightness : CGFloat = 0
    var alpha : CGFloat = 0

    let tetradHue = CGFloat(color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha))

    let tetradColor1 = UIColor(hue: (tetradHue + 0.25) - 1.0, saturation: saturation, brightness: brightness, alpha: alpha)
    let tetradColor2 = UIColor(hue: (tetradHue + 0.5) - 1.0, saturation: saturation, brightness: brightness, alpha: alpha)
    let tetradColor3 = UIColor(hue: (tetradHue + 0.75) - 1.0, saturation: saturation, brightness: brightness, alpha: alpha)



    return (tetradColor1, tetradColor2, tetradColor3)
}

And I also found nice clean code for finding complementary color, which I am very happy about the results

func getComplementColor(color: UIColor) -> UIColor{

    let ciColor = CIColor(color: color)

    let compRed: CGFloat = 1.0 - ciColor.red
    let compGreen: CGFloat = 1.0 - ciColor.green
    let compBlue: CGFloat = 1.0 - ciColor.blue

    return UIColor(red: compRed, green: compGreen, blue: compBlue, alpha: 1.0)
}

Solution

  • Your screen shot is of this web page. (Wayback Machine link because, six years later, the page has been deleted.) The formulas on that page are incorrect, because they specify the use of the absolute value function instead of the modulo function. That is, for example, your screen shot defines

    H1 = |(H0 + 180°) - 360°|

    but consider what this gives for the input H0 = 90°:

    H1 = |(90° + 180°) - 360°| = |270° - 360°| = |-90°| = 90°

    Do you think that the complementary hue of H0 = 90° is H1 = 90°, the same hue?

    The correct formula is

    H1 = (H0 + 180°) mod 360°

    where “mod” is short for “modulo” and means “the remainder after dividing by”. In other words, if the answer would be above 360°, subtract 360°. For H0 = 90°, this gives the correct answer of H1 = 270°.

    But you don't even have this problem in your code, because you didn't use the absolute value function (or the modulo function) in your code. Since you're not doing anything to keep your hue values in the range 0…1, your hue values that are less than zero are clipped to zero, and your hue values above one are clipped to one (and both zero and one mean red).

    Your getComplementColor is also not at all the standard definition of the “complementary color”.

    Here are the correct definitions:

    extension UIColor {
    
        var complement: UIColor {
            return self.withHueOffset(0.5)
        }
    
        var splitComplement0: UIColor {
            return self.withHueOffset(150 / 360)
        }
    
        var splitComplement1: UIColor {
            return self.withHueOffset(210 / 360)
        }
    
        var triadic0: UIColor {
            return self.withHueOffset(120 / 360)
        }
    
        var triadic1: UIColor {
            return self.withHueOffset(240 / 360)
        }
    
        var tetradic0: UIColor {
            return self.withHueOffset(0.25)
        }
    
        var tetradic1: UIColor {
            return self.complement
        }
    
        var tetradic2: UIColor {
            return self.withHueOffset(0.75)
        }
    
        var analagous0: UIColor {
            return self.withHueOffset(-1 / 12)
        }
    
        var analagous1: UIColor {
            return self.withHueOffset(1 / 12)
        }
    
        func withHueOffset(offset: CGFloat) -> UIColor {
            var h: CGFloat = 0
            var s: CGFloat = 0
            var b: CGFloat = 0
            var a: CGFloat = 0
            self.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
            return UIColor(hue: fmod(h + offset, 1), saturation: s, brightness: b, alpha: a)
        }
    }
    

    Here are some examples of complementary colors (original on top, complementary beneath):

    complementary

    Here are split complementary colors (original on top):

    split complementary

    Here are triadic colors (original on top):

    triadic

    Here are tetradic colors (original on top):

    tetradic

    Here are analagous colors (original in the middle):

    analagous

    Here is the playground I used to generate those images:

    import XCPlayground
    import UIKit
    
    let view = UIView(frame: CGRectMake(0, 0, 320, 480))
    view.backgroundColor = [#Color(colorLiteralRed: 0.9607843137254902, green: 0.9607843137254902, blue: 0.9607843137254902, alpha: 1)#]
    
    let vStack = UIStackView(frame: view.bounds)
    vStack.autoresizingMask = [ .FlexibleWidth, .FlexibleHeight ]
    view.addSubview(vStack)
    vStack.axis = .Vertical
    vStack.distribution = .FillEqually
    vStack.alignment = .Fill
    vStack.spacing = 10
    
    typealias ColorTransform = (UIColor) -> UIColor
    
    func tile(color color: UIColor) -> UIView {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = color
        return view
    }
    
    func strip(transforms: [ColorTransform]) -> UIStackView {
        let strip = UIStackView()
        strip.translatesAutoresizingMaskIntoConstraints = false
        strip.axis = .Vertical
        strip.distribution = .FillEqually
        strip.alignment = .Fill
        strip.spacing = 0
    
        let hStacks = (0 ..< transforms.count).map { (i: Int) -> UIStackView in
            let stack = UIStackView()
            stack.translatesAutoresizingMaskIntoConstraints = false
            stack.axis = .Horizontal
            stack.distribution = .FillEqually
            stack.alignment = .Fill
            stack.spacing = 4
            strip.addArrangedSubview(stack)
            return stack
        }
    
        for h in 0 ..< 10 {
            let hue = CGFloat(h) / 10
            let color = UIColor(hue: hue, saturation: 1, brightness: 1, alpha: 1)
            for (i, transform) in transforms.enumerate() {
                hStacks[i].addArrangedSubview(tile(color: transform(color)))
            }
        }
    
        return strip
    }
    
    vStack.addArrangedSubview(strip([
        { $0 },
        { $0.complement }]))
    
    vStack.addArrangedSubview(strip([
        { $0 },
        { $0.splitComplement0 },
        { $0.splitComplement1 }]))
    
    vStack.addArrangedSubview(strip([
        { $0 },
        { $0.triadic0 },
        { $0.triadic1 }]))
    
    vStack.addArrangedSubview(strip([
        { $0 },
        { $0.tetradic0 },
        { $0.tetradic1 },
        { $0.tetradic2 }]))
    
    vStack.addArrangedSubview(strip([
        { $0.analagous0 },
        { $0 },
        { $0.analagous1 }]))
    
    XCPlaygroundPage.currentPage.liveView = view