#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using IFSEngine.Animation.ChannelDrivers;
namespace IFSEngine.Animation;
public class Channel
{
public string Name { get; set; } = "Unnamed channel";
public List<Keyframe> Keyframes { get; init; } = [];
public AudioChannelDriver? AudioChannelDriver { get; set; } = null;
//TODO: add other channel drivers: clamp, wrap, repeat
public Channel() { }
public Channel(string name, Keyframe keyframe)
{
Name = name;
Keyframes.Add(keyframe);
}
public double EvaluateAt(double t)
{
//TODO: avoid sorting every time
var keyframes = Keyframes.OrderBy(k => k.t).ToList();
double eval = 0;
if (t >= keyframes.Last().t)
eval = keyframes.Last().Value;
else if (t <= keyframes.First().t)
eval = keyframes.First().Value;
else
{//between keyframes
var previousKeyframe = keyframes.Where(c => c.t <= t).MaxBy(c => c.t)!;
var nextKeyframe = keyframes.Where(c => c.t > t).MinBy(c => c.t)!;
var tNorm = (t - previousKeyframe.t) / (nextKeyframe.t - previousKeyframe.t);
if (previousKeyframe.Type != KeyframeType.Legacy)
return Drive(EvaluatePreset(keyframes, previousKeyframe, nextKeyframe, tNorm), t);
//easing
// Legacy only. A preset carries its own shape, and for the easing presets the
// direction is applied inside Easing.Apply instead.
// Every case must satisfy f(0)=0 and f(1)=1, or the segment does not start at the
// previous keyframe and end at the next one. `Out` was `1 - t^p`, which is 1 at
// t=0 and 0 at t=1 - it ran the whole segment BACKWARDS, from the next keyframe's
// value to the previous one, at every power including 1. The mirror of `In` about
// both axes is `1 - (1-t)^p`.
double tEasing = previousKeyframe.EasingDirection switch
{
EasingDirection.In => Math.Pow(tNorm, previousKeyframe.EasingPower),
EasingDirection.Out => 1.0 - Math.Pow(1.0 - tNorm, previousKeyframe.EasingPower),
EasingDirection.InOut => InOutPowerEasing(tNorm, previousKeyframe.EasingPower, nextKeyframe.EasingPower),
_ => throw new NotImplementedException(),
};
//interpolation
if (previousKeyframe.InterpolationMode == InterpolationMode.Linear)
{
eval = (1.0 - tEasing) * previousKeyframe.Value + tEasing * nextKeyframe.Value;
}
else if (previousKeyframe.InterpolationMode == InterpolationMode.Constant)
{
eval = previousKeyframe.Value;
}
else if (previousKeyframe.InterpolationMode == InterpolationMode.CatmullRom)
{
var p0 = keyframes.ElementAtOrDefault(keyframes.IndexOf(previousKeyframe) - 1) ?? previousKeyframe;
var p3 = keyframes.ElementAtOrDefault(keyframes.IndexOf(nextKeyframe) + 1) ?? nextKeyframe;
eval = CatmullRom(p0.Value, previousKeyframe.Value, nextKeyframe.Value, p3.Value, tEasing);
}
else throw new NotImplementedException();
}
return Drive(eval, t);
}
/// <summary>
/// Where a keyframe's bezier handles actually sit, deriving the automatic ones.
/// </summary>
/// <remarks>
/// The editor needs this to DRAW handles, and to seed the stored coordinates when a handle
/// is switched to a user-positioned type - without which the curve would jump the moment
/// "Free" was picked, because the stored coordinates start at the origin.
/// </remarks>
public (Handle Left, Handle Right) ResolveHandles(Keyframe key)
{
var sorted = Keyframes.OrderBy(k => k.t).ToList();
int index = sorted.IndexOf(key);
if (index < 0)
return (new Handle(key.t, key.Value), new Handle(key.t, key.Value));
return (BezierHandles.Resolve(sorted, index, leftSide: true),
BezierHandles.Resolve(sorted, index, leftSide: false));
}
/// <summary>Applies the channel's audio driver, if it has one, on top of an interpolated value.</summary>
private double Drive(double eval, double t) =>
AudioChannelDriver is not null ? AudioChannelDriver.Apply(eval, t) : eval;
/// <summary>
/// Evaluates a <see cref="KeyframeType"/> preset over a segment, given its NORMALIZED
/// parameter.
/// </summary>
/// <remarks>
/// <paramref name="tNorm"/> is deliberately the un-eased parameter: the legacy
/// <see cref="Keyframe.EasingPower"/> ramp is a different mechanism from these presets and
/// composing the two would shape every segment twice.
/// </remarks>
private static double EvaluatePreset(List<Keyframe> keyframes, Keyframe previous, Keyframe next, double tNorm)
{
switch (previous.Type)
{
case KeyframeType.Constant:
return previous.Value;
case KeyframeType.Linear:
return (1.0 - tNorm) * previous.Value + tNorm * next.Value;
case KeyframeType.Bezier:
int index = keyframes.IndexOf(previous);
(Handle right, Handle left) = BezierHandles.ForSegment(keyframes, index);
double t = previous.t + tNorm * (next.t - previous.t);
return BezierHandles.EvaluateSegment(previous, right, left, next, t);
default:
double eased = Easing.Apply(previous.Type, previous.EasingDirection, tNorm, previous);
return (1.0 - eased) * previous.Value + eased * next.Value;
}
}
private static double InOutPowerEasing(double t, double pow1, double pow2)
{
if (t < 0.5)
return Math.Pow(t * 2.0, pow1) / 2.0;
return 1.0 - Math.Pow((1.0 - t) * 2.0, pow2) / 2.0;
}
private static double CatmullRom(double p0, double p1, double p2, double p3, double t)
{
var h1 = 2 * t * t * t - 3 * t * t + 1;
var h2 = -2 * t * t * t + 3 * t * t;
var h3 = t * t * t - 2 * t * t + t;
var h4 = t * t * t - t * t;
var t1 = (p2 - p0) / 2;
var t2 = (p3 - p1) / 2;
return h1 * p1 + h2 * p2 + h3 * t1 + h4 * t2;
}
}