namespace IFSEngine.Animation;
public class Keyframe
{
public InterpolationMode InterpolationMode { get; set; } = InterpolationMode.CatmullRom;
public double EasingPower { get; set; } = 1.0;
public EasingDirection EasingDirection { get; set; } = EasingDirection.InOut;
public double t { get; set; }
public double Value { get; set; }
private KeyframeType _type = KeyframeType.Legacy;
/// <summary>
/// The Blender-style preset governing the segment to this keyframe's RIGHT.
/// </summary>
/// <remarks>
/// Setting this also projects <see cref="InterpolationMode"/> onto the nearest legacy
/// equivalent, so a file written here still evaluates - approximately, but without
/// throwing - in the upstream Windows app, which knows nothing about this property. See
/// <see cref="KeyframeType"/> for why a new enum value could not be used instead.
/// </remarks>
public KeyframeType Type
{
get => _type;
set
{
_type = value;
if (value != KeyframeType.Legacy)
InterpolationMode = LegacyEquivalent(value);
}
}
public HandleType LeftHandleType { get; set; } = HandleType.AutoClamped;
public HandleType RightHandleType { get; set; } = HandleType.AutoClamped;
/// <summary>Incoming handle, in (time, value) space. Authoritative only for Free and Aligned.</summary>
public double LeftHandleT { get; set; }
public double LeftHandleValue { get; set; }
/// <summary>Outgoing handle, in (time, value) space. Authoritative only for Free and Aligned.</summary>
public double RightHandleT { get; set; }
public double RightHandleValue { get; set; }
/// <summary>How far <see cref="KeyframeType.Back"/> overshoots. Blender's default.</summary>
public double BackOvershoot { get; set; } = 1.70158;
/// <summary>Zero means auto.</summary>
public double ElasticAmplitude { get; set; }
public double ElasticPeriod { get; set; }
/// <summary>The pre-existing mode that most nearly reproduces a preset.</summary>
public static InterpolationMode LegacyEquivalent(KeyframeType type) => type switch
{
KeyframeType.Constant => InterpolationMode.Constant,
KeyframeType.Linear => InterpolationMode.Linear,
_ => InterpolationMode.CatmullRom,
};
// ---- conditional serialization (Newtonsoft honours ShouldSerialize<Property>) ----
//
// Without these, every keyframe ever written would gain eleven new fields, including the
// overwhelming majority that are plain Legacy keys from existing files. Suppressing the
// ones a keyframe does not use keeps a legacy .ifsjson byte-identical across a load/save
// round-trip, which is the property the fork's compatibility claim rests on.
public bool ShouldSerializeType() => Type != KeyframeType.Legacy;
public bool ShouldSerializeLeftHandleType() => Type == KeyframeType.Bezier;
public bool ShouldSerializeRightHandleType() => Type == KeyframeType.Bezier;
public bool ShouldSerializeLeftHandleT() => IsUserPositioned(LeftHandleType);
public bool ShouldSerializeLeftHandleValue() => IsUserPositioned(LeftHandleType);
public bool ShouldSerializeRightHandleT() => IsUserPositioned(RightHandleType);
public bool ShouldSerializeRightHandleValue() => IsUserPositioned(RightHandleType);
public bool ShouldSerializeBackOvershoot() => Type == KeyframeType.Back;
public bool ShouldSerializeElasticAmplitude() => Type == KeyframeType.Elastic;
public bool ShouldSerializeElasticPeriod() => Type == KeyframeType.Elastic;
/// <summary>Derived handles are recomputed on read, so only these two are worth storing.</summary>
private bool IsUserPositioned(HandleType handle) =>
Type == KeyframeType.Bezier && handle is HandleType.Free or HandleType.Aligned;
}