Computing complementary, triadic, tetradic, and analagous colors
Asked Answered
P

2

7

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)
}
Polky answered 5/5, 2016 at 16:35 Comment(1)
You may have better results posting this to codereview.stackexchange.comRhearheba
C
24

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
Cardie answered 5/5, 2016 at 21:23 Comment(2)
Thanks a lot! That did the trick and I've learned more about the colors. Very elegant solution, much cleaner. Regarding my code, I was thinking to create if statement for subtracting 1 from the float if bigger than 1 as per other reference I found, but your solution is more than splendid!Polky
This is fantastic!Jointed
H
0

If someone needs it, here's an implementation for Jetpack Compose (Android, Kotlin)

import androidx.compose.ui.graphics.Color

fun Color.contrastColor(): Color {
    val r = this.red
    val g = this.green
    val b = this.blue

    val lum = 1 - ((0.299 * r) + (0.587 * g) + (0.114 * b))

    return if (lum < 0.5) Color.Black else Color.White
}

Usage:

class MyViewModel: ViewModel() {

    private val _menuColour = MutableStateFlow(Color.White)
    val menuColour = _menuColour.asStateFlow()

    private fun setColours() {
        val imgColour = Color.Red // this is the colour you need to contrast
        _menuColour.value = imgColour.contrastColor()
    }
}
@Composable
fun MyView(vm: MyViewModel) {
    val menuColour = vm.menuColour.collectAsState()
}
Hieratic answered 18/6, 2023 at 7:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.