Last active
September 30, 2024 20:20
-
-
Save mortenjust/8c2b2afcdbdd8b660d663d6d0afe5e1b to your computer and use it in GitHub Desktop.
Interpolate colors with easing functions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import AppKit | |
import SwiftUI | |
/// Based on https://github.com/ipedro/SmoothGradientView | |
/// See documentation on `interpolate:` | |
/// | |
/// How to use: | |
/// | |
/// You can interpolate any array of `CGColor`, `NSColor` or `Color`to any array of any of the 3 supported types. | |
/// | |
/// Let's say you want to interpolate an array of `CGColor` to an array of `NSColor` | |
/// | |
/// ``` | |
/// myNSColors = myCGcolors.interpolate(easing: .cubicInOut) | |
/// ``` | |
/// | |
/// CAGradientLayer's `colors` property is `[Any]`, so you'll have to specialize the interpoloation function manually. | |
/// | |
/// ``` | |
/// gradientlayer.colors = swiftUIColors.interpolate(returning: CGColor.self, easing: easing) | |
/// ``` | |
/// | |
/// | |
/// MIT / MIT https://github.com/ipedro/SmoothGradientView/blob/main/LICENSE | |
// MARK: Extensions of arrays of colors | |
protocol ExpressibleByColor { | |
static func fromNSColor(_ nsColor: NSColor) -> Self | |
func toNSColor() -> NSColor | |
} | |
extension CGColor: ExpressibleByColor { | |
static func fromNSColor(_ nsColor: NSColor) -> Self { | |
let cgcolor = CGColor(red: CGFloat(nsColor.redComponent), | |
green: CGFloat(nsColor.greenComponent), | |
blue: CGFloat(nsColor.blueComponent), | |
alpha: CGFloat(nsColor.alphaComponent)) | |
return cgcolor as! Self | |
} | |
func toNSColor() -> NSColor { | |
return NSColor(cgColor: self)! | |
} | |
} | |
extension NSColor: ExpressibleByColor { | |
static func fromNSColor(_ nsColor: NSColor) -> Self { | |
return nsColor as! Self | |
} | |
func toNSColor() -> Self { | |
return self | |
} | |
} | |
extension Color: ExpressibleByColor { | |
static func fromNSColor(_ nsColor: NSColor) -> Color { | |
return Color(nsColor) | |
} | |
func toNSColor() -> NSColor { | |
return NSColor(self) | |
} | |
} | |
extension Array where Element: ExpressibleByColor { | |
/// Interpolate an array of colors with easing | |
/// - Parameters: | |
/// - returning: The color type you want to return. Optional. | |
/// - easing: Easing function | |
/// - steps: Color steps | |
/// - Returns: An array of colors matching the left-hand side of your assignment. If you're assigning to an array of Any, set the `returning` parameter. | |
func interpolate<T: ExpressibleByColor>(returning:T.Type? = nil, easing: ColorInterpolator.Ease, steps: UInt = 100) -> [T] { | |
let nsColors = ColorInterpolator( | |
colors: self.map { $0.toNSColor() }, | |
ease: easing, | |
steps: steps | |
).interpolate() | |
return nsColors.map { T.fromNSColor($0) } | |
} | |
} | |
// MARK: Interpolator and helpers | |
public struct ColorInterpolator: Hashable { | |
var colors: [NSColor] | |
var ease: Ease | |
var steps: UInt | |
public init(colors: [NSColor], ease: Ease, steps: UInt) { | |
self.colors = colors | |
self.ease = ease | |
self.steps = steps | |
} | |
public func interpolate() -> [NSColor] { | |
colors | |
.enumerated() | |
.map { offset, color -> [NSColor] in | |
guard | |
offset < colors.count - 1, | |
steps > .zero | |
else { | |
return [color] | |
} | |
let nextColor = colors[offset + 1] | |
return steps(from: color, to: nextColor) | |
} | |
.flatMap { $0 } | |
} | |
private func steps(from c1: NSColor, to c2: NSColor) -> [NSColor] { | |
(0...steps).map { offset in | |
let ratio = CGFloat(offset) / CGFloat(steps + 1) | |
let easing = ease.calculate(ratio) | |
return c1.mix(with: c2, ratio: easing) | |
} | |
} | |
} | |
extension ColorInterpolator { | |
public struct ColorMixer: Hashable { | |
var c1: NSColor | |
var c2: NSColor | |
var ratio: CGFloat | |
public init(c1: NSColor, c2: NSColor, ratio: CGFloat) { | |
self.c1 = c1 | |
self.c2 = c2 | |
self.ratio = ratio | |
} | |
public func mix() -> NSColor { | |
var r1: CGFloat = 0, g1: CGFloat = 0, b1: CGFloat = 0, a1: CGFloat = 0 | |
var r2: CGFloat = 0, g2: CGFloat = 0, b2: CGFloat = 0, a2: CGFloat = 0 | |
if 4 == c1.cgColor.components?.count { | |
c1.getRed(&r1, green: &g1, blue: &b1, alpha: &a1) | |
} else { | |
c1.getWhite(&r1, alpha: &a1) | |
b1 = r1 | |
g1 = r1 | |
} | |
if 4 == c2.cgColor.components?.count { | |
c2.getRed(&r2, green: &g2, blue: &b2, alpha: &a2) | |
} else { | |
c2.getWhite(&r2, alpha: &a2) | |
b2 = r2 | |
g2 = r2 | |
} | |
let r = bezierCurve(t: ratio, p0: r1, p1: r2) | |
let g = bezierCurve(t: ratio, p0: g1, p1: g2) | |
let b = bezierCurve(t: ratio, p0: b1, p1: b2) | |
let a = bezierCurve(t: ratio, p0: a1, p1: a2) | |
return NSColor(red: r, green: g, blue: b, alpha: a) | |
} | |
private func bezierCurve(t: CGFloat, p0: CGFloat, p1: CGFloat) -> CGFloat { | |
(1.0 - t) * p0 + t * p1 | |
} | |
} | |
/// A concrete easing function implementation. | |
public struct Ease: EasingFunction, Hashable { | |
typealias LerpFunction = (_ t: CGFloat, _ b: CGFloat, _ c: CGFloat, _ d: CGFloat) -> CGFloat | |
private let lerp: LerpFunction | |
private let identifier = UUID() | |
init(_ lerp: @escaping LerpFunction) { | |
self.lerp = lerp | |
} | |
public static func == (lhs: Ease, rhs: Ease) -> Bool { | |
lhs.identifier == rhs.identifier | |
} | |
public func hash(into hasher: inout Hasher) { | |
hasher.combine(identifier) | |
} | |
func calculate(_ t: CGFloat, _ b: CGFloat = 0, _ c: CGFloat = 1, _ d: CGFloat = 1) -> CGFloat { | |
return lerp(t, b, c, d) | |
} | |
} | |
} | |
/// Easing functions specify the rate of change of a parameter over time. | |
protocol EasingFunction { | |
func calculate(_ t: CGFloat, _ b: CGFloat, _ c: CGFloat, _ d: CGFloat) -> CGFloat | |
} | |
// MARK: - CaseIterable | |
extension ColorInterpolator.Ease: CaseIterable { | |
public static var allCases: [ColorInterpolator.Ease] { [ | |
.easeInBack, | |
.easeInOutBack, | |
.easeOutBack, | |
.easeInBounce, | |
.easeInOutBounce, | |
.easeOutBounce, | |
.easeInCirc, | |
.easeInOutCirc, | |
.easeOutCirc, | |
.easeInCubic, | |
.easeInOutCubic, | |
.easeOutCubic, | |
.easeInElastic, | |
.easeInOutElastic, | |
.easeOutElastic, | |
.easeInExpo, | |
.easeInOutExpo, | |
.easeOutExpo, | |
.easeInQuad, | |
.easeInOutQuad, | |
.easeOutQuad, | |
.easeInQuart, | |
.easeInOutQuart, | |
.easeOutQuart, | |
.easeInQuint, | |
.easeInOutQuint, | |
.easeOutQuint, | |
.easeInSine, | |
.easeInOutSine, | |
.easeOutSine, | |
.linear] | |
} | |
} | |
// MARK: - CustomDebugStringConvertible | |
extension ColorInterpolator.Ease: CustomDebugStringConvertible { | |
public var debugDescription: String { | |
switch self { | |
case .linear: return "Linear" | |
case .easeInBack: return "Back Ease In" | |
case .easeInOutBack: return "Back Ease In/Out" | |
case .easeOutBack: return "Back Ease Out" | |
case .easeInBounce: return "Bounce Ease In" | |
case .easeInOutBounce: return "Bounce Ease In/Out" | |
case .easeOutBounce: return "Bounce Ease Out" | |
case .easeInCirc: return "Circular Ease In" | |
case .easeInOutCirc: return "Circular Ease In/Out" | |
case .easeOutCirc: return "Circular Ease Out" | |
case .easeInCubic: return "Cubic Ease In" | |
case .easeInOutCubic: return "Cubic Ease In/Out" | |
case .easeOutCubic: return "Cubic Ease Out" | |
case .easeInElastic: return "Elastic Ease In" | |
case .easeInOutElastic: return "Elastic Ease In/Out" | |
case .easeOutElastic: return "Elastic Ease Out" | |
case .easeInExpo: return "Expo Ease In" | |
case .easeInOutExpo: return "Expo Ease In/Out" | |
case .easeOutExpo: return "Expo Ease Out" | |
case .easeInQuad: return "Quadratic Ease In" | |
case .easeInOutQuad: return "Quadratic Ease In/Out" | |
case .easeOutQuad: return "Quadratic Ease Out" | |
case .easeInQuart: return "Quartic Ease In" | |
case .easeInOutQuart: return "Quartic Ease In/Out" | |
case .easeOutQuart: return "Quartic Ease Out" | |
case .easeInQuint: return "Quintic Ease In" | |
case .easeInOutQuint: return "Quintic Ease In/Out" | |
case .easeOutQuint: return "Quintic Ease Out" | |
case .easeInSine: return "Sine Ease In" | |
case .easeInOutSine: return "Sine Ease In/Out (Default)" | |
case .easeOutSine: return "Sine Ease Out" | |
default: return "Other" | |
} | |
} | |
} | |
// MARK: - Easing Functions | |
public extension ColorInterpolator.Ease { | |
// MARK: - Linear | |
/// This function averages values uniformly over time. | |
static let linear = ColorInterpolator.Ease { (t,b,c,d) -> CGFloat in | |
return c*(t/d)+b | |
} | |
// MARK: - Quadratic | |
/// [Reference](https://easings.net/#easeInQuad) | |
static let easeInQuad = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
let t = _t/d | |
return c*t*t + b | |
} | |
/// [Reference](https://easings.net/#easeOutQuad) | |
static let easeOutQuad = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
let t = _t/d | |
return -c * t*(t-2) + b | |
} | |
/// [Reference](https://easings.net/#easeInOutQuad) | |
static let easeInOutQuad = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
var t = _t/(d/2) | |
if t < 1 { | |
return c/2*t*t + b; | |
} | |
let t1 = t-1 | |
let t2 = t1-2 | |
return -c/2 * ((t1)*(t2) - 1) + b; | |
} | |
// MARK: - Cubic | |
/// [Reference](https://easings.net/#easeInCubic) | |
static let easeInCubic = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
let t = _t/d | |
return c*t*t*t + b | |
} | |
/// [Reference](https://easings.net/#easeOutCubic) | |
static let easeOutCubic = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
let t = _t/d-1 | |
return c*(t*t*t + 1) + b | |
} | |
/// [Reference](https://easings.net/#easeInOutCubic) | |
static let easeInOutCubic = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
var t = _t/(d/2) | |
if t < 1{ | |
return c/2*t*t*t + b; | |
} | |
t -= 2 | |
return c/2*(t*t*t + 2) + b; | |
} | |
// MARK: - Quartic | |
/// [Reference](https://easings.net/#easeInQuart) | |
static let easeInQuart = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
let t = _t/d | |
return c*t*t*t*t + b | |
} | |
/// [Reference](https://easings.net/#easeOutQuart) | |
static let easeOutQuart = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
let t = _t/d-1 | |
return -c * (t*t*t*t - 1) + b | |
} | |
/// [Reference](https://easings.net/#easeInOutQuart) | |
static let easeInOutQuart = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
var t = _t/(d/2) | |
if t < 1{ | |
return c/2*t*t*t*t + b; | |
} | |
t -= 2 | |
return -c/2 * (t*t*t*t - 2) + b; | |
} | |
// MARK: - Quintic | |
/// [Reference](https://easings.net/#easeInQuint) | |
static let easeInQuint = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
let t = _t/d | |
return c*t*t*t*t*t + b | |
} | |
/// [Reference](https://easings.net/#easeOutQuint) | |
static let easeOutQuint = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
let t = _t/d-1 | |
return c*(t*t*t*t*t + 1) + b | |
} | |
/// [Reference](https://easings.net/#easeInOutQuint) | |
static let easeInOutQuint = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
var t = _t/(d/2) | |
if t < 1 { | |
return c/2*t*t*t*t*t + b; | |
} | |
t -= 2 | |
return c/2*(t*t*t*t*t + 2) + b; | |
} | |
// MARK: - Back | |
/// [Reference](https://easings.net/#easeInBack) | |
static let easeInBack = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
let s: CGFloat = 1.70158 | |
let t = _t/d | |
return c*t*t*((s+1)*t - s) + b | |
} | |
/// [Reference](https://easings.net/#easeOutBack) | |
static let easeOutBack = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
let s: CGFloat = 1.70158 | |
let t = _t/d-1 | |
return c*(t*t*((s+1)*t + s) + 1) + b | |
} | |
/// [Reference](https://easings.net/#easeInOutBack) | |
static let easeInOutBack = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
var s: CGFloat = 1.70158 | |
var t = _t/(d/2) | |
if t < 1{ | |
s *= (1.525) | |
return c/2*(t*t*((s+1)*t - s)) + b; | |
} | |
s *= 1.525 | |
t -= 2 | |
return c/2*(t*t*((s+1)*t + s) + 2) + b; | |
} | |
// MARK: - Bounce | |
/// [Reference](https://easings.net/#easeInBounce) | |
static let easeInBounce = ColorInterpolator.Ease { (t,b,c,d) -> CGFloat in | |
return c - easeOutBounce.calculate(d-t, b, c, d) + b | |
} | |
/// [Reference](https://easings.net/#easeOutBounce) | |
static let easeOutBounce = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
var t = _t/d | |
if t < (1/2.75){ | |
return c*(7.5625*t*t) + b; | |
} else if t < (2/2.75) { | |
t -= 1.5/2.75 | |
return c*(7.5625*t*t + 0.75) + b; | |
} else if t < (2.5/2.75) { | |
t -= 2.25/2.75 | |
return c*(7.5625*t*t + 0.9375) + b; | |
} else { | |
t -= 2.625/2.75 | |
return c*(7.5625*t*t + 0.984375) + b; | |
} | |
} | |
/// [Reference](https://easings.net/#easeInOutBounce) | |
static let easeInOutBounce = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
let t = _t | |
if t < d/2 { | |
return easeInBounce.calculate(t*2, 0, c, d) * 0.5 + b | |
} | |
return easeOutBounce.calculate(t*2-d, 0, c, d) * 0.5 + c*0.5 + b | |
} | |
// MARK: - Circular | |
/// [Reference](https://easings.net/#easeInCirc) | |
static let easeInCirc = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
let t = _t/d | |
return -c * (sqrt(1 - t*t) - 1) + b | |
} | |
/// [Reference](https://easings.net/#easeOutCirc) | |
static let easeOutCirc = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
let t = _t/d-1 | |
return c * sqrt(1 - t*t) + b | |
} | |
/// [Reference](https://easings.net/#easeInOutCirc) | |
static let easeInOutCirc = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
var t = _t/(d/2) | |
if t < 1{ | |
return -c/2 * (sqrt(1 - t*t) - 1) + b; | |
} | |
t -= 2 | |
return c/2 * (sqrt(1 - t*t) + 1) + b; | |
} | |
// MARK: - Elastic | |
/// [Reference](https://easings.net/#easeInElastic) | |
static let easeInElastic = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
var t = _t | |
if t==0{ return b } | |
t/=d | |
if t==1{ return b+c } | |
let p = d * 0.3 | |
let a = c | |
let s = p/4 | |
t -= 1 | |
return -(a*pow(2,10*t) * sin( (t*d-s)*(2*CGFloat.pi)/p )) + b; | |
} | |
/// [Reference](https://easings.net/#easeOutElastic) | |
static let easeOutElastic = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
var t = _t | |
if t==0{ return b } | |
t/=d | |
if t==1{ return b+c} | |
let p = d * 0.3 | |
let a = c | |
let s = p/4 | |
return (a*pow(2,-10*t) * sin( (t*d-s)*(2*CGFloat.pi)/p ) + c + b); | |
} | |
/// [Reference](https://easings.net/#easeInOutElastic) | |
static let easeInOutElastic = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
var t = _t | |
if t==0{ return b} | |
t = t/(d/2) | |
if t==2{ return b+c } | |
let p = d * (0.3*1.5) | |
let a = c | |
let s = p/4 | |
if t < 1 { | |
t -= 1 | |
return -0.5*(a*pow(2,10*t) * sin((t*d-s)*(2*CGFloat.pi)/p )) + b; | |
} | |
t -= 1 | |
return a*pow(2,-10*t) * sin( (t*d-s)*(2*CGFloat.pi)/p )*0.5 + c + b; | |
} | |
// MARK: - Expo | |
/// [Reference](https://easings.net/#easeInExpo) | |
static let easeInExpo = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
return (_t==0) ? b : c * pow(2, 10 * (_t/d - 1)) + b | |
} | |
/// [Reference](https://easings.net/#easeOutExpo) | |
static let easeOutExpo = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
return (_t==d) ? b+c : c * (-pow(2, -10 * _t/d) + 1) + b | |
} | |
/// [Reference](https://easings.net/#easeInOutExpo) | |
static let easeInOutExpo = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
if _t==0{ return b } | |
if _t==d{ return b+c} | |
var t = _t/(d/2) | |
if t < 1{ | |
return c/2 * pow(2, 10 * (_t - 1)) + b; | |
} | |
let t1 = t-1 | |
return c/2 * (-pow(2, -10 * t1) + 2) + b; | |
} | |
// MARK: - Sine | |
/// [Reference](https://easings.net/#easeInSine) | |
static let easeInSine = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
return -c * cos(_t/d * (CGFloat.pi/2)) + c + b | |
} | |
/// [Reference](https://easings.net/#easeOutSine) | |
static let easeOutSine = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
return c * sin(_t/d * (CGFloat.pi/2)) + b | |
} | |
/// [Reference](https://easings.net/#easeInOutSine) | |
static let easeInOutSine = ColorInterpolator.Ease { (_t,b,c,d) -> CGFloat in | |
return -c/2 * (cos(CGFloat.pi*_t/d) - 1) + b | |
} | |
} | |
extension NSColor { | |
/// Mixes two colors with a ratio. | |
/// - Parameters: | |
/// - color: Another color to be mixed. | |
/// - ratio: A ratio between 0.0 - 1.0. | |
/// - Returns: The interpolated color. | |
func mix(with color: NSColor, ratio: CGFloat = 0.5) -> NSColor { | |
ColorInterpolator.ColorMixer(c1: self, c2: color, ratio: ratio).mix() | |
} | |
} | |
public extension CGPoint { | |
static let topLeft = CGPoint(x: 0.0, y: 0.0) | |
static let top = CGPoint(x: 0.5, y: 0.0) | |
static let topRight = CGPoint(x: 1.0, y: 0.0) | |
static let left = CGPoint(x: 0.0, y: 0.5) | |
static let center = CGPoint(x: 0.5, y: 0.5) | |
static let right = CGPoint(x: 1.0, y: 0.5) | |
static let bottomLeft = CGPoint(x: 0.0, y: 1.0) | |
static let bottom = CGPoint(x: 0.5, y: 1.0) | |
static let bottomRight = CGPoint(x: 1.0, y: 1.0) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment