import Foundation /// ASC CDL primary correction: out = clamp0(in * slope + offset)^power, then saturation. /// Saturation uses Rec709 luma weights per ASC CDL 1.2. public struct CDL: Equatable, Sendable { public var slope: SIMD3 public var offset: SIMD3 public var power: SIMD3 public var saturation: Float public init(slope: SIMD3, offset: SIMD3, power: SIMD3, saturation: Float) { self.slope = slope self.offset = offset self.power = power self.saturation = saturation } public static let identity = CDL(slope: .one, offset: .zero, power: .one, saturation: 1) public var isIdentity: Bool { self == .identity } /// Rec709 luma weights (ASC CDL saturation spec). static let lumaWeights = SIMD3(0.2126, 0.7152, 0.0722) public func apply(_ rgb: SIMD3) -> SIMD3 { // SOP, clamped to >=0 before power for determinism (ASC pre-power clamp). let sop = rgb * slope + offset let zero = SIMD3.zero let clamped = sop.replacing(with: zero, where: sop .< zero) var out = SIMD3( pow(clamped.x, power.x), pow(clamped.y, power.y), pow(clamped.z, power.z)) // Saturation about Rec709 luma. if saturation != 1 { let luma = (out * Self.lumaWeights).sum() out = SIMD3(repeating: luma) + (out - SIMD3(repeating: luma)) * saturation } return out } } extension SIMD3 where Scalar == Float { public static var one: SIMD3 { SIMD3(1, 1, 1) } }