using System;
using System.Collections.Generic;
namespace IFSEngine.Animation;
/// <summary>A handle position, in the channel's own (time, value) space.</summary>
public readonly struct Handle(double t, double value)
{
public double T { get; } = t;
public double Value { get; } = value;
}
/// <summary>
/// Derives and evaluates the bezier handles of <see cref="KeyframeType.Bezier"/> keyframes.
/// </summary>
/// <remarks>
/// <para>
/// Handles for <see cref="HandleType.Automatic"/>, <see cref="HandleType.AutoClamped"/> and
/// <see cref="HandleType.Vector"/> are DERIVED here on every read rather than cached on the
/// keyframe. Nothing then has to remember to invalidate them: <see cref="Keyframe"/> is a plain
/// mutable object in a plain <see cref="List{T}"/>, so a moved key raises no notification, and
/// a cache would go stale the first time one was dragged. The work is O(1) per segment against
/// an evaluator that already sorts the whole channel on every call.
/// </para>
/// <para>
/// The stored coordinates are only authoritative for <see cref="HandleType.Free"/> and
/// <see cref="HandleType.Aligned"/>, which are the two the user positions.
/// </para>
/// </remarks>
public static class BezierHandles
{
/// <summary>
/// The outgoing (right) handle of <paramref name="index"/> and the incoming (left) handle
/// of the key after it - the two control points of that segment.
/// </summary>
public static (Handle Right, Handle Left) ForSegment(List<Keyframe> sorted, int index)
{
Handle right = Resolve(sorted, index, leftSide: false);
Handle left = Resolve(sorted, index + 1, leftSide: true);
return Correct(sorted[index], right, left, sorted[index + 1]);
}
/// <summary>One handle of one keyframe, derived or stored according to its handle type.</summary>
public static Handle Resolve(List<Keyframe> sorted, int index, bool leftSide)
{
Keyframe k = sorted[index];
HandleType type = leftSide ? k.LeftHandleType : k.RightHandleType;
if (type is HandleType.Free or HandleType.Aligned)
return leftSide
? new Handle(k.LeftHandleT, k.LeftHandleValue)
: new Handle(k.RightHandleT, k.RightHandleValue);
Keyframe prev = index > 0 ? sorted[index - 1] : null;
Keyframe next = index < sorted.Count - 1 ? sorted[index + 1] : null;
// A third of the way to the neighbour is the length at which a cubic bezier with a
// matching slope reproduces the straight line through both keys.
double dtPrev = prev is not null ? (k.t - prev.t) / 3.0 : (next is not null ? (next.t - k.t) / 3.0 : 0);
double dtNext = next is not null ? (next.t - k.t) / 3.0 : (prev is not null ? (k.t - prev.t) / 3.0 : 0);
if (type == HandleType.Vector)
{
// Straight at the neighbour, which makes that side of the segment a line.
double vPrev = prev is not null ? k.Value + (prev.Value - k.Value) / 3.0 : k.Value;
double vNext = next is not null ? k.Value + (next.Value - k.Value) / 3.0 : k.Value;
return leftSide
? new Handle(k.t - dtPrev, vPrev)
: new Handle(k.t + dtNext, vNext);
}
double slope = Slope(prev, k, next, type == HandleType.AutoClamped);
if (leftSide)
{
double v = k.Value - slope * dtPrev;
if (type == HandleType.AutoClamped && prev is not null)
v = Clamp(v, prev.Value, k.Value);
return new Handle(k.t - dtPrev, v);
}
else
{
double v = k.Value + slope * dtNext;
if (type == HandleType.AutoClamped && next is not null)
v = Clamp(v, k.Value, next.Value);
return new Handle(k.t + dtNext, v);
}
}
/// <summary>
/// The tangent through a keyframe: a TIME-AWARE central difference, unlike
/// <c>Channel</c>'s Catmull-Rom tangent, which is <c>(p2 - p0) / 2</c> from the neighbours'
/// values alone and so both overshoots on uneven spacing and never flattens.
/// </summary>
/// <remarks>
/// <paramref name="clamped"/> is the whole reason a hold works: where the key is a local
/// extremum - both neighbours on the same side of it, which two equal keys in a row always
/// are - the tangent is zero, so the segment leaves the value flat and stays there.
/// </remarks>
private static double Slope(Keyframe prev, Keyframe k, Keyframe next, bool clamped)
{
if (prev is null && next is null)
return 0;
if (prev is null)
return Secant(k, next);
if (next is null)
return Secant(prev, k);
if (clamped && IsLocalExtremum(prev.Value, k.Value, next.Value))
return 0;
double span = next.t - prev.t;
return span > 0 ? (next.Value - prev.Value) / span : 0;
}
private static double Secant(Keyframe a, Keyframe b) =>
b.t - a.t > 0 ? (b.Value - a.Value) / (b.t - a.t) : 0;
/// <summary>True when both neighbours sit on the same side of the key - a peak, a trough, or a hold.</summary>
private static bool IsLocalExtremum(double prev, double value, double next) =>
(prev <= value && next <= value) || (prev >= value && next >= value);
private static double Clamp(double v, double a, double b) =>
Math.Clamp(v, Math.Min(a, b), Math.Max(a, b));
/// <summary>
/// Shortens handles that reach past the segment in TIME, which would make the curve fold
/// back on itself and stop being a function of time. Both handles scale by the same factor,
/// so the shape is preserved. This is Blender's <c>correct_bezpart</c>.
/// </summary>
private static (Handle, Handle) Correct(Keyframe k1, Handle right, Handle left, Keyframe k2)
{
double h1t = right.T - k1.t, h1v = right.Value - k1.Value;
double h2t = left.T - k2.t, h2v = left.Value - k2.Value;
double reach = Math.Abs(h1t) + Math.Abs(h2t);
double span = Math.Abs(k2.t - k1.t);
if (reach <= span || reach <= 0)
return (right, left);
double fac = span / reach;
return (new Handle(k1.t + h1t * fac, k1.Value + h1v * fac),
new Handle(k2.t + h2t * fac, k2.Value + h2v * fac));
}
/// <summary>
/// The value of a cubic bezier segment at a given TIME.
/// </summary>
/// <remarks>
/// A bezier is parameterised by u, not by time, so the time has to be inverted first.
/// After <see cref="Correct"/> the x component is monotonic over the segment, which makes
/// bisection both correct and unconditionally convergent - worth more here than the speed
/// of a Newton step that can leave [0,1] on a near-vertical segment.
/// </remarks>
public static double EvaluateSegment(Keyframe k1, Handle right, Handle left, Keyframe k2, double t)
{
double span = k2.t - k1.t;
if (span <= 0)
return k2.Value;
double lo = 0, hi = 1;
for (int i = 0; i < 60; i++)
{
double mid = (lo + hi) / 2.0;
if (Cubic(k1.t, right.T, left.T, k2.t, mid) < t)
lo = mid;
else
hi = mid;
}
return Cubic(k1.Value, right.Value, left.Value, k2.Value, (lo + hi) / 2.0);
}
private static double Cubic(double p0, double p1, double p2, double p3, double u)
{
double n = 1.0 - u;
return n * n * n * p0
+ 3.0 * n * n * u * p1
+ 3.0 * n * u * u * p2
+ u * u * u * p3;
}
}