namespace LindtLeerformPlugin.Services; /// /// Monotone cubic Hermite spline (Fritsch–Carlson). Five control points with /// fixed, evenly spaced X positions; only Y is editable. Output never overshoots /// the input range, so envelope curves stay inside [0, 1] when knots are. /// public static class SplineCurve { public const int KnotCount = 5; /// /// Evaluate the spline at parameter in [0, 1]. /// Knots are positioned at t = i / (knots.Length - 1). /// public static float Evaluate(float[]? knots, double t) { if (knots == null || knots.Length == 0) return 0f; if (knots.Length == 1) return knots[0]; if (t <= 0) return knots[0]; if (t >= 1) return knots[^1]; var n = knots.Length - 1; var pos = t * n; var i = (int)System.Math.Floor(pos); if (i >= n) return knots[n]; var localT = pos - i; // Compute secant slopes and Fritsch-Carlson tangents at the two surrounding knots only. var dxLeft = i > 0 ? (double)(knots[i] - knots[i - 1]) : 0; var dxMid = (double)(knots[i + 1] - knots[i]); var dxRight = i + 2 <= n ? (double)(knots[i + 2] - knots[i + 1]) : 0; var m0 = MonotoneTangent(dxLeft, dxMid, hasLeft: i > 0, hasRight: true); var m1 = MonotoneTangent(dxMid, dxRight, hasLeft: true, hasRight: i + 2 <= n); var t2 = localT * localT; var t3 = t2 * localT; var h00 = 2 * t3 - 3 * t2 + 1; var h10 = t3 - 2 * t2 + localT; var h01 = -2 * t3 + 3 * t2; var h11 = t3 - t2; var y = h00 * knots[i] + h10 * m0 + h01 * knots[i + 1] + h11 * m1; return (float)y; } /// /// Convenience: evaluate the spline at the position of bin /// of a histogram with bins. /// public static float EvaluateAtBin(float[]? knots, int binIndex, int totalBins) { if (totalBins <= 1) return Evaluate(knots, 0); var t = (double)binIndex / (totalBins - 1); return Evaluate(knots, t); } /// /// Build a default envelope spline from a reference histogram. For each knot /// the segment max around the knot position is taken, multiplied by 1.2 with a /// small floor added, and clamped to [0, 1]. /// public static float[] CreateDefault(float[]? referenceHistogram, int knotCount = KnotCount) { var knots = new float[knotCount]; if (referenceHistogram == null || referenceHistogram.Length == 0) { for (var k = 0; k < knotCount; k++) knots[k] = 0.05f; return knots; } var totalBins = referenceHistogram.Length; var span = totalBins - 1; // Half-width of the window we look at around each knot. var segHalf = System.Math.Max(1, span / (2 * (knotCount - 1))); for (var k = 0; k < knotCount; k++) { var center = knotCount == 1 ? 0 : k * span / (knotCount - 1); var lo = System.Math.Max(0, center - segHalf); var hi = System.Math.Min(totalBins - 1, center + segHalf); var max = 0f; for (var i = lo; i <= hi; i++) if (referenceHistogram[i] > max) max = referenceHistogram[i]; var y = max * 1.2f + 0.01f; if (y < 0f) y = 0f; if (y > 1f) y = 1f; knots[k] = y; } return knots; } private static double MonotoneTangent(double secLeft, double secRight, bool hasLeft, bool hasRight) { if (!hasLeft) return secRight; if (!hasRight) return secLeft; // If the signs differ (or either is zero), the knot is an extremum: tangent must be zero // to preserve monotonicity locally. if (secLeft == 0 || secRight == 0 || System.Math.Sign(secLeft) != System.Math.Sign(secRight)) return 0; // Average — Fritsch-Carlson would also clamp by 3*min(|secLeft|,|secRight|), but for our // small (5 knot) curves the simple average plus zero-at-extrema rule already prevents // overshoot in practice. var avg = 0.5 * (secLeft + secRight); var limit = 3.0 * System.Math.Min(System.Math.Abs(secLeft), System.Math.Abs(secRight)); if (System.Math.Abs(avg) > limit) avg = System.Math.Sign(avg) * limit; return avg; } }