Blend UIColors in Swift
Asked Answered
C

6

17

I have two SKSpriteNode and their colors are defined like this:

colorNode[0].color = UIColor(red: 255, green: 0, blue: 0, alpha: 1)
colorNode[1].color = UIColor(red: 0, green: 255, blue: 0, alpha: 1)

and I want to have a third SKSpriteNode colorized with a blend of the two first ones, the result should be like this :

colorNode[2].color = UIColor(red: 255, green: 255, blue: 0, alpha: 1)

but is there a way to addition two UIColors ? Like this :

colorNode[2].color = colorNode[0].color + colorNode[1].color
Condonation answered 7/12, 2014 at 12:46 Comment(1)
Note, color component values are CGFloats that go from 0.0 to 1.0, not from 0 to 255.Jog
J
18

How about something like this:

func addColor(_ color1: UIColor, with color2: UIColor) -> UIColor {
    var (r1, g1, b1, a1) = (CGFloat(0), CGFloat(0), CGFloat(0), CGFloat(0))
    var (r2, g2, b2, a2) = (CGFloat(0), CGFloat(0), CGFloat(0), CGFloat(0))

    color1.getRed(&r1, green: &g1, blue: &b1, alpha: &a1)
    color2.getRed(&r2, green: &g2, blue: &b2, alpha: &a2)

    // add the components, but don't let them go above 1.0
    return UIColor(red: min(r1 + r2, 1), green: min(g1 + g2, 1), blue: min(b1 + b2, 1), alpha: (a1 + a2) / 2)
}

func multiplyColor(_ color: UIColor, by multiplier: CGFloat) -> UIColor {
    var (r, g, b, a) = (CGFloat(0), CGFloat(0), CGFloat(0), CGFloat(0))
    color.getRed(&r, green: &g, blue: &b, alpha: &a)
    return UIColor(red: r * multiplier, green: g * multiplier, blue: b * multiplier, alpha: a)
}

Define operators to add colors and multiply a color by a Double:

func +(color1: UIColor, color2: UIColor) -> UIColor {
    return addColor(color1, with: color2)
}

func *(color: UIColor, multiplier: Double) -> UIColor {
    return multiplyColor(color, by: CGFloat(multiplier))
}

Then you can blend colors like this:

// Make orange with 50% red and 50% yellow    
let orange = .red * 0.5 + .yellow * 0.5

// Make light gray with 25% black and 75% white
let lightGray = .black * 0.25 + .white * 0.75

// Make sky blue by lightening a combination of 25% blue and 75% cyan
let skyBlue = (.blue * 0.25 + .cyan * 0.75) * 0.25 + .white * 0.75

// Make dark red by combining 50% red and 50% black
let darkRed = .red * 0.50 + .black * 0.50

// Make purple from 60% blue and 40% red
let purple = (.blue * 0.60 + .red * 0.40)

// Then make lavender from 25% purple and 75% white
let lavender = purple * 0.25 + .white * 0.75
Jog answered 7/12, 2014 at 13:48 Comment(5)
How can you add this to an extension?Cletis
max(r1, r2) is not blending. If you blend black with white, it will just end up white with this solution.Tympanist
@RobertGummesson, you're right. I was responding to the OP who was just adding the components. I have updated the answer with a solution that can be used to blend colors.Jog
That's actually a really beautiful way to blend colors, sir. I like that a lot. :)Heisel
Thanks, @RyuX51! I'm happy to hear someone else appreciates its beauty.Jog
P
12

Swift3 version, as extension with some more guards and fine grained adjustment:

extension UIColor {
    static func blend(color1: UIColor, intensity1: CGFloat = 0.5, color2: UIColor, intensity2: CGFloat = 0.5) -> UIColor {
        let total = intensity1 + intensity2
        let l1 = intensity1/total
        let l2 = intensity2/total
        guard l1 > 0 else { return color2}
        guard l2 > 0 else { return color1}
        var (r1, g1, b1, a1): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
        var (r2, g2, b2, a2): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)

        color1.getRed(&r1, green: &g1, blue: &b1, alpha: &a1)
        color2.getRed(&r2, green: &g2, blue: &b2, alpha: &a2)

        return UIColor(red: l1*r1 + l2*r2, green: l1*g1 + l2*g2, blue: l1*b1 + l2*b2, alpha: l1*a1 + l2*a2)
    }
}
Phrenetic answered 4/12, 2016 at 17:46 Comment(5)
Only thing is that you don't actually need the color1 parameter if you make it and extension. You can use self instead.Nodab
@Nodab if you want you can modify it to be a instance function, but I liked the symmetry with (color1, intensity1, color2, intensity2)Phrenetic
@Phrenetic Any idea about the math for calculating the intensities if you want to simulate one color with a partial alpha over another color? Example: UIColor.blue.withAlphaComponent(0.8) over UIColor.white. Basically, I want to simulate a partially transparent color without the actual transparency.Yeung
@Yeung What happens when you try blend(color1: UIColor.white, intensity1: 0.2, color1: UIColor.blue, intensity1: 0.8) ? Should look the same as blending transparency, no?Phrenetic
@Phrenetic Yes, I think that's correct. I took a different approach (see my answer below), but that makes sense.Yeung
S
6

The code has achieved all the layer blend for photoshop.If is useful for you.Don't forget to give me a start. Have fun~

GitHub:https://github.com/Orange-W/PhotoshopBending

import Foundation
import UIKit

extension UIColor {
    // MARK: - 常用叠图
    // Alpha Blending 前景色叠图
    func blendAlpha(coverColor: UIColor) -> UIColor {
        let c1 = coverColor.rgbaTuple()
        let c2 = self.rgbaTuple()

        let c1r = CGFloat(c1.r)
        let c1g = CGFloat(c1.g)
        let c1b = CGFloat(c1.b)

        let c2r = CGFloat(c2.r)
        let c2g = CGFloat(c2.g)
        let c2b = CGFloat(c2.b)

        // 前景色叠图公式
        let r = c1r * c1.a + c2r  * (1 - c1.a)
        let g = c1g * c1.a + c2g  * (1 - c1.a)
        let b = c1b * c1.a + c2b  * (1 - c1.a)

        return UIColor.init(red: r/255.0, green: g/255.0, blue: b/255.0, alpha: 1.0)
    }


    // MARK: - 去亮度型
    /// Darken 变暗  B<=A: C=B; B>=A: C=A
    func blendDarken(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) { return ($0 <= $1) ? $0 : $1 }
    }

    /// Multiply 正片叠底 C = A*B
    func blendMultiply(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) { return $0 * $1 }
    }

    /// Color Burn 颜色加深 C=1-(1-B)/A
    func blendColorBurn(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) { return 1 - (1 - $0) / $1 }
    }

    /// Linear Burn 线性加深 C=A+B-1
    func blendLinearBurn(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) { return ($1 + $0) - 1.0 }
    }

    // MARK: - 去暗型
    /// Lighten 变亮   B>=A: C=B; B<=A: C=A
    func blendLighten(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) { return ($0 >= $1) ? $0 : $1 }
    }

    /// Screen 滤色 C=1-(1-A)*(1-B), 也可以写成 1-C=(1-A)*(1-B)
    func blendScreen(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) { return 1 - (1 - $1) * (1 - $0) }
    }

    /// Color Dodge 颜色减淡 C=B/(1-A)
    func blendColorDodge(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) {
            if $1 >= 1.0 { return $1 }
            else { return min(1.0, $0 / (1 - $1)) }
        }
    }

    /// Linear Dodge 线性减淡 C=A+B
    func blendLinearDodge(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) { return min(1, $1 + $0) }
    }


    // MARK: - 溶合型
    /// Overlay 叠加 B<=0.5: C=2*A*B; B>0.5: C=1-2*(1-A)*(1-B)
    func blendOverlay(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) {
            if $0 <= 0.5 { return 2 * $1 * $0 }
            else { return 1 - 2 * (1 - $1) * (1 - $0) }
        }
    }

    /// Soft Light 柔光 A<=0.5: C=(2*A-1)*(B-B*B)+B; A>0.5: C=(2*A-1)*(sqrt(B)-B)+B
    func blendSoftLight(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) {
            if $1 <= 0.5 { return (2 * $1 - 1) * ($0 - $0 * $0) + $0 }
            else { return (2 * $1 - 1)*( sqrt($0) - $0) + $0 }
        }
    }

    /// Hard Light 强光 A<=0.5: C=2*A*B; A>0.5: C=1-2*(1-A)*(1-B)
    func blendHardLight(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) {
            if $1 <= 0.5 { return 2 * $1 * $0 }
            else { return 1 - 2 * (1 - $1) * (1 - $0) }
        }
    }

    /// Vivid Light 亮光 A<=0.5: C=1-(1-B)/(2*A); A>0.5: C=B/(2*(1-A))
    func blendVividLight(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) {
            if $1 <= 0.5 { return self.fitIn((1 - (1 - $0) / (2 * $1)), ceil: 1.0) }
            else { return self.fitIn($0 / (2 * (1 - $1)), ceil: 1.0) }
        }
    }

    /// Linear Light 线性光 C=B+2*A-1
    func blendLinearLight(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) { return self.fitIn($0 + 2 * $1 - 1, ceil: 1.0) }
    }

    /// Pin Light 点光
    /// B<2*A-1:     C=2*A-1
    /// 2*A-1<B<2*A: C=B
    /// B>2*A:       C=2*A
    func blendPinLight(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) {
            if $0 <= 2 * $1 - 1 { return 2 * $1 - 1 }
            else if (2 * $1 - 1 < $0) && ($0 < 2 * $1) { return $0}
            else { return 2 * $1 }
        }
    }

    /// Hard Mix 实色混合A<1-B: C=0; A>1-B: C=1
    func blendHardMix(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) {
            if $1 <= 1 - $0 { return 0 }
            else { return 1 }
        }
    }

    // MARK: - 色差型
    /// Difference 差值 C=|A-B|
    func blendDifference(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) { fabs($1 - $0) }
    }

    /// Exclusion 排除 C = A+B-2*A*B
    func blendExclusion(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) { $1 + $0 - 2 * $1 * $0  }
    }

    /// 减去 C=A-B
    func blendMinus(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) { $1 - $0 }
    }

    /// 划分 C=A/B
    func blendDivision(coverColor: UIColor,alpha: CGFloat = 1.0) -> UIColor {
        return blendProcedure(coverColor: coverColor, alpha: alpha) {
            if $0 == 0{
                return 1.0
            }else {
                return self.fitIn($1 / $0, ceil: 1.0)
            }
        }
    }

    // MARK: 处理函数
    func blendProcedure(
        coverColor: UIColor,
        alpha: CGFloat,
        procedureBlock: ((_ baseValue: CGFloat,_ topValue: CGFloat) -> CGFloat)?
        ) -> UIColor {
        let baseCompoment = self.rgbaTuple()
        let topCompoment = coverColor.rgbaTuple()

        // 该层透明度
        let mixAlpha = alpha * topCompoment.a + (1.0 - alpha) * baseCompoment.a

        // RGB 值
        let mixR = procedureBlock?(
            baseCompoment.r / 255.0,
            topCompoment.r / 255.0)
            ?? (baseCompoment.r) / 255.0

        let mixG = procedureBlock?(
            baseCompoment.g / 255.0,
            topCompoment.g / 255.0)
            ?? (baseCompoment.g) / 255.0

        let mixB = procedureBlock?(
            baseCompoment.b / 255.0,
            topCompoment.b / 255.0)
            ?? baseCompoment.b / 255.0


        return UIColor.init(red:   fitIn(mixR),
                            green: fitIn(mixG),
                            blue:  fitIn(mixB),
                            alpha: mixAlpha)
    }

    // 防止越界
    func fitIn(_ value: CGFloat, ceil: CGFloat = 255) -> CGFloat { return max(min(value,ceil),0) }
    func fitIn(_ value: Double, ceil: CGFloat = 255) -> CGFloat { return fitIn(CGFloat(value), ceil: ceil) }

    // 返回 RBGA
    func rgbaTuple() -> (r: CGFloat, g: CGFloat, b: CGFloat,a: CGFloat) {
        var r: CGFloat = 0
        var g: CGFloat = 0
        var b: CGFloat = 0
        var a: CGFloat = 0
        self.getRed(&r, green: &g, blue: &b, alpha: &a)
        r = r * 255
        g = g * 255
        b = b * 255

        return ((r),(g),(b),a)
    }
}
Spelling answered 7/12, 2014 at 12:46 Comment(1)
how to apply cgclor like this : key.layer.borderColor = UIColor.blendOverlay(.white) as! CGColorJacqui
Y
3

For my project I needed to simulate a dynamic theme color with partial opacity over a white background, without any actual transparency. I was able to achieve this with the following UIColor extension (based on this answer):

static func simulatingAlpha(_ alpha: CGFloat, for color1: UIColor, over color2: UIColor) -> UIColor {

    let whiteComponents: [CGFloat] = [1.0, 1.0, 1.0, 1.0] //UIColor.white.cgColor.components only returns [1.0, 1.0]

    var rgba1: [CGFloat] = whiteComponents //set a valid default
    var rgba2: [CGFloat] = whiteComponents

    if let components = color1.cgColor.components, components.count > 2 {
        rgba1 = components
    }

    if let components = color2.cgColor.components, components.count > 2  {
        rgba2 = components
    }

    let r1: CGFloat = rgba1[0]
    let g1: CGFloat = rgba1[1]
    let b1: CGFloat = rgba1[2]

    let r2: CGFloat = rgba2[0]
    let g2: CGFloat = rgba2[1]
    let b2: CGFloat = rgba2[2]

    let r3 = ((1 - alpha) * r2) + (r1 * alpha)
    let g3 = ((1 - alpha) * g2) + (g1 * alpha)
    let b3 = ((1 - alpha) * b2) + (b1 * alpha)

    print("Simulated RGB: \(Int(r3 * 255)), \(Int(g3 * 255)), \(Int(b3 * 255))")

    let newComponents: [CGFloat] = [r3, g3, b3, 1.0]
    let space = CGColorSpace(name:CGColorSpace.sRGB)!
    guard let cgColor3 = CGColor(colorSpace: space, components: newComponents) else {
        print("Failed to create new CGColor in default color space")
        return color1
    }

    return UIColor(cgColor: cgColor3)
}
Yeung answered 2/8, 2017 at 1:2 Comment(0)
T
1

My version (it blends as many colors as you'd like), Swift 4+:

func blend(colors: [UIColor]) -> UIColor {
    let numberOfColors = CGFloat(colors.count)
    var (red, green, blue, alpha) = (CGFloat(0), CGFloat(0), CGFloat(0), CGFloat(0))

    let componentsSum = colors.reduce((red: CGFloat(0), green: CGFloat(0), blue: CGFloat(0), alpha: CGFloat())) { temp, color in
        color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
        return (temp.red+red, temp.green + green, temp.blue + blue, temp.alpha+alpha)
    }
    return UIColor(red: componentsSum.red / numberOfColors,
                       green: componentsSum.green / numberOfColors,
                       blue: componentsSum.blue / numberOfColors,
                       alpha: componentsSum.alpha / numberOfColors)
}
Tremolant answered 30/7, 2017 at 7:30 Comment(5)
This fails if you have colors that are not from the RGB color space, such as UIColor.lightGray. You need to extract the components using UIColor.getRed(_:green:blue:alpha:) if you want to blend e.g. UIColor.white with UIColor.blueDiamagnetic
Hi Noyer282, would you please edit/update my answer with your suggestion? Thanks :)Tremolant
Did that. You can test it by trying e.g. let blended = blend(colors: [UIColor.white, UIColor.red]). Your original code would crash, but now it should work.Diamagnetic
This is not good; it doesn't properly take into account the alpha of each color. For example, a the components of black at 5% don't contribute 5% of the color, they contribute 100% and the final alpha would be e.g. 52.5%. This just averages each component and the alpha separately.Nobility
@GrahamPerks yes, I tend to agree with what you say. It's really up to the developer to decide what blending means for your project. Both ways, the answer above and your suggestion, are correct in my opinion :)Tremolant
S
0

All Platforms

✅ Both UIKit & AppKit: 'aka' UIColor & NSColor

With this method, you can mix colors in any platform with any color.

public extension NativeColor {
    func mix(with target: NativeColor, amount: CGFloat) -> Self {
        var r1: CGFloat = 0, g1: CGFloat = 0, b1: CGFloat = 0, a1: CGFloat = 0
        var r2: CGFloat = 0, g2: CGFloat = 0, b2: CGFloat = 0, a2: CGFloat = 0

        getRed(&r1, green: &g1, blue: &b1, alpha: &a1)
        target.getRed(&r2, green: &g2, blue: &b2, alpha: &a2)

        return Self(
            red: r1 * (1.0 - amount) + r2 * amount,
            green: g1 * (1.0 - amount) + g2 * amount,
            blue: b1 * (1.0 - amount) + b2 * amount,
            alpha: a1
        )
    }
}

Don't forget to define what is the NativeColor in each environment:

#if canImport(UIKit)
public typealias NativeColor = UIColor
#elseif canImport(AppKit)
public typealias NativeColor = NSColor
#endif

SwiftUI: Color

iOS 18 / macOS 15

Color.red.mix(with: .blue, by: 0.5)
Implementation for older platforms - iOS 14 / macOS 10.16

⚠️ Requires previous extension

public extension Color {
    func mix(with target: Color, by amount: CGFloat) -> Color {
        Color(NativeColor(self).mix(with: NativeColor(target), amount: amount))
    }
}
Sternwheeler answered 10/7, 2023 at 12:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.