125 lines
4.2 KiB
Swift
125 lines
4.2 KiB
Swift
|
|
import Foundation
|
|||
|
|
|
|||
|
|
/// Monotone cubic (Fritsch–Carlson) tone curve. Monotonic control points
|
|||
|
|
/// guarantee monotonic output — no spline overshoot, safe for grading.
|
|||
|
|
public struct Curve: Equatable, Sendable, Codable {
|
|||
|
|
public struct Point: Equatable, Sendable, Codable {
|
|||
|
|
public var x: Float
|
|||
|
|
public var y: Float
|
|||
|
|
public init(x: Float, y: Float) {
|
|||
|
|
self.x = x
|
|||
|
|
self.y = y
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public private(set) var points: [Point]
|
|||
|
|
/// Fritsch–Carlson tangents, one per point.
|
|||
|
|
private var tangents: [Float]
|
|||
|
|
|
|||
|
|
public init(points: [Point]) {
|
|||
|
|
precondition(points.count >= 2, "curve needs >= 2 points")
|
|||
|
|
let sorted = points.sorted { $0.x < $1.x }
|
|||
|
|
self.points = sorted
|
|||
|
|
self.tangents = Self.computeTangents(sorted)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static let identity = Curve(points: [.init(x: 0, y: 0), .init(x: 1, y: 1)])
|
|||
|
|
|
|||
|
|
public var isIdentity: Bool { self == .identity }
|
|||
|
|
|
|||
|
|
private static func computeTangents(_ pts: [Point]) -> [Float] {
|
|||
|
|
let n = pts.count
|
|||
|
|
// Secant slopes.
|
|||
|
|
var delta = [Float](repeating: 0, count: n - 1)
|
|||
|
|
for i in 0..<(n - 1) {
|
|||
|
|
let dx = pts[i + 1].x - pts[i].x
|
|||
|
|
delta[i] = dx == 0 ? 0 : (pts[i + 1].y - pts[i].y) / dx
|
|||
|
|
}
|
|||
|
|
var m = [Float](repeating: 0, count: n)
|
|||
|
|
m[0] = delta[0]
|
|||
|
|
m[n - 1] = delta[n - 2]
|
|||
|
|
for i in 1..<(n - 1) {
|
|||
|
|
// Zero tangent at local extrema / flats prevents overshoot.
|
|||
|
|
m[i] = (delta[i - 1] * delta[i] <= 0) ? 0 : (delta[i - 1] + delta[i]) / 2
|
|||
|
|
}
|
|||
|
|
// Fritsch–Carlson limiter.
|
|||
|
|
for i in 0..<(n - 1) {
|
|||
|
|
if delta[i] == 0 {
|
|||
|
|
m[i] = 0
|
|||
|
|
m[i + 1] = 0
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
let a = m[i] / delta[i]
|
|||
|
|
let b = m[i + 1] / delta[i]
|
|||
|
|
let s = a * a + b * b
|
|||
|
|
if s > 9 {
|
|||
|
|
let tau = 3 / sqrt(s)
|
|||
|
|
m[i] = tau * a * delta[i]
|
|||
|
|
m[i + 1] = tau * b * delta[i]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return m
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public func evaluate(_ x: Float) -> Float {
|
|||
|
|
// Endpoint pinning.
|
|||
|
|
if x <= points[0].x { return points[0].y }
|
|||
|
|
if x >= points[points.count - 1].x { return points[points.count - 1].y }
|
|||
|
|
// Find segment (linear scan; point counts are tiny).
|
|||
|
|
var i = 0
|
|||
|
|
while i < points.count - 2 && x > points[i + 1].x { i += 1 }
|
|||
|
|
let p0 = points[i], p1 = points[i + 1]
|
|||
|
|
let h = p1.x - p0.x
|
|||
|
|
if h == 0 { return p0.y }
|
|||
|
|
let t = (x - p0.x) / h
|
|||
|
|
let t2 = t * t
|
|||
|
|
let t3 = t2 * t
|
|||
|
|
// Cubic Hermite basis.
|
|||
|
|
let h00 = 2 * t3 - 3 * t2 + 1
|
|||
|
|
let h10 = t3 - 2 * t2 + t
|
|||
|
|
let h01 = -2 * t3 + 3 * t2
|
|||
|
|
let h11 = t3 - t2
|
|||
|
|
return h00 * p0.y + h10 * h * tangents[i] + h01 * p1.y + h11 * h * tangents[i + 1]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Codable: encode points only, rebuild tangents on decode.
|
|||
|
|
enum CodingKeys: String, CodingKey { case points }
|
|||
|
|
|
|||
|
|
public init(from decoder: Decoder) throws {
|
|||
|
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|||
|
|
let pts = try c.decode([Point].self, forKey: .points)
|
|||
|
|
self.init(points: pts)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public func encode(to encoder: Encoder) throws {
|
|||
|
|
var c = encoder.container(keyedBy: CodingKeys.self)
|
|||
|
|
try c.encode(points, forKey: .points)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static func == (l: Curve, r: Curve) -> Bool { l.points == r.points }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Master + per-channel curves. Master first, then channel curve.
|
|||
|
|
public struct CurveSet: Equatable, Sendable, Codable {
|
|||
|
|
public var master: Curve
|
|||
|
|
public var red: Curve
|
|||
|
|
public var green: Curve
|
|||
|
|
public var blue: Curve
|
|||
|
|
|
|||
|
|
public init(master: Curve = .identity, red: Curve = .identity, green: Curve = .identity, blue: Curve = .identity) {
|
|||
|
|
self.master = master
|
|||
|
|
self.red = red
|
|||
|
|
self.green = green
|
|||
|
|
self.blue = blue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static let identity = CurveSet()
|
|||
|
|
|
|||
|
|
public var isIdentity: Bool { self == .identity }
|
|||
|
|
|
|||
|
|
public func apply(_ rgb: SIMD3<Float>) -> SIMD3<Float> {
|
|||
|
|
let m = SIMD3(master.evaluate(rgb.x), master.evaluate(rgb.y), master.evaluate(rgb.z))
|
|||
|
|
return SIMD3(red.evaluate(m.x), green.evaluate(m.y), blue.evaluate(m.z))
|
|||
|
|
}
|
|||
|
|
}
|