CommunityToolkit / Lottie-Windows

Lottie-Windows is a library (and related tools) for rendering Lottie animations on Windows 10 and Windows 11.
https://aka.ms/lottiedocs
Other
621 stars 75 forks source link

Fast, reliable Lottie file loader and player #556

Open r2d2Proton opened 4 months ago

r2d2Proton commented 4 months ago

Would like to share a fast and (appears) reliable Lottie file loader and player. One that uses SkiaSharp.Skottie.Animation:

public class LottieData : MyData
{
    public string Filename;
    public SKPoint Pos = new SKPoint { };
    public float Scale = 1.0f;
    public int Width = 0;
    public int Height = 0;

    public SkiaSharp.Skottie.Animation? Animation;
    public bool PlayForwards = true;
    public int RepeatsCompleted = 0;

    protected TimeSpan duration = new TimeSpan();
    public TimeSpan Duration
    {
        get => duration;
        set => duration = value;
    }

    protected TimeSpan progress = new TimeSpan();
    public TimeSpan Progress
    {
        get => progress;
        set => progress = value;
    }

    protected bool isComplete = false;
    public bool IsComplete
    {
        get => isComplete;
        set => isComplete = value;
    }

    int repeatCount = 0;
    public int RepeatCount
    {
        get => repeatCount;
        set => repeatCount = value;
    }

    protected SKLottieRepeatMode repeatMode = SKLottieRepeatMode.Restart;
    public SKLottieRepeatMode RepeatMode
    {
        get => repeatMode;
        set => repeatMode = value;
    }

    protected MyEventArgs myArgs;
    public MyEventArgs MyArgs
    {
        get => myArgs;
        set => myArgs = value;
    }

    public event EventHandler? AnimationFailed;

    public event EventHandler? AnimationLoaded;

    public event EventHandler<MyEventArgs>? AnimationCompleted;
    public void OnAnimationCompleted(LottiePlayer lottiePlayer)
    {
        AnimationCompleted?.Invoke(lottiePlayer, myArgs);
    }
}

public class LottiePlayer
{
    public new LottieData MyData { get => (LottieData)base.MyData; set => base.MyData = value; }

    public LottiePlayer() : base(ShapeType.Lottie)
    {
        MyData = new LottieData();
        InitWidget();
    }

    public LottiePlayer(LottieData lottieData) : base(ShapeType.Lottie, lottieData)
    {
        InitWidget();
    }

    protected void InitWidget()
    {
        CreateLottie();
        //CreateLottieView();
    }

    protected void CreateLottie()
    {
        LottieData lottieData = MyData;
        if (lottieData != null)
        {
            if (!string.IsNullOrEmpty(lottieData.Filename))
            {
                string lottieStr = Utils.LoadStringResource(lottieData.Filename);
                if (!string.IsNullOrEmpty(lottieStr))
                {
                    //Stream stream = new MemoryStream(bytes);
                    var bytes = System.Text.Encoding.UTF8.GetBytes(lottieStr);
                    var data = SKData.CreateCopy(bytes);
                    if (SkiaSharp.Skottie.Animation.TryCreate(data.AsStream(), out lottieData.Animation))
                    {
                    }
                }
            }

            ResetAnimation();
        }
    }

    void ResetAnimation()
    {
        LottieData lottieData = MyData;
        if (lottieData != null)
        {
            lottieData.PlayForwards = true;
            lottieData.RepeatsCompleted = 0;

            lottieData.Progress = TimeSpan.Zero;
            lottieData.Duration = TimeSpan.Zero;

            if (lottieData.Animation != null)
                lottieData.Duration = lottieData.Animation.Duration;
        }
    }

    public virtual void Update(TimeSpan deltaTime)
    {
        LottieData lottieData = MyData;
        if (lottieData == null || !lottieData.IsEnabled) return;

        // TODO: handle case where a repeat or revers cases the progress
        //       to either wrap or start the next round
        if (!lottieData.PlayForwards)
            deltaTime = -deltaTime;

        var newProgress = lottieData.Progress + deltaTime;

        if (newProgress > lottieData.Duration)
            newProgress = lottieData.Duration;

        if (newProgress < TimeSpan.Zero)
            newProgress = TimeSpan.Zero;

        lottieData.Progress = newProgress;

        UpdateProgress(lottieData.Progress);
    }

    private void UpdateProgress(TimeSpan progress)
    {
        LottieData lottieData = MyData;
        if (lottieData == null || lottieData.Animation == null)
        {
            if (lottieData != null)
                lottieData.IsComplete = true;

            return;
        }

        lottieData.Animation.SeekFrameTime(progress.TotalSeconds);

        var repeatMode = lottieData.RepeatMode;
        var duration = lottieData.Duration;

        // have we reached the end of this run
        var atStart = !lottieData.PlayForwards && progress <= TimeSpan.Zero;
        var atEnd = lottieData.PlayForwards && progress >= duration;
        var isFinishedRun = repeatMode == SKLottieRepeatMode.Restart ? atEnd : atStart;

        // maybe the direction changed
        var needsFlip = (atEnd && repeatMode == SKLottieRepeatMode.Reverse) || (atStart && repeatMode == SKLottieRepeatMode.Restart);
        if (needsFlip)
        {
            // we need to reverse to finish the run
            lottieData.PlayForwards = !lottieData.PlayForwards;
            lottieData.IsComplete = false;
        }
        else
        {
            // make sure repeats are positive to make things easier
            var totalRepeatCount = lottieData.RepeatCount;
            if (totalRepeatCount < 0)
                totalRepeatCount = int.MaxValue;

            // infinite
            var infinite = totalRepeatCount == int.MaxValue;
            if (infinite)
                lottieData.RepeatsCompleted = 0;

            // if we are at the end and we are repeating, then repeat
            if (isFinishedRun && lottieData.RepeatsCompleted < totalRepeatCount)
            {
                if (!infinite)
                    lottieData.RepeatsCompleted++;

                isFinishedRun = false;

                if (repeatMode == SKLottieRepeatMode.Restart)
                    lottieData.Progress = TimeSpan.Zero;
                else if (repeatMode == SKLottieRepeatMode.Reverse)
                    lottieData.PlayForwards = !lottieData.PlayForwards;
            }

            lottieData.IsComplete = isFinishedRun && lottieData.RepeatsCompleted >= totalRepeatCount;

            if (lottieData.IsComplete)
                lottieData.OnAnimationCompleted(this);
        }
    }

    public override void Draw(SKCanvas canvas, RectF dirtyRect)
    {
        canvas.Save();

        base.Draw(canvas, dirtyRect);

        LottieData lottieData = MyData;
        if (lottieData != null && lottieData.IsVisible)
        {
            int width = lottieData.Width;
            int height = lottieData.Height;

            // set transforms
            canvas.Translate(lottieData.Pos);
            canvas.Scale(lottieData.Scale);

            SKRect rect = new SKRect(0, 0, width, height);

            if (lottieData.Animation != null)
            {
                lottieData.Animation.Render(canvas, rect);
            }
        }

        canvas.Restore();
    }
}
r2d2Proton commented 4 months ago

I have yet to see Lottie's fail on Windows with the above code.

When I try with SKLottieView some lotties do not render correctly or not at all. . . but that may be due to intermingled calls such as:

private void UpdateProgress(TimeSpan progress)
{
    :
    lottieData.Animation.SeekFrameTime(progress.TotalSeconds);
}

And would expect a pixel perfect overdraw of the entire animation wtih:

public override void Draw(SKCanvas canvas, RectF dirtyRect)
{
    :
            lottieData.Animation.Render(canvas, rect);
    :
}
r2d2Proton commented 4 months ago

In an effort to test with SKLottieView, it was added to my LottieData class and created instead of the version above:

public class LottieData : MyData
{
    :
    public SKLottieView skLottieView = null;
    :
}

public class LottiePlayer
{
    :
    protected void InitWidget()
    {
        //CreateLottie();
        CreateLottieView();
    }

    public async Task CreateLottieView()
    {
        LottieData lottieData = MyData;
        if (lottieData != null)
        {
            if (!string.IsNullOrEmpty(lottieData.Filename))
            {
                //SKLottieImageSource skLottieImageSource = (SKLottieImageSource)SKLottieImageSource.FromFile(lottieData.Filename);
                string lottieStr = Utils.LoadStringResource(lottieData.Filename);
                if (!string.IsNullOrEmpty(lottieStr))
                {
                    //Stream stream = new MemoryStream(bytes);
                    var bytes = System.Text.Encoding.UTF8.GetBytes(lottieStr);
                    var data = SKData.CreateCopy(bytes);
                    SKLottieImageSource skLottieImageSource = (SKLottieImageSource)SKLottieImageSource.FromStream(data.AsStream());
                    await TaskDelay(100);

                    lottieData.skLottieView = new SKLottieView
                    {
                        Source = skLottieImageSource,
                        IsAnimationEnabled = true,
                        RepeatCount = -1,
                        RepeatMode = SKLottieRepeatMode.Reverse,
                        HeightRequest = lottieData.Height,
                        WidthRequest = lottieData.Width
                    };

                    lottieData.Animation = await lottieData.skLottieView.Source.LoadAnimationAsync();
                    await TaskDelay(100);
                }
            }

            ResetAnimation();
        }
    }

    protected async Task TaskDelay(int delay)
    {
        await Task.Delay(delay);
    }