diff --git a/Example/Example.playground/Pages/iOS.xcplaygroundpage/Contents.swift b/Example/Example.playground/Pages/iOS.xcplaygroundpage/Contents.swift index cb4cae2..f424ba8 100644 --- a/Example/Example.playground/Pages/iOS.xcplaygroundpage/Contents.swift +++ b/Example/Example.playground/Pages/iOS.xcplaygroundpage/Contents.swift @@ -44,6 +44,38 @@ let hsbColors = SmoothGradientGenerator() precision: .high ) +_ = SmoothGradientGenerator().generate( + from: LCHColor(l: 1, c: 1, h: 1, alpha: 1), + to: LCHColor(l: 1, c: 1, h: 1, alpha: 1), + precision: .high +) + +_ = SmoothGradientGenerator().generate( + from: HSLColor(h: 1, s: 1, l: 1, alpha: 1), + to: HSLColor(h: 1, s: 1, l: 1, alpha: 1), + precision: .high +) + +_ = SmoothGradientGenerator().generate( + from: HSBColor(h: 1, s: 1, b: 1, alpha: 1), + to: HSBColor(h: 1, s: 1, b: 1, alpha: 1), + precision: .high +) + +_ = SmoothGradientGenerator().generate( + from: XYZColor(x: 1, y: 1, z: 1, alpha: 1), + to: XYZColor(x: 1, y: 1, z: 1, alpha: 1), + interpolation: .hsb, + precision: .high +) + +_ = SmoothGradientGenerator().generate( + from: LABColor(l: 1, a: 1, b: 1, alpha: 1), + to: LABColor(l: 1, a: 1, b: 1, alpha: 1), + interpolation: .hsb, + precision: .high +) + struct ContentView: View { var body: some View { VStack { diff --git a/Example/Example.playground/Pages/macOS.xcplaygroundpage/Contents.swift b/Example/Example.playground/Pages/macOS.xcplaygroundpage/Contents.swift index a51e154..925e046 100644 --- a/Example/Example.playground/Pages/macOS.xcplaygroundpage/Contents.swift +++ b/Example/Example.playground/Pages/macOS.xcplaygroundpage/Contents.swift @@ -44,6 +44,38 @@ let hsbColors = SmoothGradientGenerator() precision: .high ) +_ = SmoothGradientGenerator().generate( + from: LCHColor(l: 1, c: 1, h: 1, alpha: 1), + to: LCHColor(l: 1, c: 1, h: 1, alpha: 1), + precision: .high +) + +_ = SmoothGradientGenerator().generate( + from: HSLColor(h: 1, s: 1, l: 1, alpha: 1), + to: HSLColor(h: 1, s: 1, l: 1, alpha: 1), + precision: .high +) + +_ = SmoothGradientGenerator().generate( + from: HSBColor(h: 1, s: 1, b: 1, alpha: 1), + to: HSBColor(h: 1, s: 1, b: 1, alpha: 1), + precision: .high +) + +_ = SmoothGradientGenerator().generate( + from: XYZColor(x: 1, y: 1, z: 1, alpha: 1), + to: XYZColor(x: 1, y: 1, z: 1, alpha: 1), + interpolation: .hsb, + precision: .high +) + +_ = SmoothGradientGenerator().generate( + from: LABColor(l: 1, a: 1, b: 1, alpha: 1), + to: LABColor(l: 1, a: 1, b: 1, alpha: 1), + interpolation: .hsb, + precision: .high +) + struct ContentView: View { var body: some View { VStack { diff --git a/Sources/SmoothGradient/ColorSpace.swift b/Sources/SmoothGradient/ColorSpace.swift index 91cb2c5..b232f0c 100644 --- a/Sources/SmoothGradient/ColorSpace.swift +++ b/Sources/SmoothGradient/ColorSpace.swift @@ -152,26 +152,17 @@ public struct RGBColor { return HSBColor(h: h * 360, s: s * 100, b: v * 100, alpha: alpha) } - - func lerp(_ other: RGBColor, t: Double) -> RGBColor { - return RGBColor( - r: r + (other.r - r) * t, - g: g + (other.g - g) * t, - b: b + (other.b - b) * t, - alpha: alpha + (other.alpha - alpha) * t - ) - } } // MARK: - XYZ -struct XYZColor { - let x: Double // 0..0.95047 - let y: Double // 0..1 - let z: Double // 0..1.08883 - let alpha: Double // 0..1 +public struct XYZColor { + public let x: Double // 0..0.95047 + public let y: Double // 0..1 + public let z: Double // 0..1.08883 + public let alpha: Double // 0..1 - init(x: Double, y: Double, z: Double, alpha: Double) { + public init(x: Double, y: Double, z: Double, alpha: Double) { self.x = x self.y = y self.z = z @@ -217,13 +208,13 @@ struct XYZColor { // MARK: - LAB -struct LABColor { - let l: Double // 0..100 - let a: Double // -128..128 - let b: Double // -128..128 - let alpha: Double // 0..1 +public struct LABColor { + public let l: Double // 0..100 + public let a: Double // -128..128 + public let b: Double // -128..128 + public let alpha: Double // 0..1 - init(l: Double, a: Double, b: Double, alpha: Double) { + public init(l: Double, a: Double, b: Double, alpha: Double) { self.l = l self.a = a self.b = b @@ -261,13 +252,13 @@ struct LABColor { // MARK: - LCH -struct LCHColor { - let l: Double // 0..100 - let c: Double // 0..128 - let h: Double // 0..360 - let alpha: Double // 0..1 +public struct LCHColor { + public let l: Double // 0..100 + public let c: Double // 0..128 + public let h: Double // 0..360 + public let alpha: Double // 0..1 - init(l: Double, c: Double, h: Double, alpha: Double) { + public init(l: Double, c: Double, h: Double, alpha: Double) { self.l = l self.c = c self.h = h @@ -292,11 +283,18 @@ struct LCHColor { // MARK: - HSL -struct HSLColor { - let h: Double // 0..360 - let s: Double // 0..100 - let l: Double // 0..100 - let alpha: Double // 0..1 +public struct HSLColor { + public let h: Double // 0..360 + public let s: Double // 0..100 + public let l: Double // 0..100 + public let alpha: Double // 0..1 + + public init(h: Double, s: Double, l: Double, alpha: Double) { + self.h = h + self.s = s + self.l = l + self.alpha = alpha + } /// Converts HSL to RGB (https://en.wikipedia.org/wiki/HSL_and_HSV) func toRGB() -> RGBColor { @@ -331,11 +329,18 @@ struct HSLColor { // MARK: - HSB / HSV -struct HSBColor { - let h: Double // 0..360 - let s: Double // 0..100 - let b: Double // 0..100 - let alpha: Double // 0..1 +public struct HSBColor { + public let h: Double // 0..360 + public let s: Double // 0..100 + public let b: Double // 0..100 + public let alpha: Double // 0..1 + + public init(h: Double, s: Double, b: Double, alpha: Double) { + self.h = h + self.s = s + self.b = b + self.alpha = alpha + } /// Converts HSB to RGB (https://en.wikipedia.org/wiki/HSL_and_HSV) func toRGB() -> RGBColor { diff --git a/Sources/SmoothGradient/SmoothGradientGenerator.swift b/Sources/SmoothGradient/SmoothGradientGenerator.swift index 4195c9f..aebd6f4 100644 --- a/Sources/SmoothGradient/SmoothGradientGenerator.swift +++ b/Sources/SmoothGradient/SmoothGradientGenerator.swift @@ -14,6 +14,16 @@ public enum SmoothGradientPrecision: Int { case high = 9 } +protocol RGBColorConvertible { + func toRGB() -> RGBColor +} + +extension XYZColor: RGBColorConvertible {} +extension LABColor: RGBColorConvertible {} +extension LCHColor: RGBColorConvertible {} +extension HSLColor: RGBColorConvertible {} +extension HSBColor: RGBColorConvertible {} + /// Generate a set of intermediate colors to make gradient of 2 colors buttery-smooth. public struct SmoothGradientGenerator { public init() {} @@ -28,19 +38,14 @@ public struct SmoothGradientGenerator { let count = precision.rawValue return interpolate(from: from, to: to, count: count, interpolation: interpolation) } -} -#if canImport(UIKit) -import UIKit - -extension SmoothGradientGenerator { - /// Generate gradient from `UIColor`s. - public func generate( - from: UIColor, - to: UIColor, - interpolation: SmoothGradientInterpolation = .hcl, - precision: SmoothGradientPrecision = .medium - ) -> [UIColor] { + /// Generate gradient from any type that convertible from/to RGBColor + func generateAsRGBColor( + from: T, + to: T, + interpolation: SmoothGradientInterpolation, + precision: SmoothGradientPrecision + ) -> [RGBColor] { let rgb_from = from.toRGB() let rgb_to = to.toRGB() return generate( @@ -49,18 +54,140 @@ extension SmoothGradientGenerator { interpolation: interpolation, precision: precision ) - .map { - UIColor( - red: CGFloat($0.r), - green: CGFloat($0.g), - blue: CGFloat($0.b), - alpha: CGFloat($0.alpha) + } + + /// Generate gradient from `LCHColor`s. Default to interpolate in LCH. + /// + /// When interpolating in color space other than LCH, colors will be converted to RGBColor for + /// computation. + public func generate( + from: LCHColor, + to: LCHColor, + interpolation: SmoothGradientInterpolation = .hcl, + precision: SmoothGradientPrecision = .medium + ) -> [LCHColor] { + switch interpolation { + case .hcl: + let count = precision.rawValue + return interpolate(from: from, to: to, count: count) + default: + return generateAsRGBColor( + from: from, + to: to, + interpolation: interpolation, + precision: precision ) + .map { $0.toLCH() } } } + + /// Generate gradient from `HSLColor`s. Default to interpolate in HSL. + /// + /// When interpolating in color space other than HSL, colors will be converted to RGBColor for + /// computation. + public func generate( + from: HSLColor, + to: HSLColor, + interpolation: SmoothGradientInterpolation = .hsl, + precision: SmoothGradientPrecision = .medium + ) -> [HSLColor] { + switch interpolation { + case .hsl: + let count = precision.rawValue + return interpolate(from: from, to: to, count: count) + default: + return generateAsRGBColor( + from: from, + to: to, + interpolation: interpolation, + precision: precision + ) + .map { $0.toHSL() } + } + } + + /// Generate gradient from `HSBColor`s. Default to interpolate in HSB. + /// + /// When interpolating in color space other than HSB, colors will be converted to RGBColor for + /// computation. + public func generate( + from: HSBColor, + to: HSBColor, + interpolation: SmoothGradientInterpolation = .hsb, + precision: SmoothGradientPrecision = .medium + ) -> [HSBColor] { + switch interpolation { + case .hsb: + let count = precision.rawValue + return interpolate(from: from, to: to, count: count) + default: + return generateAsRGBColor( + from: from, + to: to, + interpolation: interpolation, + precision: precision + ) + .map { $0.toHSB() } + } + } + + /// Generate gradient from `LABColor`s. + public func generate( + from: LABColor, + to: LABColor, + interpolation: SmoothGradientInterpolation = .hcl, + precision: SmoothGradientPrecision = .medium + ) -> [LABColor] { + return generateAsRGBColor( + from: from, + to: to, + interpolation: interpolation, + precision: precision + ) + .map { $0.toLAB() } + } + + /// Generate gradient from `XYZColor`s. + public func generate( + from: XYZColor, + to: XYZColor, + interpolation: SmoothGradientInterpolation = .hcl, + precision: SmoothGradientPrecision = .medium + ) -> [XYZColor] { + return generateAsRGBColor( + from: from, + to: to, + interpolation: interpolation, + precision: precision + ) + .map { $0.toXYZ() } + } } -extension UIColor { +#if canImport(UIKit) +import UIKit + +public extension SmoothGradientGenerator { + /// Generate gradient from `UIColor`s. + func generate( + from: UIColor, + to: UIColor, + interpolation: SmoothGradientInterpolation = .hcl, + precision: SmoothGradientPrecision = .medium + ) -> [UIColor] { + generateAsRGBColor(from: from, to: to, interpolation: interpolation, precision: precision) + .map { + UIColor( + red: CGFloat($0.r), + green: CGFloat($0.g), + blue: CGFloat($0.b), + alpha: CGFloat($0.alpha) + ) + } + } +} + +extension UIColor: RGBColorConvertible { func toRGB() -> RGBColor { var r: CGFloat = 0 var g: CGFloat = 0 @@ -75,34 +202,27 @@ extension UIColor { #elseif canImport(AppKit) import AppKit -extension SmoothGradientGenerator { +public extension SmoothGradientGenerator { /// Generate gradient from `NSColor`s. - public func generate( + func generate( from: NSColor, to: NSColor, interpolation: SmoothGradientInterpolation = .hcl, precision: SmoothGradientPrecision = .medium ) -> [NSColor] { - let rgb_from = from.toRGB() - let rgb_to = to.toRGB() - return generate( - from: rgb_from, - to: rgb_to, - interpolation: interpolation, - precision: precision - ) - .map { - NSColor( - red: CGFloat($0.r), - green: CGFloat($0.g), - blue: CGFloat($0.b), - alpha: CGFloat($0.alpha) - ) - } + generateAsRGBColor(from: from, to: to, interpolation: interpolation, precision: precision) + .map { + NSColor( + red: CGFloat($0.r), + green: CGFloat($0.g), + blue: CGFloat($0.b), + alpha: CGFloat($0.alpha) + ) + } } } -extension NSColor { +extension NSColor: RGBColorConvertible { func toRGB() -> RGBColor { var r: CGFloat = 0 var g: CGFloat = 0 @@ -120,10 +240,10 @@ extension NSColor { import SwiftUI -extension SmoothGradientGenerator { +public extension SmoothGradientGenerator { /// Generate gradient from SwiftUI `Color`s. @available(iOS 14.0, OSX 11, tvOS 14, *) - public func generate( + func generate( from: Color, to: Color, colorSpace: Color.RGBColorSpace = .sRGB, @@ -176,42 +296,51 @@ func interpolate( case .hcl: let lch_from = from.toLCH() let lch_to = to.toLCH() - let l = interpolate(from: lch_from.l, to: lch_to.l, count: count) - let c = interpolate(from: lch_from.c, to: lch_to.c, count: count) - let h = interpolateCircular(from: lch_from.h, to: lch_to.h, count: count) - let alpha = interpolate(from: lch_from.alpha, to: lch_to.alpha, count: count) - - return zip(l, c, h, alpha) - .lazy - .map(LCHColor.init(l:c:h:alpha:)) + return interpolate(from: lch_from, to: lch_to, count: count) .map { $0.toRGB() } case .hsl: let hsl_from = from.toHSL() let hsl_to = to.toHSL() - let l = interpolate(from: hsl_from.l, to: hsl_to.l, count: count) - let s = interpolate(from: hsl_from.s, to: hsl_to.s, count: count) - let h = interpolateCircular(from: hsl_from.h, to: hsl_to.h, count: count) - let alpha = interpolate(from: hsl_from.alpha, to: hsl_to.alpha, count: count) - - return zip(h, s, l, alpha) - .lazy - .map(HSLColor.init(h:s:l:alpha:)) + return interpolate(from: hsl_from, to: hsl_to, count: count) .map { $0.toRGB() } case .hsb: let hsb_from = from.toHSB() let hsb_to = to.toHSB() - let b = interpolate(from: hsb_from.b, to: hsb_to.b, count: count) - let s = interpolate(from: hsb_from.s, to: hsb_to.s, count: count) - let h = interpolateCircular(from: hsb_from.h, to: hsb_to.h, count: count) - let alpha = interpolate(from: hsb_from.alpha, to: hsb_to.alpha, count: count) - - return zip(h, s, b, alpha) - .lazy - .map(HSBColor.init(h:s:b:alpha:)) + return interpolate(from: hsb_from, to: hsb_to, count: count) .map { $0.toRGB() } } } +func interpolate(from: LCHColor, to: LCHColor, count: Int) -> [LCHColor] { + let l = interpolate(from: from.l, to: to.l, count: count) + let c = interpolate(from: from.c, to: to.c, count: count) + let h = interpolateCircular(from: from.h, to: to.h, count: count) + let alpha = interpolate(from: from.alpha, to: to.alpha, count: count) + + return zip(l, c, h, alpha) + .map(LCHColor.init(l:c:h:alpha:)) +} + +func interpolate(from: HSLColor, to: HSLColor, count: Int) -> [HSLColor] { + let l = interpolate(from: from.l, to: to.l, count: count) + let s = interpolate(from: from.s, to: to.s, count: count) + let h = interpolateCircular(from: from.h, to: to.h, count: count) + let alpha = interpolate(from: from.alpha, to: to.alpha, count: count) + + return zip(h, s, l, alpha) + .map(HSLColor.init(h:s:l:alpha:)) +} + +func interpolate(from: HSBColor, to: HSBColor, count: Int) -> [HSBColor] { + let b = interpolate(from: from.b, to: to.b, count: count) + let s = interpolate(from: from.s, to: to.s, count: count) + let h = interpolateCircular(from: from.h, to: to.h, count: count) + let alpha = interpolate(from: from.alpha, to: to.alpha, count: count) + + return zip(h, s, b, alpha) + .map(HSBColor.init(h:s:b:alpha:)) +} + /// Interpolate values linearly. func interpolate(from: Double, to: Double, count: Int) -> [Double] { guard count > 0 else { return [from, to] } diff --git a/Tests/SmoothGradientTests/SmoothGradientTests.swift b/Tests/SmoothGradientTests/SmoothGradientTests.swift index c01edf1..f473d09 100644 --- a/Tests/SmoothGradientTests/SmoothGradientTests.swift +++ b/Tests/SmoothGradientTests/SmoothGradientTests.swift @@ -332,7 +332,7 @@ final class SmoothGradientTests: XCTestCase { extension SmoothGradient.RGBColor: Equatable { public static func == (lhs: SmoothGradient.RGBColor, rhs: SmoothGradient.RGBColor) -> Bool { - func closeEnough(l: CGFloat, r: CGFloat, v: CGFloat) -> Bool { + func closeEnough(l: Double, r: Double, v: Double) -> Bool { abs(l - r) < v } return closeEnough(l: lhs.r, r: rhs.r, v: 0.01) @@ -344,7 +344,7 @@ extension SmoothGradient.RGBColor: Equatable { extension LCHColor: Equatable { public static func == (lhs: LCHColor, rhs: LCHColor) -> Bool { - func closeEnough(l: CGFloat, r: CGFloat, v: CGFloat) -> Bool { + func closeEnough(l: Double, r: Double, v: Double) -> Bool { abs(l - r) < v } return closeEnough(l: lhs.l, r: rhs.l, v: 0.5) @@ -356,7 +356,7 @@ extension LCHColor: Equatable { extension HSLColor: Equatable { public static func == (lhs: HSLColor, rhs: HSLColor) -> Bool { - func closeEnough(l: CGFloat, r: CGFloat, d: CGFloat) -> Bool { + func closeEnough(l: Double, r: Double, d: Double) -> Bool { abs(l - r) < d } return closeEnough(l: lhs.l, r: rhs.l, d: 0.5) @@ -368,7 +368,7 @@ extension HSLColor: Equatable { extension HSBColor: Equatable { public static func == (lhs: HSBColor, rhs: HSBColor) -> Bool { - func closeEnough(l: CGFloat, r: CGFloat, d: CGFloat) -> Bool { + func closeEnough(l: Double, r: Double, d: Double) -> Bool { abs(l - r) < d } return closeEnough(l: lhs.h, r: rhs.h, d: 0.5)