Skip to content

Instantly share code, notes, and snippets.

@mortenjust
Last active June 28, 2023 12:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mortenjust/8c2b2afcdbdd8b660d663d6d0afe5e1b to your computer and use it in GitHub Desktop.
Save mortenjust/8c2b2afcdbdd8b660d663d6d0afe5e1b to your computer and use it in GitHub Desktop.
Interpolate colors with easing functions
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