36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/Sources/SwiftUI/Color.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * DynamicColor
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | import SwiftUI
28 |
29 | @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
30 | public extension Color {
31 | // MARK: - Manipulating Hexa-decimal Values and Strings
32 |
33 | /**
34 | Creates a color from an hex string (e.g. "#3498db"). The RGBA string are also supported (e.g. "#3498dbff").
35 |
36 | If the given hex string is invalid the initialiser will create a black color.
37 |
38 | - parameter hexString: A hexa-decimal color string representation.
39 | */
40 | init(hexString: String) {
41 | let hexString = hexString.trimmingCharacters(in: .whitespacesAndNewlines)
42 | let scanner = Scanner(string: hexString)
43 | scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#")
44 |
45 | var color: UInt64 = 0
46 |
47 | if scanner.scanHexInt64(&color) {
48 | self.init(hex: color, useOpacity: hexString.count > 7)
49 | }
50 | else {
51 | self.init(hex: 0x000000)
52 | }
53 | }
54 |
55 | /**
56 | Creates a color from an hex integer (e.g. 0x3498db).
57 |
58 | - parameter hex: A hexa-decimal UInt64 that represents a color.
59 | - parameter opacityChannel: If true the given hex-decimal UInt64 includes the opacity channel (e.g. 0xFF0000FF).
60 | */
61 | init(hex: UInt64, useOpacity opacityChannel: Bool = false) {
62 | let mask = UInt64(0xFF)
63 | let cappedHex = !opacityChannel && hex > 0xffffff ? 0xffffff : hex
64 |
65 | let r = cappedHex >> (opacityChannel ? 24 : 16) & mask
66 | let g = cappedHex >> (opacityChannel ? 16 : 8) & mask
67 | let b = cappedHex >> (opacityChannel ? 8 : 0) & mask
68 | let o = opacityChannel ? cappedHex & mask : 255
69 |
70 | let red = Double(r) / 255.0
71 | let green = Double(g) / 255.0
72 | let blue = Double(b) / 255.0
73 | let opacity = Double(o) / 255.0
74 |
75 | self.init(red: red, green: green, blue: blue, opacity: opacity)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Tests/DynamicColor+HSLTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * DynamicColor
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | import XCTest
28 | @testable import DynamicColor
29 |
30 | class DynamicColorHSLTests: XCTestCase {
31 | func testInitWithHSLComponents() {
32 | let black1 = DynamicColor(hue: 0, saturation: 0, lightness: 0)
33 | let black2 = DynamicColor(hue: 1, saturation: 1, lightness: 0)
34 | let white1 = DynamicColor(hue: 0, saturation: 0, lightness: 1)
35 | let white2 = DynamicColor(hue: 1, saturation: 1, lightness: 1)
36 |
37 | let red = DynamicColor(hue: 0, saturation: 1, lightness: 0.5)
38 | let green = DynamicColor(hue: 120, saturation: 1, lightness: 0.5)
39 | let blue = DynamicColor(hue: 240, saturation: 1, lightness: 0.5)
40 |
41 | let custom = DynamicColor(hue: 6, saturation: 0.781, lightness: 0.571)
42 |
43 | XCTAssert(black1.toHex() == 0, "Color should be black")
44 | XCTAssert(black2.toHex() == 0, "Color should be black")
45 | XCTAssert(white1.toHex() == 0xffffff, "Color should be white")
46 | XCTAssert(white2.toHex() == 0xffffff, "Color should be white")
47 |
48 | XCTAssert(red.isEqual(toHexString: DynamicColor.red.toHexString()), "Color should be red")
49 | XCTAssert(green.isEqual(toHexString: DynamicColor.green.toHexString()), "Color should be green")
50 | XCTAssert(blue.isEqual(toHexString: DynamicColor.blue.toHexString()), "Color should be blue")
51 | XCTAssert(custom.isEqual(toHexString: "#e74d3c"), "Color should be equal to #e74d3c")
52 | }
53 |
54 | func testToHSLComponents() {
55 | let customColor = DynamicColor(hue: 6, saturation: 0.781, lightness: 0.571)
56 | let hsl = customColor.toHSLComponents()
57 |
58 | XCTAssertEqual(hsl.h, 6, accuracy: TestsAcceptedAccuracy, "Color hue component should be equal to 6° (not \(hsl.h))")
59 | XCTAssertEqual(hsl.s, 0.781, accuracy: TestsAcceptedAccuracy, "Saturation component should be equal to 0.781 (not \(hsl.s))")
60 | XCTAssertEqual(hsl.l, 0.571, accuracy: TestsAcceptedAccuracy, "Lightness component should be equal to 0.571 (not \(hsl.l))")
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Examples/DynamicColorExample.xcodeproj/xcshareddata/xcschemes/DynamicColorTvOs.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
45 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/Examples/DynamicColorExample.xcodeproj/xcshareddata/xcschemes/DynamicColorMacOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
45 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/Examples/DynamicColorExample.xcodeproj/xcshareddata/xcschemes/DynamicColorWatchOs.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
45 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/Examples/iOSExample/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // DynamicColorExample
4 | //
5 | // Created by Yannick LORIOT on 01/06/15.
6 | // Copyright (c) 2015 Yannick LORIOT. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import DynamicColor
11 |
12 | class ViewController: UIViewController {
13 | private let colorCellIdentifier = "ColorCell"
14 |
15 | @IBOutlet weak var colorCollectionView: UICollectionView!
16 |
17 | private lazy var colors: [(String, UIColor)] = {
18 | let mainColor = UIColor(hexString: "#c0392b")
19 |
20 | return [
21 | ("Original", mainColor),
22 | ("Lighter", mainColor.lighter()),
23 | ("Darkered", mainColor.darkened()),
24 | ("Saturated", mainColor.saturated()),
25 | ("Desaturated", mainColor.desaturated()),
26 | ("Grayscaled", mainColor.grayscaled()),
27 | ("Adjusted", mainColor.adjustedHue(amount: 45)),
28 | ("Complemented", mainColor.complemented()),
29 | ("Inverted", mainColor.inverted()),
30 | ("Mix Blue", mainColor.mixed(withColor: .blue)),
31 | ("Mix Green", mainColor.mixed(withColor: .green)),
32 | ("Mix Yellow", mainColor.mixed(withColor: .yellow)),
33 | ("Tinted", mainColor.tinted()),
34 | ("Shaded", mainColor.shaded())
35 | ]
36 | }()
37 |
38 | private lazy var gradients: [(String, UIColor)] = {
39 | return [UIColor(hex: 0x3498db), UIColor(hex: 0xe74c3c), UIColor(hex: 0xf1c40f)].gradient.colorPalette(amount: 15).map { ($0.toHexString(), $0) }
40 | }()
41 |
42 | func collection(inSection section: Int) -> [(String, UIColor)] {
43 | return section == 0 ? colors : gradients
44 | }
45 | }
46 |
47 | extension ViewController: UICollectionViewDataSource {
48 | func numberOfSections(in collectionView: UICollectionView) -> Int {
49 | return 2
50 | }
51 |
52 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
53 | return collection(inSection: section).count
54 | }
55 |
56 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
57 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: colorCellIdentifier, for: indexPath)
58 |
59 | self.collectionView(collectionView, willDisplay: cell, forItemAt: indexPath)
60 |
61 | return cell
62 | }
63 |
64 | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
65 | // swiftlint:disable force_cast
66 | let supplementaryView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "HeaderView", for: indexPath) as! HeaderView
67 | supplementaryView.titleLabel.text = indexPath.section == 0 ? "Colors" : "Gradients"
68 |
69 | return supplementaryView
70 | }
71 | }
72 |
73 |
74 | extension ViewController: UICollectionViewDelegate {
75 | func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
76 | guard let cell = cell as? ColorCellView else { return }
77 |
78 | let (title, color) = collection(inSection: indexPath.section)[indexPath.row]
79 |
80 | cell.titleLabel?.text = title
81 | cell.colorView?.backgroundColor = color
82 |
83 | cell.layoutColorView()
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Examples/DynamicColorExample.xcodeproj/xcshareddata/xcschemes/DynamicColor.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
55 |
61 |
62 |
63 |
64 |
65 |
66 |
72 |
73 |
79 |
80 |
81 |
82 |
84 |
85 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/Examples/iOSExample/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
20 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Sources/Core/DynamicColor+Lab.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * DynamicColor
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | #if os(iOS) || os(tvOS) || os(watchOS)
28 | import UIKit
29 | #elseif os(OSX)
30 | import AppKit
31 | #endif
32 |
33 | // MARK: CIE L*a*b* Color Space
34 |
35 | public extension DynamicColor {
36 | /**
37 | Initializes and returns a color object using CIE XYZ color space component values with an observer at 2° and a D65 illuminant.
38 |
39 | Notes that values out of range are clipped.
40 |
41 | - parameter L: The lightness, specified as a value from 0 to 100.0.
42 | - parameter a: The red-green axis, specified as a value from -128.0 to 127.0.
43 | - parameter b: The yellow-blue axis, specified as a value from -128.0 to 127.0.
44 | - parameter alpha: The opacity value of the color object, specified as a value from 0.0 to 1.0. Default to 1.0.
45 | */
46 | convenience init(L: CGFloat, a: CGFloat, b: CGFloat, alpha: CGFloat = 1) {
47 | let clippedL = clip(L, 0.0, 100.0)
48 | let clippedA = clip(a, -128.0, 127.0)
49 | let clippedB = clip(b, -128.0, 127.0)
50 |
51 | let normalized = { (c: CGFloat) -> CGFloat in
52 | pow(c, 3) > 0.008856 ? pow(c, 3) : (c - (16 / 116)) / 7.787
53 | }
54 |
55 | let preY = (clippedL + 16.0) / 116.0
56 | let preX = (clippedA / 500.0) + preY
57 | let preZ = preY - (clippedB / 200.0)
58 |
59 | let X = 95.05 * normalized(preX)
60 | let Y = 100.0 * normalized(preY)
61 | let Z = 108.9 * normalized(preZ)
62 |
63 | self.init(X: X, Y: Y, Z: Z, alpha: alpha)
64 | }
65 |
66 | // MARK: - Getting the L*a*b* Components
67 |
68 | /**
69 | Returns the Lab (lightness, red-green axis, yellow-blue axis) components.
70 | It is based on the CIE XYZ color space with an observer at 2° and a D65 illuminant.
71 |
72 | Notes that L values are between 0 to 100.0, a values are between -128 to 127.0 and b values are between -128 to 127.0.
73 |
74 | - returns: The L*a*b* components as a tuple (L, a, b).
75 | */
76 | final func toLabComponents() -> (L: CGFloat, a: CGFloat, b: CGFloat) {
77 | let normalized = { (c: CGFloat) -> CGFloat in
78 | c > 0.008856 ? pow(c, 1.0 / 3.0) : (7.787 * c) + (16.0 / 116.0)
79 | }
80 |
81 | let xyz = toXYZComponents()
82 | let normalizedX = normalized(xyz.X / 95.05)
83 | let normalizedY = normalized(xyz.Y / 100.0)
84 | let normalizedZ = normalized(xyz.Z / 108.9)
85 |
86 | let L = roundDecimal((116.0 * normalizedY) - 16.0, precision: 1000)
87 | let a = roundDecimal(500.0 * (normalizedX - normalizedY), precision: 1000)
88 | let b = roundDecimal(200.0 * (normalizedY - normalizedZ), precision: 1000)
89 |
90 | return (L: L, a: a, b: b)
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/Sources/Core/DynamicColor+XYZ.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * DynamicColor
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | #if os(iOS) || os(tvOS) || os(watchOS)
28 | import UIKit
29 | #elseif os(OSX)
30 | import AppKit
31 | #endif
32 |
33 | // MARK: CIE XYZ Color Space
34 |
35 | public extension DynamicColor {
36 | /**
37 | Initializes and returns a color object using CIE XYZ color space component values with an observer at 2° and a D65 illuminant.
38 |
39 | Notes that values out of range are clipped.
40 |
41 | - parameter X: The mix of cone response curves, specified as a value from 0 to 95.05.
42 | - parameter Y: The luminance, specified as a value from 0 to 100.0.
43 | - parameter Z: The quasi-equal to blue stimulation, specified as a value from 0 to 108.9.
44 | - parameter alpha: The opacity value of the color object, specified as a value from 0.0 to 1.0. Default to 1.0.
45 | */
46 | convenience init(X: CGFloat, Y: CGFloat, Z: CGFloat, alpha: CGFloat = 1) {
47 | let clippedX = clip(X, 0.0, 95.05) / 100.0
48 | let clippedY = clip(Y, 0.0, 100) / 100.0
49 | let clippedZ = clip(Z, 0.0, 108.9) / 100.0
50 |
51 | let toRGB = { (c: CGFloat) -> CGFloat in
52 | let rgb = c > 0.0031308 ? 1.055 * pow(c, 1.0 / 2.4) - 0.055 : c * 12.92
53 |
54 | return abs(roundDecimal(rgb, precision: 1000.0))
55 | }
56 |
57 | let red = toRGB((clippedX * 3.2406) + (clippedY * -1.5372) + (clippedZ * -0.4986))
58 | let green = toRGB((clippedX * -0.9689) + (clippedY * 1.8758) + (clippedZ * 0.0415))
59 | let blue = toRGB((clippedX * 0.0557) + (clippedY * -0.2040) + (clippedZ * 1.0570))
60 |
61 | self.init(red: red, green: green, blue: blue, alpha: alpha)
62 | }
63 |
64 | // MARK: - Getting the XYZ Components
65 |
66 | /**
67 | Returns the XYZ (mix of cone response curves, luminance, quasi-equal to blue stimulation) components with an observer at 2° and a D65 illuminant.
68 |
69 | Notes that X values are between 0 to 95.05, Y values are between 0 to 100.0 and Z values are between 0 to 108.9.
70 |
71 | - returns: The XYZ components as a tuple (X, Y, Z).
72 | */
73 | final func toXYZComponents() -> (X: CGFloat, Y: CGFloat, Z: CGFloat) {
74 | let toSRGB = { (c: CGFloat) -> CGFloat in
75 | c > 0.04045 ? pow((c + 0.055) / 1.055, 2.4) : c / 12.92
76 | }
77 |
78 | let rgba = toRGBAComponents()
79 | let red = toSRGB(rgba.r)
80 | let green = toSRGB(rgba.g)
81 | let blue = toSRGB(rgba.b)
82 |
83 | let X = roundDecimal(((red * 0.4124) + (green * 0.3576) + (blue * 0.1805)) * 100.0, precision: 1000.0)
84 | let Y = roundDecimal(((red * 0.2126) + (green * 0.7152) + (blue * 0.0722)) * 100.0, precision: 1000.0)
85 | let Z = roundDecimal(((red * 0.0193) + (green * 0.1192) + (blue * 0.9505)) * 100.0, precision: 1000.0)
86 |
87 | return (X: X, Y: Y, Z: Z)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Tests/DynamicColorArrayTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * DynamicColor
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | import XCTest
28 | @testable import DynamicColor
29 |
30 | class DynamicColorArrayTests: XCTestCase {
31 | func testGradientProperty() {
32 | let colors: [DynamicColor] = []
33 |
34 | XCTAssertNotNil(colors.gradient)
35 | }
36 |
37 | func testColors() {
38 | let colors = [#colorLiteral(red: 1, green: 0, blue: 0, alpha: 1), #colorLiteral(red: 0, green: 1, blue: 0, alpha: 1), #colorLiteral(red: 0, green: 0, blue: 1, alpha: 1)].gradient.colorPalette(amount: 5)
39 |
40 | XCTAssert(colors[0].isEqual(#colorLiteral(red: 1, green: 0, blue: 0, alpha: 1)), "Should be red")
41 | XCTAssert(colors[1].isEqual(DynamicColor(red: 0.5, green: 0.5, blue: 0, alpha: 1)), "Should be kaki")
42 | XCTAssert(colors[2].isEqual(#colorLiteral(red: 0, green: 1, blue: 0, alpha: 1)), "Should be green")
43 | XCTAssert(colors[3].isEqual(DynamicColor(red: 0, green: 0.5, blue: 0.5, alpha: 1)), "Should be purple")
44 | XCTAssert(colors[4].isEqual(#colorLiteral(red: 0, green: 0, blue: 1, alpha: 1)), "Should be blue")
45 | }
46 |
47 | func testColorAt() {
48 | let emptyColors = Array().gradient
49 |
50 | XCTAssert(emptyColors.pickColorAt(scale: 0.25).isEqual(toHex: 0x00000), "Should be black")
51 |
52 | let oneColor = [#colorLiteral(red: 1, green: 0, blue: 0, alpha: 1)].gradient
53 |
54 | XCTAssert(oneColor.pickColorAt(scale: 0.75).isEqual(#colorLiteral(red: 1, green: 0, blue: 0, alpha: 1)), "Should be red")
55 |
56 | let primaryColors = [#colorLiteral(red: 1, green: 0, blue: 0, alpha: 1), #colorLiteral(red: 0, green: 1, blue: 0, alpha: 1), #colorLiteral(red: 0, green: 0, blue: 1, alpha: 1)].gradient
57 |
58 | let red = primaryColors.pickColorAt(scale: 0)
59 | XCTAssert(red.isEqual(#colorLiteral(red: 1, green: 0, blue: 0, alpha: 1)), "Should be red")
60 |
61 | let clippedRed = primaryColors.pickColorAt(scale: -7.9)
62 | XCTAssert(clippedRed.isEqual(#colorLiteral(red: 1, green: 0, blue: 0, alpha: 1)), "Should be red")
63 |
64 | let blue = primaryColors.pickColorAt(scale: 1)
65 | XCTAssert(blue.isEqual(#colorLiteral(red: 0, green: 0, blue: 1, alpha: 1)), "Should be blue")
66 |
67 | let clippedBlue = primaryColors.pickColorAt(scale: 34)
68 | XCTAssert(clippedBlue.isEqual(#colorLiteral(red: 0, green: 0, blue: 1, alpha: 1)), "Should be blue")
69 |
70 | let kaki = primaryColors.pickColorAt(scale: 0.25)
71 | XCTAssert(kaki.toRGBAComponents() == (r: 0.5, g: 0.5, b: 0, a: 1), "Should be kaki (not \(kaki))")
72 |
73 | let green = primaryColors.pickColorAt(scale: 0.65)
74 | XCTAssert(green.toRGBAComponents().r == 0, "Should be green (not \(green.toRGBAComponents()))")
75 | XCTAssert(green.toRGBAComponents().g == 0.7, "Should be green (not \(green.toRGBAComponents()))")
76 | XCTAssert(round(green.toRGBAComponents().b * 10) == 3, "Should be green (not \(green.toRGBAComponents().b))")
77 | XCTAssert(green.toRGBAComponents().a == 1, "Should be green (not \(green.toRGBAComponents()))")
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Sources/Core/DynamicGradient.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * DynamicColor
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | #if os(iOS) || os(tvOS) || os(watchOS)
28 | import UIKit
29 | #elseif os(OSX)
30 | import AppKit
31 | #endif
32 |
33 | /**
34 | Object representing a gradient object. It allows you to manipulate colors inside different gradients and color spaces.
35 | */
36 | final public class DynamicGradient {
37 | let colors: [DynamicColor]
38 |
39 | /**
40 | Initializes and creates a gradient from a color array.
41 |
42 | - Parameter colors: An array of colors.
43 | */
44 | public init(colors: [DynamicColor]) {
45 | self.colors = colors
46 | }
47 |
48 | /**
49 | Returns the color palette of `amount` elements by grabbing equidistant colors.
50 |
51 | - Parameter amount: An amount of colors to return. 2 by default.
52 | - Parameter colorspace: The color space used to mix the colors. By default it uses the RBG color space.
53 | - Returns: An array of DynamicColor objects with equi-distant space in the gradient.
54 | */
55 | public func colorPalette(amount: UInt = 2, inColorSpace colorspace: DynamicColorSpace = .rgb) -> [DynamicColor] {
56 | guard amount > 0 && colors.count > 0 else {
57 | return []
58 | }
59 |
60 | guard colors.count > 1 else {
61 | return (0 ..< amount).map { _ in colors[0] }
62 | }
63 |
64 | let increment = 1.0 / CGFloat(amount - 1)
65 |
66 | return (0 ..< amount).map { pickColorAt(scale: CGFloat($0) * increment, inColorSpace: colorspace) }
67 | }
68 |
69 | /**
70 | Picks up and returns the color at the given scale by interpolating the colors.
71 |
72 | For example, given this color array `[red, green, blue]` and a scale of `0.25` you will get a kaki color.
73 |
74 | - Parameter scale: A float value between 0.0 and 1.0.
75 | - Parameter colorspace: The color space used to mix the colors. By default it uses the RBG color space.
76 | - Returns: A DynamicColor object corresponding to the color at the given scale.
77 | */
78 | public func pickColorAt(scale: CGFloat, inColorSpace colorspace: DynamicColorSpace = .rgb) -> DynamicColor {
79 | guard colors.count > 1 else {
80 | return colors.first ?? .black
81 | }
82 |
83 | let clippedScale = clip(scale, 0.0, 1.0)
84 | let positions = (0 ..< colors.count).map { CGFloat($0) / CGFloat(colors.count - 1) }
85 |
86 | var color: DynamicColor = .black
87 |
88 | for (index, position) in positions.enumerated() {
89 | guard clippedScale <= position else { continue }
90 |
91 | guard clippedScale != 0.0 && clippedScale != 1.0 else {
92 | return colors[index]
93 | }
94 |
95 | let previousPosition = positions[index - 1]
96 | let weight = (clippedScale - previousPosition) / (position - previousPosition)
97 |
98 | color = colors[index - 1].mixed(withColor: colors[index], weight: weight, inColorSpace: colorspace)
99 |
100 | break
101 | }
102 |
103 | return color
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/Tests/DynamicColor+XYZTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * DynamicColor
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | import XCTest
28 | @testable import DynamicColor
29 |
30 | class DynamicColorXYZTests: XCTestCase {
31 | func testInitWithXYZComponents() {
32 | let whiteColor = DynamicColor(X: 95.05, Y: 100, Z: 108.9).toRGBAComponents()
33 | XCTAssert(whiteColor.r == 1, "Red component should be equal to 1 (not \(whiteColor.r))")
34 | XCTAssert(whiteColor.g == 1, "Green component should be equal to 1 (not \(whiteColor.g))")
35 | XCTAssert(whiteColor.b == 1, "Blue component should be equal to 1 (not \(whiteColor.b))")
36 |
37 | let blackColor = DynamicColor(X: 0, Y: 0, Z: 0).toRGBAComponents()
38 | XCTAssert(blackColor.r == 0, "Red component should be equal to 0 (not \(blackColor.r))")
39 | XCTAssert(blackColor.g == 0, "Green component should be equal to 0 (not \(blackColor.g))")
40 | XCTAssert(blackColor.b == 0, "Blue component should be equal to 0 (not \(blackColor.b))")
41 |
42 | let blueColor = DynamicColor(X: 18.05, Y: 7.22, Z: 95.05).toRGBAComponents()
43 | XCTAssert(blueColor.r == 0, "Red component should be equal to 0 (not \(blueColor.r))")
44 | XCTAssert(blueColor.g == 0, "Green component should be equal to 0 (not \(blueColor.g))")
45 | XCTAssert(blueColor.b == 1, "Blue component should be equal to 1 (not \(blueColor.b))")
46 |
47 | let customColor = DynamicColor(X: 37.177, Y: 46.108, Z: 10.189).toRGBAComponents()
48 | XCTAssert(customColor.r == 0.698, "Red component should be equal to 0.698 (not \(customColor.r))")
49 | XCTAssert(customColor.g == 0.741, "Green component should be equal to 0.741 (not \(customColor.g))")
50 | XCTAssert(customColor.b == 0.204, "Blue component should be equal to 0.204 (not \(customColor.b))")
51 | }
52 |
53 | func testToHSLComponents() {
54 | let whiteXYZ = DynamicColor.white.toXYZComponents()
55 | XCTAssert(whiteXYZ.X == 95.05, "X component should be equal to 95.05 (not \(whiteXYZ.X))")
56 | XCTAssert(whiteXYZ.Y == 100, "Y component should be equal to 100 (not \(whiteXYZ.Y))")
57 | XCTAssert(whiteXYZ.Z == 108.9, "Z component should be equal to 108.9 (not \(whiteXYZ.Z))")
58 |
59 | let blackXYZ = DynamicColor.black.toXYZComponents()
60 | XCTAssert(blackXYZ.X == 0, "X component should be equal to 0 (not \(blackXYZ.X))")
61 | XCTAssert(blackXYZ.Y == 0, "Y component should be equal to 0 (not \(blackXYZ.Y))")
62 | XCTAssert(blackXYZ.Z == 0, "Z component should be equal to 0 (not \(blackXYZ.Z))")
63 |
64 | let blueXYZ = DynamicColor.blue.toXYZComponents()
65 | XCTAssert(blueXYZ.X == 18.05, "X component should be equal to 18.05 (not \(blueXYZ.X))")
66 | XCTAssert(blueXYZ.Y == 7.22, "Y component should be equal to 7.22 (not \(blueXYZ.Y))")
67 | XCTAssert(blueXYZ.Z == 95.05, "Z component should be equal to 95.05 (not \(blueXYZ.Z))")
68 |
69 | let customXYZ = DynamicColor(red: 0.69804, green: 0.74118, blue: 0.20392, alpha: 1).toXYZComponents()
70 | XCTAssert(customXYZ.X == 37.178, "X component should be equal to 37.178 (not \(customXYZ.X))")
71 | XCTAssert(customXYZ.Y == 46.109, "Y component should be equal to 46.109 (not \(customXYZ.Y))")
72 | XCTAssert(customXYZ.Z == 10.189, "Z component should be equal to 10.189 (not \(customXYZ.Z))")
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Tests/DynamicColor+RGBATests.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * DynamicColor
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | import XCTest
28 | @testable import DynamicColor
29 |
30 | class DynamicColorRGBATests: XCTestCase {
31 | func testInit() {
32 | let customInitColor = DynamicColor(r: 58.65, g: 117.3, b: 81.6)
33 | let customColor = DynamicColor(red: 0.23, green: 0.46, blue: 0.32, alpha: 1)
34 |
35 | XCTAssertEqual(customInitColor.toHex(), customColor.toHex())
36 | }
37 |
38 | func testToRGBAComponents() {
39 | let rgbaColor = DynamicColor(red: 0.23, green: 0.46, blue: 0.32, alpha: 1)
40 | let rgba1 = rgbaColor.toRGBAComponents()
41 | XCTAssertEqual(rgba1.r, 0.23)
42 | XCTAssertEqual(rgba1.g, 0.46)
43 | XCTAssertEqual(rgba1.b, 0.32)
44 | XCTAssertEqual(rgba1.a, 1.00)
45 |
46 | let grayscaleColor = DynamicColor(white: 0.42, alpha: 1)
47 | let rgba2 = grayscaleColor.toRGBAComponents()
48 | XCTAssertEqual(rgba2.r, 0.42, accuracy: 0.001)
49 | XCTAssertEqual(rgba2.g, 0.42, accuracy: 0.001)
50 | XCTAssertEqual(rgba2.b, 0.42, accuracy: 0.001)
51 | XCTAssertEqual(rgba2.a, 1.00, accuracy: 0.001)
52 | }
53 |
54 | func testRedComponent() {
55 | let customColor = DynamicColor(red: 0.23, green: 0.46, blue: 0.32, alpha: 1)
56 |
57 | let redComponent = customColor.redComponent
58 |
59 | XCTAssert(redComponent == 0.23, "Color red component should be equal to 0.23")
60 | }
61 |
62 | func testGreenComponent() {
63 | let customColor = DynamicColor(red: 0.23, green: 0.46, blue: 0.32, alpha: 1)
64 |
65 | let greenComponent = customColor.greenComponent
66 |
67 | XCTAssert(greenComponent == 0.46, "Color green component should be equal to 0.46")
68 | }
69 |
70 | func testBlueComponent() {
71 | let customColor = DynamicColor(red: 0.23, green: 0.46, blue: 0.32, alpha: 1)
72 |
73 | let blueComponent = customColor.blueComponent
74 |
75 | XCTAssert(blueComponent == 0.32, "Color blue component should be equal to 0.32")
76 | }
77 |
78 | func testAlphaComponent() {
79 | let customColor = DynamicColor(red: 0.23, green: 0.46, blue: 0.32, alpha: 0.8)
80 |
81 | let alphaComponent = customColor.alphaComponent
82 |
83 | XCTAssert(alphaComponent == 0.8, "Color alpha component should be equal to 0.8")
84 | }
85 |
86 | func testAdjustedAlphaColor() {
87 | let customColor = DynamicColor(red: 0.23, green: 0.46, blue: 0.32, alpha: 1)
88 |
89 | XCTAssert(customColor.alphaComponent == 1, "Color alpha component should be equal to 1")
90 |
91 | let adjustedAlpha1 = customColor.adjustedAlpha(amount: -0.5)
92 |
93 | XCTAssert(adjustedAlpha1.alphaComponent == 0.5, "Color alpha component should be equal to 0.5")
94 |
95 | let adjustedAlpha2 = adjustedAlpha1.adjustedAlpha(amount: 0.2)
96 |
97 | XCTAssert(adjustedAlpha2.alphaComponent == 0.7, "Color alpha component should be equal to 0.7")
98 |
99 | let adjustedAlpha3 = adjustedAlpha2.adjustedAlpha(amount: -1)
100 |
101 | XCTAssert(adjustedAlpha3.alphaComponent == 0, "Color alpha component should be equal to 0")
102 |
103 | let adjustedAlpha4 = adjustedAlpha3.adjustedAlpha(amount: 23)
104 |
105 | XCTAssert(adjustedAlpha4.alphaComponent == 1, "Color alpha component should be equal to 1")
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/Sources/Core/DynamicColor+RGBA.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * DynamicColor
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | #if os(iOS) || os(tvOS) || os(watchOS)
28 | import UIKit
29 | #elseif os(OSX)
30 | import AppKit
31 | #endif
32 |
33 | // MARK: RGBA Color Space
34 |
35 | public extension DynamicColor {
36 | /**
37 | Initializes and returns a color object using the specified opacity and RGB component values.
38 |
39 | Notes that values out of range are clipped.
40 |
41 | - Parameter r: The red component of the color object, specified as a value from 0.0 to 255.0.
42 | - Parameter g: The green component of the color object, specified as a value from 0.0 to 255.0.
43 | - Parameter b: The blue component of the color object, specified as a value from 0.0 to 255.0.
44 | - Parameter a: The opacity value of the color object, specified as a value from 0.0 to 255.0. The default value is 255.
45 | */
46 | convenience init(r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat = 255) {
47 | self.init(red: clip(r, 0, 255) / 255, green: clip(g, 0, 255) / 255, blue: clip(b, 0, 255) / 255, alpha: clip(a, 0, 255) / 255)
48 | }
49 |
50 | // MARK: - Getting the RGBA Components
51 |
52 | /**
53 | Returns the RGBA (red, green, blue, alpha) components.
54 |
55 | - returns: The RGBA components as a tuple (r, g, b, a).
56 | */
57 | final func toRGBAComponents() -> (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {
58 | var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
59 |
60 | #if os(iOS) || os(tvOS) || os(watchOS)
61 | getRed(&r, green: &g, blue: &b, alpha: &a)
62 |
63 | return (r, g, b, a)
64 | #elseif os(OSX)
65 | guard let rgbaColor = self.usingColorSpace(.deviceRGB) else {
66 | fatalError("Could not convert color to RGBA.")
67 | }
68 |
69 | rgbaColor.getRed(&r, green: &g, blue: &b, alpha: &a)
70 |
71 | return (r, g, b, a)
72 | #endif
73 | }
74 |
75 | #if os(iOS) || os(tvOS) || os(watchOS)
76 | /**
77 | The red component as CGFloat between 0.0 to 1.0.
78 | */
79 | final var redComponent: CGFloat {
80 | return toRGBAComponents().r
81 | }
82 |
83 | /**
84 | The green component as CGFloat between 0.0 to 1.0.
85 | */
86 | final var greenComponent: CGFloat {
87 | return toRGBAComponents().g
88 | }
89 |
90 | /**
91 | The blue component as CGFloat between 0.0 to 1.0.
92 | */
93 | final var blueComponent: CGFloat {
94 | return toRGBAComponents().b
95 | }
96 |
97 | /**
98 | The alpha component as CGFloat between 0.0 to 1.0.
99 | */
100 | final var alphaComponent: CGFloat {
101 | return toRGBAComponents().a
102 | }
103 | #endif
104 |
105 | // MARK: - Setting the RGBA Components
106 |
107 | /**
108 | Creates and returns a color object with the alpha increased by the given amount.
109 |
110 | - parameter amount: CGFloat between 0.0 and 1.0.
111 | - returns: A color object with its alpha channel modified.
112 | */
113 | final func adjustedAlpha(amount: CGFloat) -> DynamicColor {
114 | let components = toRGBAComponents()
115 | let normalizedAlpha = clip(components.a + amount, 0.0, 1.0)
116 |
117 | return DynamicColor(red: components.r, green: components.g, blue: components.b, alpha: normalizedAlpha)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Tests/DynamicColor+HSBTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * DynamicColor
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | import XCTest
28 | @testable import DynamicColor
29 |
30 | let TestsAcceptedAccuracy = CGFloat(0.000001)
31 |
32 | class DynamicColorHSBTests: XCTestCase {
33 | func testToHSBComponents() {
34 | let customColor = DynamicColor(hue: 0.1, saturation: 0.3, brightness: 0.5, alpha: 1)
35 | let hsb = customColor.toHSBComponents()
36 |
37 |
38 | XCTAssertEqual(hsb.h, 0.1, accuracy: TestsAcceptedAccuracy, "Color hue component should be equal to 0.1 (not \(hsb.h))")
39 | XCTAssertEqual(hsb.s, 0.3, accuracy: TestsAcceptedAccuracy, "Saturation component should be equal to 0.3 (not \(hsb.s))")
40 | XCTAssertEqual(hsb.b, 0.5, accuracy: TestsAcceptedAccuracy, "Brightness component should be equal to 0.3 (not \(hsb.b))")
41 |
42 | let blackHSB = DynamicColor.black.toHSBComponents()
43 |
44 | XCTAssertEqual(blackHSB.h, 0, accuracy: TestsAcceptedAccuracy, "Color hue component should be equal to 0 (not \(blackHSB.h))")
45 | XCTAssertEqual(blackHSB.s, 0, accuracy: TestsAcceptedAccuracy, "Saturation component should be equal to 0 (not \(blackHSB.s))")
46 | XCTAssertEqual(blackHSB.b, 0, accuracy: TestsAcceptedAccuracy, "Brightness component should be equal to 0 (not \(blackHSB.b))")
47 |
48 | let whiteHSB = DynamicColor.white.toHSBComponents()
49 |
50 | XCTAssertEqual(whiteHSB.h, 0, accuracy: TestsAcceptedAccuracy, "Color hue component should be equal to O (not \(whiteHSB.h))")
51 | XCTAssertEqual(whiteHSB.s, 0, accuracy: TestsAcceptedAccuracy, "Saturation component should be equal to 0 (not \(blackHSB.s))")
52 | XCTAssertEqual(whiteHSB.b, 1, accuracy: TestsAcceptedAccuracy, "Brightness component should be equal to 1 (not \(whiteHSB.b))")
53 |
54 |
55 | let redHSB = DynamicColor.red.toHSBComponents()
56 |
57 | XCTAssert(redHSB.h == 1 || redHSB.h == 0, "Color hue component should be equal to 1 or 0 (not \(redHSB.h))")
58 | XCTAssert(redHSB.s == 1, "Saturation component should be equal to 1 (not \(redHSB.s))")
59 | XCTAssert(redHSB.b == 1, "Brightness component should be equal to 1 (not \(redHSB.b))")
60 | }
61 |
62 | func testHueComponent() {
63 | let redHue = DynamicColor.red.hueComponent
64 |
65 | XCTAssert(redHue == 1 || redHue == 0, "Color hue component should be equal to 1 or 0 (not \(redHue))")
66 |
67 | let blackHue = DynamicColor(r: 0, g: 0, b: 0).hueComponent
68 |
69 | XCTAssert(blackHue == 0, "Color hue component should be equal to 0 (not \(blackHue))")
70 | }
71 |
72 | func testSaturationComponent() {
73 | let redSaturation = DynamicColor.red.saturationComponent
74 |
75 | XCTAssert(redSaturation == 1 || redSaturation == 0, "Color saturation component should be equal to 1 or 0 (not \(redSaturation))")
76 |
77 | let blackSaturation = DynamicColor(r: 0, g: 0, b: 0).saturationComponent
78 |
79 | XCTAssert(blackSaturation == 0, "Color saturation component should be equal to 0 (not \(blackSaturation))")
80 | }
81 |
82 | func testBrightnessComponent() {
83 | let redBrightness = DynamicColor.red.brightnessComponent
84 |
85 | XCTAssert(redBrightness == 1 || redBrightness == 0, "Color brightness component should be equal to 1 or 0 (not \(redBrightness))")
86 |
87 | let blackBrightness = DynamicColor(r: 0, g: 0, b: 0).brightnessComponent
88 |
89 | XCTAssert(blackBrightness == 0, "Color brightness component should be equal to 0 (not \(blackBrightness))")
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Sources/Core/DynamicColor+Deriving.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * DynamicColor
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | #if os(iOS) || os(tvOS) || os(watchOS)
28 | import UIKit
29 | #elseif os(OSX)
30 | import AppKit
31 | #endif
32 |
33 | // MARK: Deriving Colors
34 |
35 | public extension DynamicColor {
36 | /**
37 | Creates and returns a color object with the hue rotated along the color wheel by the given amount.
38 |
39 | - parameter amount: A float representing the number of degrees as ratio (usually between -360.0 degree and 360.0 degree).
40 | - returns: A DynamicColor object with the hue changed.
41 | */
42 | final func adjustedHue(amount: CGFloat) -> DynamicColor {
43 | return HSL(color: self).adjustedHue(amount: amount).toDynamicColor()
44 | }
45 |
46 | /**
47 | Creates and returns the complement of the color object.
48 |
49 | This is identical to adjustedHue(180).
50 |
51 | - returns: The complement DynamicColor.
52 | - seealso: adjustedHueColor:
53 | */
54 | final func complemented() -> DynamicColor {
55 | return adjustedHue(amount: 180.0)
56 | }
57 |
58 | /**
59 | Creates and returns a color object with the lightness increased by the given amount.
60 |
61 | - parameter amount: CGFloat between 0.0 and 1.0. Default value is 0.2.
62 | - returns: A lighter DynamicColor.
63 | */
64 | final func lighter(amount: CGFloat = 0.2) -> DynamicColor {
65 | return HSL(color: self).lighter(amount: amount).toDynamicColor()
66 | }
67 |
68 | /**
69 | Creates and returns a color object with the lightness decreased by the given amount.
70 |
71 | - parameter amount: Float between 0.0 and 1.0. Default value is 0.2.
72 | - returns: A darker DynamicColor.
73 | */
74 | final func darkened(amount: CGFloat = 0.2) -> DynamicColor {
75 | return HSL(color: self).darkened(amount: amount).toDynamicColor()
76 | }
77 |
78 | /**
79 | Creates and returns a color object with the saturation increased by the given amount.
80 |
81 | - parameter amount: CGFloat between 0.0 and 1.0. Default value is 0.2.
82 |
83 | - returns: A DynamicColor more saturated.
84 | */
85 | final func saturated(amount: CGFloat = 0.2) -> DynamicColor {
86 | return HSL(color: self).saturated(amount: amount).toDynamicColor()
87 | }
88 |
89 | /**
90 | Creates and returns a color object with the saturation decreased by the given amount.
91 |
92 | - parameter amount: CGFloat between 0.0 and 1.0. Default value is 0.2.
93 | - returns: A DynamicColor less saturated.
94 | */
95 | final func desaturated(amount: CGFloat = 0.2) -> DynamicColor {
96 | return HSL(color: self).desaturated(amount: amount).toDynamicColor()
97 | }
98 |
99 | /**
100 | Creates and returns a color object converted to grayscale.
101 |
102 | - returns: A grayscale DynamicColor.
103 | - seealso: desaturated:
104 | */
105 | final func grayscaled(mode: GrayscalingMode = .lightness) -> DynamicColor {
106 | let (r, g, b, a) = self.toRGBAComponents()
107 |
108 | let l: CGFloat
109 | switch mode {
110 | case .luminance:
111 | l = (0.299 * r) + (0.587 * g) + (0.114 * b)
112 | case .lightness:
113 | l = 0.5 * (max(r, g, b) + min(r, g, b))
114 | case .average:
115 | l = (1.0 / 3.0) * (r + g + b)
116 | case .value:
117 | l = max(r, g, b)
118 | }
119 |
120 | return HSL(hue: 0.0, saturation: 0.0, lightness: l, alpha: a).toDynamicColor()
121 | }
122 |
123 | /**
124 | Creates and return a color object where the red, green, and blue values are inverted, while the alpha channel is left alone.
125 |
126 | - returns: An inverse (negative) of the original color.
127 | */
128 | final func inverted() -> DynamicColor {
129 | let rgba = toRGBAComponents()
130 |
131 | let invertedRed = 1.0 - rgba.r
132 | let invertedGreen = 1.0 - rgba.g
133 | let invertedBlue = 1.0 - rgba.b
134 |
135 | return DynamicColor(red: invertedRed, green: invertedGreen, blue: invertedBlue, alpha: rgba.a)
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/Tests/DynamicColorLabTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * DynamicColor
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | import XCTest
28 | @testable import DynamicColor
29 |
30 | class DynamicColorLabTests: XCTestCase {
31 | func testInitWithLabComponents() {
32 | let whiteColor = DynamicColor(L: 100, a: 0, b: 0).toRGBAComponents()
33 | XCTAssert(whiteColor.r == 1, "Red component should be equal to 1 (not \(whiteColor.r))")
34 | XCTAssert(whiteColor.g == 1, "Green component should be equal to 1 (not \(whiteColor.g))")
35 | XCTAssert(whiteColor.b == 1, "Blue component should be equal to 1 (not \(whiteColor.b))")
36 |
37 | let blackColor = DynamicColor(L: 0, a: 0, b: 0).toRGBAComponents()
38 | XCTAssert(blackColor.r == 0, "Red component should be equal to 0 (not \(blackColor.r))")
39 | XCTAssert(blackColor.g == 0, "Green component should be equal to 0 (not \(blackColor.g))")
40 | XCTAssert(blackColor.b == 0, "Blue component should be equal to 0 (not \(blackColor.b))")
41 |
42 | let redColor = DynamicColor(L: 53.23, a: 80.10, b: 67.22).toRGBAComponents()
43 | XCTAssert(redColor.r == 1, "Red component should be equal to 1 (not \(redColor.r))")
44 | XCTAssert(redColor.g == 0, "Green component should be equal to 0 (not \(redColor.g))")
45 | XCTAssert(redColor.b == 0, "Blue component should be equal to 0 (not \(redColor.b))")
46 |
47 | let yellowColor = DynamicColor(L: 97.138, a: -21.56, b: 94.487).toRGBAComponents()
48 | XCTAssert(yellowColor.r == 1, "L component should be equal to 1 (not \(yellowColor.r))")
49 | XCTAssert(yellowColor.g == 1, "a component should be equal to 1 (not \(yellowColor.g))")
50 | XCTAssert(yellowColor.b == 0, "b component should be equal to 0 (not \(yellowColor.b))")
51 |
52 | let customColor = DynamicColor(L: 50.493, a: -49.333, b: 31.056).toRGBAComponents()
53 | XCTAssert(customColor.r == 0, "Red component should be equal to 0 (not \(customColor.r))")
54 | XCTAssert(customColor.g == 0.545, "Green component should be equal to 0.545 (not \(customColor.g))")
55 | XCTAssert(customColor.b == 0.251, "Blue component should be equal to 0.251 (not \(customColor.b))")
56 | }
57 |
58 | func testToLabComponents() {
59 | let whiteLab = DynamicColor.white.toLabComponents()
60 | XCTAssert(whiteLab.L == 100, "L component should be equal to 100 (not \(whiteLab.L))")
61 | XCTAssert(whiteLab.a == 0, "a component should be equal to 0 (not \(whiteLab.a))")
62 | XCTAssert(whiteLab.b == 0, "b component should be equal to 0 (not \(whiteLab.b))")
63 |
64 | let blackLab = DynamicColor.black.toLabComponents()
65 | XCTAssert(blackLab.L == 0, "L component should be equal to 0 (not \(blackLab.L))")
66 | XCTAssert(blackLab.a == 0, "a component should be equal to 0 (not \(blackLab.a))")
67 | XCTAssert(blackLab.b == 0, "b component should be equal to 0 (not \(blackLab.b))")
68 |
69 | let redLab = DynamicColor.red.toLabComponents()
70 | XCTAssert(redLab.L == 53.233, "L component should be equal to 53.233 (not \(redLab.L))")
71 | XCTAssert(redLab.a == 80.105, "a component should be equal to 80.105 (not \(redLab.a))")
72 | XCTAssert(redLab.b == 67.223, "b component should be equal to 67.223 (not \(redLab.b))")
73 |
74 | let yellowLab = DynamicColor.yellow.toLabComponents()
75 | XCTAssert(yellowLab.L == 97.138, "L component should be equal to 97.138 (not \(yellowLab.L))")
76 | XCTAssert(yellowLab.a == -21.561, "a component should be equal to -21.561 (not \(yellowLab.a))")
77 | XCTAssert(yellowLab.b == 94.488, "b component should be equal to 94.488 (not \(yellowLab.b))")
78 |
79 | let maxLab = DynamicColor(hex: 0xFF4500).toLabComponents()
80 | XCTAssert(maxLab.L == 57.575, "L component should be equal to 57.575 (not \(maxLab.L))")
81 | XCTAssert(maxLab.a == 67.792, "a component should be equal to 67.792 (not \(maxLab.a))")
82 | XCTAssert(maxLab.b == 68.977, "b component should be equal to 68.977 (not \(maxLab.b))")
83 |
84 | let minLab = DynamicColor(hex: 0x008B40).toLabComponents()
85 | XCTAssert(minLab.L == 50.494, "L component should be equal to 50.494 (not \(minLab.L))")
86 | XCTAssert(minLab.a == -49.334, "a component should be equal to -49.334 (not \(minLab.a))")
87 | XCTAssert(minLab.b == 31.053, "b component should be equal to 31.053 (not \(minLab.b))")
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Sources/Core/DynamicColor+Mixing.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * DynamicColor
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | #if os(iOS) || os(tvOS) || os(watchOS)
28 | import UIKit
29 | #elseif os(OSX)
30 | import AppKit
31 | #endif
32 |
33 | // MARK: Mixing Colors
34 |
35 | public extension DynamicColor {
36 | /**
37 | Mixes the given color object with the receiver.
38 |
39 | Specifically, takes the average of each of the RGB components, optionally weighted by the given percentage.
40 |
41 | - Parameter color: A color object to mix with the receiver.
42 | - Parameter weight: The weight specifies the amount of the given color object (between 0 and 1).
43 | The default value is 0.5, which means that half the given color and half the receiver color object should be used.
44 | 0.25 means that a quarter of the given color object and three quarters of the receiver color object should be used.
45 | - Parameter colorspace: The color space used to mix the colors. By default it uses the RBG color space.
46 | - Returns: A color object corresponding to the two colors object mixed together.
47 | */
48 | final func mixed(withColor color: DynamicColor, weight: CGFloat = 0.5, inColorSpace colorspace: DynamicColorSpace = .rgb) -> DynamicColor {
49 | let normalizedWeight = clip(weight, 0.0, 1.0)
50 |
51 | switch colorspace {
52 | case .lab:
53 | return mixedLab(withColor: color, weight: normalizedWeight)
54 | case .hsl:
55 | return mixedHSL(withColor: color, weight: normalizedWeight)
56 | case .hsb:
57 | return mixedHSB(withColor: color, weight: normalizedWeight)
58 | case .rgb:
59 | return mixedRGB(withColor: color, weight: normalizedWeight)
60 | }
61 | }
62 |
63 | /**
64 | Creates and returns a color object corresponding to the mix of the receiver and an amount of white color, which increases lightness.
65 |
66 | - Parameter amount: Float between 0.0 and 1.0. The default amount is equal to 0.2.
67 | - Returns: A lighter DynamicColor.
68 | */
69 | final func tinted(amount: CGFloat = 0.2) -> DynamicColor {
70 | return mixed(withColor: .white, weight: amount)
71 | }
72 |
73 | /**
74 | Creates and returns a color object corresponding to the mix of the receiver and an amount of black color, which reduces lightness.
75 |
76 | - Parameter amount: Float between 0.0 and 1.0. The default amount is equal to 0.2.
77 | - Returns: A darker DynamicColor.
78 | */
79 | final func shaded(amount: CGFloat = 0.2) -> DynamicColor {
80 | return mixed(withColor: DynamicColor(red: 0, green: 0, blue: 0, alpha: 1), weight: amount)
81 | }
82 |
83 | // MARK: - Convenient Internal Methods
84 |
85 | func mixedLab(withColor color: DynamicColor, weight: CGFloat) -> DynamicColor {
86 | let c1 = toLabComponents()
87 | let c2 = color.toLabComponents()
88 |
89 | let L = c1.L + (weight * (c2.L - c1.L))
90 | let a = c1.a + (weight * (c2.a - c1.a))
91 | let b = c1.b + (weight * (c2.b - c1.b))
92 | let alpha = alphaComponent + (weight * (color.alphaComponent - alphaComponent))
93 |
94 | return DynamicColor(L: L, a: a, b: b, alpha: alpha)
95 | }
96 |
97 | func mixedHSL(withColor color: DynamicColor, weight: CGFloat) -> DynamicColor {
98 | let c1 = toHSLComponents()
99 | let c2 = color.toHSLComponents()
100 |
101 | let h = c1.h + (weight * mixedHue(source: c1.h, target: c2.h))
102 | let s = c1.s + (weight * (c2.s - c1.s))
103 | let l = c1.l + (weight * (c2.l - c1.l))
104 | let alpha = alphaComponent + (weight * (color.alphaComponent - alphaComponent))
105 |
106 | return DynamicColor(hue: h, saturation: s, lightness: l, alpha: alpha)
107 | }
108 |
109 | func mixedHSB(withColor color: DynamicColor, weight: CGFloat) -> DynamicColor {
110 | let c1 = toHSBComponents()
111 | let c2 = color.toHSBComponents()
112 |
113 | let h = c1.h + (weight * mixedHue(source: c1.h, target: c2.h))
114 | let s = c1.s + (weight * (c2.s - c1.s))
115 | let b = c1.b + (weight * (c2.b - c1.b))
116 | let alpha = alphaComponent + (weight * (color.alphaComponent - alphaComponent))
117 |
118 | return DynamicColor(hue: h, saturation: s, brightness: b, alpha: alpha)
119 | }
120 |
121 | func mixedRGB(withColor color: DynamicColor, weight: CGFloat) -> DynamicColor {
122 | let c1 = toRGBAComponents()
123 | let c2 = color.toRGBAComponents()
124 |
125 | let red = c1.r + (weight * (c2.r - c1.r))
126 | let green = c1.g + (weight * (c2.g - c1.g))
127 | let blue = c1.b + (weight * (c2.b - c1.b))
128 | let alpha = alphaComponent + (weight * (color.alphaComponent - alphaComponent))
129 |
130 | return DynamicColor(red: red, green: green, blue: blue, alpha: alpha)
131 | }
132 |
133 | func mixedHue(source: CGFloat, target: CGFloat) -> CGFloat {
134 | if target > source && target - source > 180.0 {
135 | return target - source + 360.0
136 | }
137 | else if target < source && source - target > 180.0 {
138 | return target + 360.0 - source
139 | }
140 |
141 | return target - source
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/Sources/Shared/HSL.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * DynamicColor
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | import SwiftUI
28 |
29 | #if os(iOS) || os(tvOS) || os(watchOS)
30 | import UIKit
31 | #elseif os(OSX)
32 | import AppKit
33 | #endif
34 |
35 | /// Hue-saturation-lightness structure to make the color manipulation easier.
36 | internal struct HSL {
37 | /// Hue value between 0.0 and 1.0 (0.0 = 0 degree, 1.0 = 360 degree).
38 | var h: CGFloat = 0.0
39 | /// Saturation value between 0.0 and 1.0.
40 | var s: CGFloat = 0.0
41 | /// Lightness value between 0.0 and 1.0.
42 | var l: CGFloat = 0.0
43 | /// Alpha value between 0.0 and 1.0.
44 | var a: CGFloat = 1.0
45 |
46 | // MARK: - Initializing HSL Colors
47 |
48 | /**
49 | Initializes and creates a HSL color from the hue, saturation, lightness and alpha components.
50 |
51 | - parameter h: The hue component of the color object, specified as a value between 0.0 and 360.0 degree.
52 | - parameter s: The saturation component of the color object, specified as a value between 0.0 and 1.0.
53 | - parameter l: The lightness component of the color object, specified as a value between 0.0 and 1.0.
54 | - parameter a: The opacity component of the color object, specified as a value between 0.0 and 1.0.
55 | */
56 | init(hue: CGFloat, saturation: CGFloat, lightness: CGFloat, alpha: CGFloat = 1.0) {
57 | h = hue.truncatingRemainder(dividingBy: 360.0) / 360.0
58 | s = clip(saturation, 0.0, 1.0)
59 | l = clip(lightness, 0.0, 1.0)
60 | a = clip(alpha, 0.0, 1.0)
61 | }
62 |
63 | /**
64 | Initializes and creates a HSL (hue, saturation, lightness) color from a DynamicColor object.
65 |
66 | - parameter color: A DynamicColor object.
67 | */
68 | init(color: DynamicColor) {
69 | let rgba = color.toRGBAComponents()
70 |
71 | let maximum = max(rgba.r, max(rgba.g, rgba.b))
72 | let minimum = min(rgba.r, min(rgba.g, rgba.b))
73 |
74 | let delta = maximum - minimum
75 |
76 | h = 0.0
77 | s = 0.0
78 | l = (maximum + minimum) / 2.0
79 |
80 | if delta != 0.0 {
81 | if l < 0.5 {
82 | s = delta / (maximum + minimum)
83 | }
84 | else {
85 | s = delta / (2.0 - maximum - minimum)
86 | }
87 |
88 | if rgba.r == maximum {
89 | h = ((rgba.g - rgba.b) / delta) + (rgba.g < rgba.b ? 6.0 : 0.0)
90 | }
91 | else if rgba.g == maximum {
92 | h = ((rgba.b - rgba.r) / delta) + 2.0
93 | }
94 | else if rgba.b == maximum {
95 | h = ((rgba.r - rgba.g) / delta) + 4.0
96 | }
97 | }
98 |
99 | h /= 6.0
100 | a = rgba.a
101 | }
102 |
103 | // MARK: - Transforming HSL Color
104 |
105 | /**
106 | Returns the DynamicColor representation from the current HSV color.
107 |
108 | - returns: A DynamicColor object corresponding to the current HSV color.
109 | */
110 | func toDynamicColor() -> DynamicColor {
111 | let (r, g, b, a) = rgbaComponents()
112 |
113 | return DynamicColor(red: r, green: g, blue: b, alpha: a)
114 | }
115 |
116 | /// Returns the RGBA components from the current HSV color.
117 | func rgbaComponents() -> (CGFloat, CGFloat, CGFloat, CGFloat) {
118 | let m2 = l <= 0.5 ? l * (s + 1.0) : (l + s) - (l * s)
119 | let m1 = (l * 2.0) - m2
120 |
121 | let r = hueToRGB(m1: m1, m2: m2, h: h + (1.0 / 3.0))
122 | let g = hueToRGB(m1: m1, m2: m2, h: h)
123 | let b = hueToRGB(m1: m1, m2: m2, h: h - (1.0 / 3.0))
124 |
125 | return (r, g, b, CGFloat(a))
126 | }
127 |
128 | /// Hue to RGB helper function
129 | private func hueToRGB(m1: CGFloat, m2: CGFloat, h: CGFloat) -> CGFloat {
130 | let hue = moda(h, m: 1)
131 |
132 | if hue * 6 < 1.0 {
133 | return m1 + ((m2 - m1) * hue * 6.0)
134 | }
135 | else if hue * 2.0 < 1.0 {
136 | return m2
137 | }
138 | else if hue * 3.0 < 1.9999 {
139 | return m1 + ((m2 - m1) * ((2.0 / 3.0) - hue) * 6.0)
140 | }
141 |
142 | return m1
143 | }
144 |
145 | // MARK: - Deriving the Color
146 |
147 | /**
148 | Returns a color with the hue rotated along the color wheel by the given amount.
149 |
150 | - parameter amount: A float representing the number of degrees as ratio (usually between -360.0 degree and 360.0 degree).
151 | - returns: A HSL color with the hue changed.
152 | */
153 | func adjustedHue(amount: CGFloat) -> HSL {
154 | return HSL(hue: (h * 360.0) + amount, saturation: s, lightness: l, alpha: a)
155 | }
156 |
157 | /**
158 | Returns a color with the lightness increased by the given amount.
159 |
160 | - parameter amount: CGFloat between 0.0 and 1.0.
161 | - returns: A lighter HSL color.
162 | */
163 | func lighter(amount: CGFloat) -> HSL {
164 | return HSL(hue: h * 360.0, saturation: s, lightness: l + amount, alpha: a)
165 | }
166 |
167 | /**
168 | Returns a color with the lightness decreased by the given amount.
169 |
170 | - parameter amount: CGFloat between 0.0 and 1.0.
171 | - returns: A darker HSL color.
172 | */
173 | func darkened(amount: CGFloat) -> HSL {
174 | return lighter(amount: amount * -1.0)
175 | }
176 |
177 | /**
178 | Returns a color with the saturation increased by the given amount.
179 |
180 | - parameter amount: CGFloat between 0.0 and 1.0.
181 | - returns: A HSL color more saturated.
182 | */
183 | func saturated(amount: CGFloat) -> HSL {
184 | return HSL(hue: h * 360.0, saturation: s + amount, lightness: l, alpha: a)
185 | }
186 |
187 | /**
188 | Returns a color with the saturation decreased by the given amount.
189 |
190 | - parameter amount: CGFloat between 0.0 and 1.0.
191 | - returns: A HSL color less saturated.
192 | */
193 | func desaturated(amount: CGFloat) -> HSL {
194 | return saturated(amount: amount * -1.0)
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change log
2 |
3 | ## [Version 5.0.0](https://github.com/yannickl/DynamicColor/releases/tag/5.0.0)
4 | *Released on 2019-12-24.*
5 |
6 | - [ADD] Basic SwiftUI color support
7 |
8 | ## [Version 4.1.1](https://github.com/yannickl/DynamicColor/releases/tag/4.2.0)
9 | *Released on 2019-09-15.*
10 |
11 | - [FIX] Add parenthesis arround mathematical operators to avoid precedence issues (#51)
12 | - [ADD] `toRGBA` method
13 | - [ADD] `toAGBR` method
14 |
15 | ## [Version 4.1.0](https://github.com/yannickl/DynamicColor/releases/tag/4.1.0)
16 | *Released on 2019-05-27.*
17 |
18 | - [FIX] Minor improvements (compilation performance, documentation)
19 | - Swift 5/Xcode 10 support
20 |
21 | ## [Version 4](https://github.com/yannickl/DynamicColor/releases/tag/4)
22 | *Released on 2017-11-07.*
23 |
24 | - [FIX] Minor improvements (compilation performance, documentation)
25 | - Swift 4/Xcode 9 support
26 |
27 | ## [Version 3.3](https://github.com/yannickl/DynamicColor/releases/tag/3.3)
28 | *Released on 2017-04-15.*
29 |
30 | - [ADD] Support alpha channel with the hex strings (e.g. #FF0934CC)
31 |
32 | ## [Version 3.2.1](https://github.com/yannickl/DynamicColor/releases/tag/3.2.1)
33 | *Released on 2016-12-15.*
34 |
35 | - [FIX] `mixed` method with the alpha channel
36 |
37 | ## [Version 3.2.0](https://github.com/yannickl/DynamicColor/releases/tag/3.2.0)
38 | *Released on 2016-12-12.*
39 |
40 | - [ADD] `luminance` property
41 | - [ADD] `contrastRatio` method
42 | - [ADD] `isContrasting` method
43 |
44 | ## [Version 3.1.0](https://github.com/yannickl/DynamicColor/releases/tag/3.1.0)
45 | *Released on 2016-09-08.*
46 |
47 | - [ADD] CIE XYZ Color Space
48 | - [ADD] Initialization with XYZ components
49 | - [ADD] `toXYZComponents()` method
50 | - [ADD] CIE L*a*b* Color Space
51 | - [ADD] Initialization with L*a*b* components
52 | - [ADD] `toLabComponents()` method
53 | - [ADD] `toHSBComponents()` method
54 | - [REFACTORING] `toHSLAComponents` to `toHSLComponents`
55 | - [REFACTORING] Hue range is now from 0° and 360° instead of 0.0 and 1.0
56 | - [ADD] `DynamicGradient` object
57 | - [ADD] `colorPalette` method to a gradient
58 | - [ADD] `pickColorAt` method to a gradient
59 | - [ADD] `gradient` property to array of colors
60 | - [ADD] `DynamicColorSpace` enum
61 | - [REFACTORING] `mixed` colors using different color spaces
62 |
63 | ## [Version 3.0.0](https://github.com/yannickl/DynamicColor/releases/tag/3.0.0)
64 | *Released on 2016-06-14.*
65 |
66 | - Swift 3 Supports
67 | - `isLight` instead of `isLightColor`
68 | - `adjustedAlpha` instead of `adjustedAlphaColor`
69 | - `inverted` instead of `invertColor`
70 | - `grayscaled` instead of `grayscaledColor`
71 | - `darkened` instead of `darkerColor`
72 | - `lighter` instead of `lighterColor`
73 | - `saturated` instead of `saturatedColor`
74 | - `desaturated` instead of `desaturatedColor`
75 | - `complemented` instead of `complementColor`
76 | - `adjustedHue` instead of `adjustedHueColor`
77 | - `tinted` instead of `tintColor`
78 | - `shaded` instead of `shadeColor`
79 | - `mixed` instead of `mixWithColor`
80 | - `isEqual:toHex` instead of `isEqualToHex`
81 | - `isEqual:toHexString` instead of `isEqualToHexString`
82 | - Removing the `darkenColor`, use `darkened` instead
83 | - Removing the `lightenColor`, use `lighter` instead
84 | - Removing the `saturateColor`, use `saturated` instead
85 | - Removing the `desaturateColor`, use `desaturated` instead
86 | - Use `CGFloat` instead of `Double` everywhere
87 |
88 | ## [Version 2.4.0](https://github.com/yannickl/DynamicColor/releases/tag/2.4.0)
89 | *Released on 2016-02-29.*
90 |
91 | - [ADD] Swift Package Manager supports
92 |
93 | ## [Version 2.3.0](https://github.com/yannickl/DynamicColor/releases/tag/2.3.0)
94 | *Released on 2015-12-12.*
95 |
96 | - [ADD] `adjustedAlphaColor` method
97 | - [REFACTORING] Move `redComponent/greenComponent/blueComponent/alphaComponent` methods to properties in order to reflect the OSX API
98 |
99 | ## [Version 2.2.0](https://github.com/yannickl/DynamicColor/releases/tag/2.2.0)
100 | *Released on 2015-11-19.*
101 |
102 | - [ADD] `isLightColor` method
103 | - [REFACTORING] `red/green/blue/alpha` methods to `redComponent/greenComponent/blueComponent/alphaComponent`
104 | - [ADD] OSX supports
105 |
106 | ## [Version 2.1.0](https://github.com/yannickl/DynamicColor/releases/tag/2.1.0)
107 | *Released on 2015-10-29.*
108 |
109 | - [ADD] WatchOS 2 supports
110 | - [ADD] TVOS 9 supports
111 |
112 | ## [Version 2.0.1](https://github.com/yannickl/DynamicColor/releases/tag/2.0.1)
113 | *Released on 2015-10-26.*
114 |
115 | - [FIX] BITCODE supports ([#6](https://github.com/yannickl/DynamicColor/pull/6))
116 |
117 | ## [Version 2.0.0](https://github.com/yannickl/DynamicColor/releases/tag/2.0.0)
118 | *Released on 2015-09-17.*
119 |
120 | - [UPDATE] Swift 2 compatibility
121 | - [FIX] Use Double instead of CGFloat due to float precision
122 |
123 | ## [Version 1.5.4](https://github.com/yannickl/DynamicColor/releases/tag/1.5.4)
124 | *Released on 2015-09-01.*
125 |
126 | - [FIX] Mandatory clip parameters for Xcode7 beta 6 ([#5](https://github.com/yannickl/DynamicColor/pull/5))
127 |
128 | ## [Version 1.5.3](https://github.com/yannickl/DynamicColor/releases/tag/1.5.3)
129 | *Released on 2015-08-29.*
130 |
131 | - [UPDATE] Make the DynamicColor typealias public
132 |
133 | ## [Version 1.5.2](https://github.com/yannickl/DynamicColor/releases/tag/1.5.2)
134 | Released on 2015-08-27.
135 |
136 | - [ADD] Changelog.md file
137 |
138 | ## [Version 1.5.1](https://github.com/yannickl/DynamicColor/releases/tag/1.5.1)
139 | *Released on 2015-07-29.*
140 |
141 | - [FIX] Project framework target sets to 8.0 for Carthage support ([#4](https://github.com/yannickl/DynamicColor/pull/4))
142 |
143 | ## [Version 1.5.0](https://github.com/yannickl/DynamicColor/releases/tag/1.5.0)
144 | *Released on 2015-07-28.*
145 |
146 | - [ADD] Initialization with hue
147 | - [ADD] `toHSLAComponents` method
148 |
149 | ## [Version 1.4.0](https://github.com/yannickl/DynamicColor/releases/tag/1.4.0)
150 | *Released on 2015-07-24.*
151 |
152 | - [ADD] `toHex` method
153 | - [ADD] `isEqualToHex` method
154 |
155 | ## [Version 1.3.0](https://github.com/yannickl/DynamicColor/releases/tag/1.3.0)
156 | *Released on 2015-07-09.*
157 |
158 | - [UPDATE] Documentation
159 | - [FIX] Some var to let ([#3](https://github.com/yannickl/DynamicColor/pull/3))
160 | - [ADD] `toComponents` method
161 | - [ADD] `red/green/blue/alpha` methods
162 |
163 | ## [Version 1.2.0](https://github.com/yannickl/DynamicColor/releases/tag/1.2.0)
164 | *Released on 2015-06-12.*
165 |
166 | - [ADD] Carthage support
167 |
168 | ## [Version 1.1.1](https://github.com/yannickl/DynamicColor/releases/tag/1.1.1)
169 | *Released on 2015-06-10.*
170 |
171 | - [FIX] Typos in the documentation ([#1](https://github.com/yannickl/DynamicColor/pull/1))
172 | - [IMPROVEMENT] Check interval for parameter
173 |
174 | ## [Version 1.1.0](https://github.com/yannickl/DynamicColor/releases/tag/1.1.0)
175 | *Released on 2015-06-06.*
176 |
177 | - [ADD] `shadeColor` method
178 | - [ADD] `tintColor` method
179 | - [ADD] `mixWithColor` method
180 |
181 | ## [Version 1.0.0](https://github.com/yannickl/DynamicColor/releases/tag/1.0.0)
182 | *Released on 2015-06-02.*
183 |
184 | - `saturated` method
185 | - `desaturate` method
186 | - `lighter/lighten` methods
187 | - `darker/darken` methods
188 | - `grayscale` method
189 | - `adjustHue` method
190 | - `complementColor` method
191 | - `invertColor` method
192 | - `toHexString` method
193 | - `isEqualToHexString` method
194 | - Initialization with hex strings and integers
195 | - Cocoapods support
196 |
--------------------------------------------------------------------------------
/Sources/Core/DynamicColor.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * DynamicColor
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | #if os(iOS) || os(tvOS) || os(watchOS)
28 | import UIKit
29 |
30 | /**
31 | Extension to manipulate colours easily.
32 |
33 | It allows you to work hexadecimal strings and value, HSV and RGB components, derivating colours, and many more...
34 | */
35 | public typealias DynamicColor = UIColor
36 | #elseif os(OSX)
37 | import AppKit
38 |
39 | /**
40 | Extension to manipulate colours easily.
41 |
42 | It allows you to work hexadecimal strings and value, HSV and RGB components, derivating colours, and many more...
43 | */
44 | public typealias DynamicColor = NSColor
45 | #endif
46 |
47 | public extension DynamicColor {
48 | // MARK: - Manipulating Hexa-decimal Values and Strings
49 |
50 | /**
51 | Creates a color from an hex string (e.g. "#3498db"). The RGBA string are also supported (e.g. "#3498dbff").
52 |
53 | If the given hex string is invalid the initialiser will create a black color.
54 |
55 | - parameter hexString: A hexa-decimal color string representation.
56 | */
57 | convenience init(hexString: String) {
58 | let hexString = hexString.trimmingCharacters(in: .whitespacesAndNewlines)
59 | let scanner = Scanner(string: hexString)
60 | scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#")
61 |
62 | var color: UInt64 = 0
63 |
64 | if scanner.scanHexInt64(&color) {
65 | self.init(hex: color, useAlpha: hexString.count > 7)
66 | }
67 | else {
68 | self.init(hex: 0x000000)
69 | }
70 | }
71 |
72 | /**
73 | Creates a color from an hex integer (e.g. 0x3498db).
74 |
75 | - parameter hex: A hexa-decimal UInt64 that represents a color.
76 | - parameter alphaChannel: If true the given hex-decimal UInt64 includes the alpha channel (e.g. 0xFF0000FF).
77 | */
78 | convenience init(hex: UInt64, useAlpha alphaChannel: Bool = false) {
79 | let mask = UInt64(0xFF)
80 | let cappedHex = !alphaChannel && hex > 0xffffff ? 0xffffff : hex
81 |
82 | let r = cappedHex >> (alphaChannel ? 24 : 16) & mask
83 | let g = cappedHex >> (alphaChannel ? 16 : 8) & mask
84 | let b = cappedHex >> (alphaChannel ? 8 : 0) & mask
85 | let a = alphaChannel ? cappedHex & mask : 255
86 |
87 | let red = CGFloat(r) / 255.0
88 | let green = CGFloat(g) / 255.0
89 | let blue = CGFloat(b) / 255.0
90 | let alpha = CGFloat(a) / 255.0
91 |
92 | self.init(red: red, green: green, blue: blue, alpha: alpha)
93 | }
94 |
95 | /**
96 | Returns the color representation as hexadecimal string.
97 |
98 | - returns: A string similar to this pattern "#f4003b".
99 | */
100 | final func toHexString() -> String {
101 | return String(format: "#%06x", toHex())
102 | }
103 |
104 | /**
105 | Returns the color representation as an integer (without the alpha channel).
106 |
107 | - returns: A UInt32 that represents the hexa-decimal color.
108 | */
109 | final func toHex() -> UInt32 {
110 | let rgba = toRGBAComponents()
111 |
112 | return roundToHex(rgba.r) << 16 | roundToHex(rgba.g) << 8 | roundToHex(rgba.b)
113 | }
114 |
115 | /**
116 | Returns the RGBA color representation.
117 |
118 | - returns: A UInt32 that represents the color as an RGBA value.
119 | */
120 | func toRGBA() -> UInt32 {
121 | let rgba = toRGBAComponents()
122 |
123 | return roundToHex(rgba.r) << 24 | roundToHex(rgba.g) << 16 | roundToHex(rgba.b) << 8 | roundToHex(rgba.a)
124 | }
125 |
126 | /**
127 | Returns the AGBR color representation.
128 |
129 | - returns: A UInt32 that represents the color as an AGBR value.
130 | */
131 | func toAGBR() -> UInt32 {
132 | let rgba = toRGBAComponents()
133 |
134 | return roundToHex(rgba.a) << 24 | roundToHex(rgba.b) << 16 | roundToHex(rgba.g) << 8 | roundToHex(rgba.r)
135 | }
136 |
137 | // MARK: - Identifying and Comparing Colors
138 |
139 | /**
140 | Returns a boolean value that indicates whether the receiver is equal to the given hexa-decimal string.
141 |
142 | - parameter hexString: A hexa-decimal color number representation to be compared to the receiver.
143 | - returns: true if the receiver and the string are equals, otherwise false.
144 | */
145 | func isEqual(toHexString hexString: String) -> Bool {
146 | return self.toHexString() == hexString
147 | }
148 |
149 | /**
150 | Returns a boolean value that indicates whether the receiver is equal to the given hexa-decimal integer.
151 |
152 | - parameter hex: A UInt32 that represents the hexa-decimal color.
153 | - returns: true if the receiver and the integer are equals, otherwise false.
154 | */
155 | func isEqual(toHex hex: UInt32) -> Bool {
156 | return self.toHex() == hex
157 | }
158 |
159 | // MARK: - Querying Colors
160 |
161 | /**
162 | Determines if the color object is dark or light.
163 |
164 | It is useful when you need to know whether you should display the text in black or white.
165 |
166 | - returns: A boolean value to know whether the color is light. If true the color is light, dark otherwise.
167 | */
168 | func isLight() -> Bool {
169 | let components = toRGBAComponents()
170 | let brightness = ((components.r * 299.0) + (components.g * 587.0) + (components.b * 114.0)) / 1000.0
171 |
172 | return brightness >= 0.5
173 | }
174 |
175 | /**
176 | A float value representing the luminance of the current color. May vary from 0 to 1.0.
177 |
178 | We use the formula described by W3C in WCAG 2.0. You can read more here: https://www.w3.org/TR/WCAG20/#relativeluminancedef.
179 | */
180 | var luminance: CGFloat {
181 | let components = toRGBAComponents()
182 |
183 | let componentsArray = [components.r, components.g, components.b].map { (val) -> CGFloat in
184 | guard val <= 0.03928 else { return pow((val + 0.055) / 1.055, 2.4) }
185 |
186 | return val / 12.92
187 | }
188 |
189 | return (0.2126 * componentsArray[0]) + (0.7152 * componentsArray[1]) + (0.0722 * componentsArray[2])
190 | }
191 |
192 | /**
193 | Returns a float value representing the contrast ratio between 2 colors.
194 |
195 | We use the formula described by W3C in WCAG 2.0. You can read more here: https://www.w3.org/TR/WCAG20-TECHS/G18.html
196 | NB: the contrast ratio is a relative value. So the contrast between Color1 and Color2 is exactly the same between Color2 and Color1.
197 |
198 | - returns: A CGFloat representing contrast value.
199 | */
200 | func contrastRatio(with otherColor: DynamicColor) -> CGFloat {
201 | let otherLuminance = otherColor.luminance
202 |
203 | let l1 = max(luminance, otherLuminance)
204 | let l2 = min(luminance, otherLuminance)
205 |
206 | return (l1 + 0.05) / (l2 + 0.05)
207 | }
208 |
209 | /**
210 | Indicates if two colors are contrasting, regarding W3C's WCAG 2.0 recommendations.
211 |
212 | You can read it here: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast-contrast
213 |
214 | The acceptable contrast ratio depends on the context of display. Most of the time, the default context (.Standard) is enough.
215 |
216 | You can look at ContrastDisplayContext for more options.
217 |
218 | - parameter otherColor: The other color to compare with.
219 | - parameter context: An optional context to determine the minimum acceptable contrast ratio. Default value is .Standard.
220 |
221 | - returns: true is the contrast ratio between 2 colors exceed the minimum acceptable ratio.
222 | */
223 | func isContrasting(with otherColor: DynamicColor, inContext context: ContrastDisplayContext = .standard) -> Bool {
224 | return self.contrastRatio(with: otherColor) > context.minimumContrastRatio
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/Examples/tvOSExample/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | **DynamicColor** provides powerful methods to manipulate colors in an easy way in Swift and SwiftUI.
15 |
16 |
24 |
25 | ## Requirements
26 |
27 | - iOS 11.0+ / Mac OS X 10.11+ / tvOS 11.0+ / watchOS 4.0+
28 | - Xcode 10.2+
29 | - Swift 5.0+
30 |
31 | ## Usage
32 |
33 | #### Creation (Hex String)
34 |
35 | Firstly, DynamicColor provides useful initializers to create colors using hex strings or values:
36 |
37 | ```swift
38 | let color = UIColor(hexString: "#3498db")
39 | // equivalent to
40 | // color = UIColor(hex: 0x3498db)
41 | ```
42 |
43 | To be platform independent, the typealias `DynamicColor` can also be used:
44 |
45 | ```swift
46 | let color = DynamicColor(hex: 0x3498db)
47 | // On iOS, WatchOS or tvOS, equivalent to
48 | // color = UIColor(hex: 0x3498db)
49 | // On OSX, equivalent to
50 | // color = NSColor(hex: 0x3498db)
51 | ```
52 |
53 | You can also retrieve the RGBA value and components very easily using multiple methods like `toHexString`, `toHex`, `toRGBA`, etc.
54 |
55 | ##### SwiftUI
56 |
57 | From the v5, DynamicColor also support basic methods to create and manipulate colors with SwiftUI.
58 |
59 | ```swift
60 | let color = Color(hex: 0x3498db)
61 | ```
62 |
63 | #### Darken & Lighten
64 |
65 | These two create a new color by adjusting the lightness of the receiver. You have to use a value between 0 and 1.
66 |
67 |
68 |
69 |
70 |
71 | ```swift
72 | let originalColor = DynamicColor(hexString: "#c0392b")
73 |
74 | let lighterColor = originalColor.lighter()
75 | // equivalent to
76 | // lighterColor = originalColor.lighter(amount: 0.2)
77 |
78 | let darkerColor = originalColor.darkened()
79 | // equivalent to
80 | // darkerColor = originalColor.darkened(amount: 0.2)
81 | ```
82 |
83 | #### Saturate, Desaturate & Grayscale
84 |
85 | These will adjust the saturation of the color object, much like `darkened` and `lighter` adjusted the lightness. Again, you need to use a value between 0 and 1.
86 |
87 |
88 |
89 |
90 |
91 | ```swift
92 | let originalColor = DynamicColor(hexString: "#c0392b")
93 |
94 | let saturatedColor = originalColor.saturated()
95 | // equivalent to
96 | // saturatedColor = originalColor.saturated(amount: 0.2)
97 |
98 | let desaturatedColor = originalColor.desaturated()
99 | // equivalent to
100 | // desaturatedColor = originalColor.desaturated(amount: 0.2)
101 |
102 | // equivalent to
103 | // let grayscaledColor = originalColor.grayscaled(mode: .luminance)
104 | let grayscaledColor = originalColor.grayscaled()
105 |
106 | let grayscaledColorLuminance = originalColor.grayscaled(mode: .luminance)
107 | let grayscaledColorLightness = originalColor.grayscaled(mode: .lightness)
108 | let grayscaledColorAverage = originalColor.grayscaled(mode: .average)
109 | let grayscaledColorValue = originalColor.grayscaled(mode: .value)
110 | ```
111 |
112 | #### Adjust-hue & Complement
113 |
114 | These adjust the hue value of the color in the same way like the others do. Again, it takes a value between 0 and 1 to update the value.
115 |
116 |
117 |
118 |
119 |
120 | ```swift
121 | let originalColor = DynamicColor(hex: 0xc0392b)
122 |
123 | // Hue values are in degrees
124 | let adjustHueColor = originalColor.adjustedHue(amount: 45)
125 |
126 | let complementedColor = originalColor.complemented()
127 | ````
128 |
129 | #### Tint & Shade
130 |
131 | A tint is the mixture of a color with white and a shade is the mixture of a color with black. Again, it takes a value between 0 and 1 to update the value.
132 |
133 |
134 |
135 |
136 |
137 | ```swift
138 | let originalColor = DynamicColor(hexString: "#c0392b")
139 |
140 | let tintedColor = originalColor.tinted()
141 | // equivalent to
142 | // tintedColor = originalColor.tinted(amount: 0.2)
143 |
144 | let shadedColor = originalColor.shaded()
145 | // equivalent to
146 | // shadedColor = originalColor.shaded(amount: 0.2)
147 | ```
148 |
149 | #### Invert
150 |
151 | This can invert the color object. The red, green, and blue values are inverted, while the opacity is left alone.
152 |
153 |
154 |
155 |
156 |
157 | ```swift
158 | let originalColor = DynamicColor(hexString: "#c0392b")
159 |
160 | let invertedColor = originalColor.inverted()
161 | ```
162 |
163 | #### Mix
164 |
165 | This can mix a given color with the receiver. It takes the average of each of the RGB components, optionally weighted by the given percentage (value between 0 and 1).
166 |
167 |
168 |
169 |
170 |
171 | ```swift
172 | let originalColor = DynamicColor(hexString: "#c0392b")
173 |
174 | let mixedColor = originalColor.mixed(withColor: .blue)
175 | // equivalent to
176 | // mixedColor = originalColor.mixed(withColor: .blue, weight: 0.5)
177 | // or
178 | // mixedColor = originalColor.mixed(withColor: .blue, weight: 0.5, inColorSpace: .rgb)
179 | ```
180 |
181 | #### Gradients
182 |
183 | **DynamicColor** provides an useful object to work with gradients: **DynamicGradient**. It'll allow you to pick color from gradients, or to build a palette using different color spaces (.e.g.: *RGB*, *HSL*, *HSB*, *Cie L\*a\*b\**).
184 |
185 | Let's define our reference colors and the gradient object:
186 | ```swift
187 | let blue = UIColor(hexString: "#3498db")
188 | let red = UIColor(hexString: "#e74c3c")
189 | let yellow = UIColor(hexString: "#f1c40f")
190 |
191 | let gradient = DynamicGradient(colors: [blue, red, yellow])
192 | // equivalent to
193 | // let gradient = [blue, red, yellow].gradient
194 | ```
195 |
196 | ##### RGB
197 |
198 | Let's build the RGB palette (the default color space) with 8 colors:
199 |
200 |
201 |
202 |
203 |
204 | ```swift
205 | let rgbPalette = gradient.colorPalette(amount: 8)
206 | ```
207 |
208 | ##### HSL
209 |
210 | Now if you want to change the gradient color space to have a different effect, just write the following lines:
211 |
212 |
213 |
214 |
215 |
216 | ```swift
217 | let hslPalette = gradient.colorPalette(amount: 8, inColorSpace: .hsl)
218 | ```
219 |
220 | ##### Cie L\*a\*b\*
221 |
222 | Or if you prefer to work directly with array of colors, you can:
223 |
224 |
225 |
226 |
227 |
228 | ```swift
229 | let labPalette = [blue, red, yellow].gradient.colorPalette(amount: 8, inColorSpace: .lab)
230 | ```
231 |
232 | #### And many more...
233 |
234 | `DynamicColor` also provides many another useful methods to manipulate the colors like hex strings, color components, color spaces, etc. To go further, take a look at the example project.
235 |
236 | ## Installation
237 |
238 | #### CocoaPods
239 |
240 | Install CocoaPods if not already available:
241 |
242 | ``` bash
243 | $ [sudo] gem install cocoapods
244 | $ pod setup
245 | ```
246 | Go to the directory of your Xcode project, and Create and Edit your *Podfile* and add _DynamicColor_:
247 |
248 | ``` bash
249 | $ cd /path/to/MyProject
250 | $ touch Podfile
251 | $ edit Podfile
252 | source 'https://github.com/CocoaPods/Specs.git'
253 | platform :ios, '8.0'
254 |
255 | use_frameworks!
256 | pod 'DynamicColor', '~> 5.0.0'
257 | ```
258 |
259 | Install into your project:
260 |
261 | ``` bash
262 | $ pod install
263 | ```
264 |
265 | Open your project in Xcode from the .xcworkspace file (not the usual project file):
266 |
267 | ``` bash
268 | $ open MyProject.xcworkspace
269 | ```
270 |
271 | You can now `import DynamicColor` framework into your files.
272 |
273 | #### Carthage
274 |
275 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that automates the process of adding frameworks to your Cocoa application.
276 |
277 | You can install Carthage with [Homebrew](http://brew.sh/) using the following command:
278 |
279 | ```bash
280 | $ brew update
281 | $ brew install carthage
282 | ```
283 |
284 | To integrate `DynamicColor` into your Xcode project using Carthage, specify it in your `Cartfile` file:
285 |
286 | ```ogdl
287 | github "yannickl/DynamicColor" >= 5.0.0
288 | ```
289 |
290 | #### Swift Package Manager
291 | You can use [The Swift Package Manager](https://swift.org/package-manager) to install `DynamicColor` by adding the proper description to your `Package.swift` file:
292 | ```swift
293 | import PackageDescription
294 |
295 | let package = Package(
296 | name: "YOUR_PROJECT_NAME",
297 | targets: [],
298 | dependencies: [
299 | .package(url: "https://github.com/yannickl/DynamicColor.git", from: "5.0.0")
300 | ]
301 | )
302 | ```
303 |
304 | Note that the [Swift Package Manager](https://swift.org/package-manager) is still in early design and development, for more information checkout its [GitHub Page](https://github.com/apple/swift-package-manager).
305 |
306 | #### Manually
307 |
308 | [Download](https://github.com/YannickL/DynamicColor/archive/master.zip) the project and copy the `DynamicColor` folder into your project to use it in.
309 |
310 | ## Contribution
311 |
312 | Contributions are welcomed and encouraged *♡*.
313 |
314 | ## Contact
315 |
316 | Yannick Loriot
317 | - [https://21.co/yannickl/](https://21.co/yannickl/)
318 | - [https://twitter.com/yannickloriot](https://twitter.com/yannickloriot)
319 |
320 | ## License (MIT)
321 |
322 | Copyright (c) 2015-present - Yannick Loriot
323 |
324 | Permission is hereby granted, free of charge, to any person obtaining a copy
325 | of this software and associated documentation files (the "Software"), to deal
326 | in the Software without restriction, including without limitation the rights
327 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
328 | copies of the Software, and to permit persons to whom the Software is
329 | furnished to do so, subject to the following conditions:
330 |
331 | The above copyright notice and this permission notice shall be included in
332 | all copies or substantial portions of the Software.
333 |
334 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
335 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
336 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
337 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
338 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
339 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
340 | THE SOFTWARE.
341 |
--------------------------------------------------------------------------------
/Examples/iOSExample/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------