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;
}
}