OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / IFSEngine / Animation / Keyframe.cs

81 lines · csharp ref:
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;
}