+ Play Lottie animations using SKSkottiePlayer from
+ SkiaSharp.Extended. The same player logic is shared between
+ this Blazor component and the .NET MAUI SKLottieView.
+
+
+@code {
+ private SKCanvasView? canvas;
+ private readonly SKSkottiePlayer player = new() { RepeatCount = -1 };
+ private Animation? loadedAnimation;
+ private CancellationTokenSource? animationCts;
+ private bool isLoading = true;
+ private SKSkottieRepeatMode repeatMode = SKSkottieRepeatMode.Restart;
+ private double animationSpeed = 1.0;
+ private int repeatCount = -1;
+ private DateTime lastFrame;
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (!firstRender)
+ return;
+
+ await LoadAnimationAsync();
+
+ animationCts = new CancellationTokenSource();
+ _ = AnimationLoop(animationCts.Token);
+ }
+
+ private async Task LoadAnimationAsync()
+ {
+ isLoading = true;
+
+ try
+ {
+ using var stream = await Http.GetStreamAsync("animations/trophy.json");
+ loadedAnimation = Animation.Create(stream);
+ ApplySettings();
+ player.SetAnimation(loadedAnimation);
+ }
+ catch
+ {
+ loadedAnimation = null;
+ player.SetAnimation(null);
+ }
+ finally
+ {
+ isLoading = false;
+ }
+
+ StateHasChanged();
+ }
+
+ private async Task AnimationLoop(CancellationToken ct)
+ {
+ using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(16));
+ lastFrame = DateTime.UtcNow;
+
+ while (await timer.WaitForNextTickAsync(ct))
+ {
+ var now = DateTime.UtcNow;
+ player.Update(now - lastFrame);
+ lastFrame = now;
+ await InvokeAsync(() =>
+ {
+ canvas?.Invalidate();
+ StateHasChanged();
+ });
+ }
+ }
+
+ private void OnPaintSurface(SKPaintSurfaceEventArgs e)
+ {
+ var info = e.Info;
+ var skCanvas = e.Surface.Canvas;
+
+ skCanvas.Clear(SKColors.Transparent);
+
+ if (player.HasAnimation)
+ player.Render(skCanvas, SKRect.Create(0, 0, info.Width, info.Height));
+ }
+
+ private void OnRepeatModeChanged()
+ {
+ player.RepeatMode = repeatMode;
+ }
+
+ private void OnSpeedChanged()
+ {
+ player.AnimationSpeed = animationSpeed;
+ }
+
+ private void OnRepeatCountChanged()
+ {
+ player.RepeatCount = repeatCount;
+ }
+
+ private void Restart()
+ {
+ ApplySettings();
+ player.SetAnimation(loadedAnimation);
+ canvas?.Invalidate();
+ }
+
+ private void ApplySettings()
+ {
+ player.RepeatMode = repeatMode;
+ player.AnimationSpeed = animationSpeed;
+ player.RepeatCount = repeatCount;
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ animationCts?.Cancel();
+ animationCts?.Dispose();
+ return ValueTask.CompletedTask;
+ }
+}
diff --git a/samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj b/samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj
index e22a8ae99e..42d6d41fdf 100644
--- a/samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj
+++ b/samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj
@@ -29,6 +29,7 @@
+
diff --git a/samples/SkiaSharpDemo.Blazor/wwwroot/animations/trophy.json b/samples/SkiaSharpDemo.Blazor/wwwroot/animations/trophy.json
new file mode 100644
index 0000000000..a4f54d8c74
--- /dev/null
+++ b/samples/SkiaSharpDemo.Blazor/wwwroot/animations/trophy.json
@@ -0,0 +1 @@
+{"v":"5.8.1","fr":30,"ip":0,"op":71,"w":500,"h":500,"nm":"Trophy","ddd":0,"assets":[{"id":"comp_0","nm":"Pre-comp 3","fr":30,"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[391.176,345.588,0],"ix":2,"l":2},"a":{"a":0,"k":[50,48.5,0],"ix":1,"l":2},"s":{"a":0,"k":[30,30,100],"ix":6,"l":2}},"ao":0,"w":100,"h":97,"ip":2,"op":17,"st":2,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"Pre-comp 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[344.118,294.118,0],"ix":2,"l":2},"a":{"a":0,"k":[50,48.5,0],"ix":1,"l":2},"s":{"a":0,"k":[50,50,100],"ix":6,"l":2}},"ao":0,"w":100,"h":97,"ip":1,"op":16,"st":1,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"Pre-comp 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[151.471,317.647,0],"ix":2,"l":2},"a":{"a":0,"k":[50,48.5,0],"ix":1,"l":2},"s":{"a":0,"k":[30,30,100],"ix":6,"l":2}},"ao":0,"w":100,"h":97,"ip":7,"op":22,"st":7,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"Pre-comp 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[104.412,266.176,0],"ix":2,"l":2},"a":{"a":0,"k":[50,48.5,0],"ix":1,"l":2},"s":{"a":0,"k":[50,50,100],"ix":6,"l":2}},"ao":0,"w":100,"h":97,"ip":6,"op":21,"st":6,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"Pre-comp 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[342.647,145.588,0],"ix":2,"l":2},"a":{"a":0,"k":[50,48.5,0],"ix":1,"l":2},"s":{"a":0,"k":[30,30,100],"ix":6,"l":2}},"ao":0,"w":100,"h":97,"ip":4,"op":19,"st":4,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"Pre-comp 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[295.588,94.118,0],"ix":2,"l":2},"a":{"a":0,"k":[50,48.5,0],"ix":1,"l":2},"s":{"a":0,"k":[50,50,100],"ix":6,"l":2}},"ao":0,"w":100,"h":97,"ip":3,"op":18,"st":3,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"Pre-comp 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[133.824,122.059,0],"ix":2,"l":2},"a":{"a":0,"k":[50,48.5,0],"ix":1,"l":2},"s":{"a":0,"k":[30,30,100],"ix":6,"l":2}},"ao":0,"w":100,"h":97,"ip":1,"op":16,"st":1,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"Pre-comp 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[179.412,82.353,0],"ix":2,"l":2},"a":{"a":0,"k":[50,48.5,0],"ix":1,"l":2},"s":{"a":0,"k":[50,50,100],"ix":6,"l":2}},"ao":0,"w":100,"h":97,"ip":0,"op":15,"st":0,"bm":0}]},{"id":"comp_1","nm":"Pre-comp 2","fr":30,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 12","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[50.5,47,0],"ix":2,"l":2},"a":{"a":0,"k":[-142.5,-154,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-142.5,-154],[-101.5,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.705882352941,0.247058838489,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[0]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":15,"st":-11,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 11","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":0,"k":[50.5,47,0],"ix":2,"l":2},"a":{"a":0,"k":[-142.5,-154,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-142.5,-154],[-101.5,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.705882352941,0.247058838489,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[0]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":15,"st":-11,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 10","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[50.5,47,0],"ix":2,"l":2},"a":{"a":0,"k":[-142.5,-154,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-142.5,-154],[-101.5,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.705882352941,0.247058838489,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[0]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":15,"st":-11,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 9","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50.5,47,0],"ix":2,"l":2},"a":{"a":0,"k":[-142.5,-154,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-142.5,-154],[-101.5,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.705882352941,0.247058838489,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[0]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":15,"st":-11,"bm":0}]},{"id":"comp_2","nm":"Pre-comp 1","fr":30,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 10","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.705882352941,0.247058838489,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 11","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":30,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.705882352941,0.247058838489,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 12","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":60,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.705882352941,0.247058838489,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 13","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.705882352941,0.247058838489,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 14","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":120,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.705882352941,0.247058838489,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 15","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":150,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.705882352941,0.247058838489,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 16","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.705882352941,0.247058838489,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Shape Layer 17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":210,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.705882352941,0.247058838489,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 18","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":240,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.705882352941,0.247058838489,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Shape Layer 19","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":270,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.705882352941,0.247058838489,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Shape Layer 21","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":300,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.705882352941,0.247058838489,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":3,"s":[60]},{"t":13,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Shape Layer 20","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":330,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.705882352941,0.247058838489,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":3,"s":[60]},{"t":13,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 3","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":39,"op":61,"st":39,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"Pre-comp 3","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":24,"op":46,"st":24,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Cup 3","parent":14,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.371,-98.838,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.8,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[-11.815,0],[0,0],[1.176,-11.756],[0,0],[5.492,54.916],[0,0]],"o":[[0,0],[11.815,0],[0,0],[-5.492,54.916],[0,0],[-1.176,-11.756]],"v":[[-49.8,-128.285],[49.3,-128.285],[70.626,-106.958],[62.096,-21.652],[-62.596,-21.652],[-71.126,-106.958]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.2,"y":0},"t":14,"s":[{"i":[[0,6.785],[0,0],[0,-11.667],[0,0],[0,55.777],[0,0]],"o":[[0,0],[0,8.035],[0,0],[0,54.652],[0,0],[0,-12.042]],"v":[[-0.25,-128.285],[-0.25,-128.285],[-0.25,-106.958],[-0.25,-21.652],[-0.25,-21.652],[-0.25,-106.958]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[-11.815,0],[0,0],[1.176,-11.756],[0,0],[5.492,54.916],[0,0]],"o":[[0,0],[11.815,0],[0,0],[-5.492,54.916],[0,0],[-1.176,-11.756]],"v":[[-49.8,-128.285],[49.3,-128.285],[70.626,-106.958],[62.096,-21.652],[-62.596,-21.652],[-71.126,-106.958]],"c":true}]},{"i":{"x":1,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[-11.815,0],[0,0],[1.176,-11.756],[0,0],[5.492,54.916],[0,0]],"o":[[0,0],[11.815,0],[0,0],[-5.492,54.916],[0,0],[-1.176,-11.756]],"v":[[-49.8,-128.285],[49.3,-128.285],[70.626,-106.958],[62.096,-21.652],[-62.596,-21.652],[-71.126,-106.958]],"c":true}]},{"i":{"x":0.223,"y":1},"o":{"x":0.2,"y":0},"t":31,"s":[{"i":[[0,6.785],[0,0],[0,-11.667],[0,0],[0,55.777],[0,0]],"o":[[0,0],[0,8.035],[0,0],[0,54.652],[0,0],[0,-12.042]],"v":[[-0.25,-128.285],[-0.25,-128.285],[-0.25,-106.958],[-0.25,-21.652],[-0.25,-21.652],[-0.25,-106.958]],"c":true}]},{"t":50,"s":[{"i":[[-11.815,0],[0,0],[1.176,-11.756],[0,0],[5.492,54.916],[0,0]],"o":[[0,0],[11.815,0],[0,0],[-5.492,54.916],[0,0],[-1.176,-11.756]],"v":[[-49.8,-128.285],[49.3,-128.285],[70.626,-106.958],[62.096,-21.652],[-62.596,-21.652],[-71.126,-106.958]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.705882370472,0.247058823705,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Cup","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":31,"op":310,"st":10,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 7","tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"t":24,"s":[{"i":[[3.191,-0.395],[2.304,-0.927],[2.095,-1.709],[1.788,-1.877],[0.908,-1.912],[-1.334,-6.312],[-2.779,-4.188],[-3.401,-3.602],[-3.548,-3.297],[-2.312,-2.352],[-2.506,-2.506],[-2.476,-2.535],[-0.232,-1.997],[0.723,-0.831],[0.267,-1.304],[-2.88,-0.857],[1.3,9.712],[4.203,4.76],[9.453,16.328],[-0.295,3.28],[-3.343,1.249],[-4.023,-0.951],[-1.8,-0.768],[-8.286,2.069],[-0.398,3.182],[3.129,3.445],[1.614,1.176],[1.189,0.657],[2.306,0.956],[2.086,0.582]],"o":[[-4.689,0.581],[-2.304,0.927],[-1.938,1.582],[-1.788,1.877],[-3.116,6.566],[1.334,6.312],[2.849,4.294],[3.401,3.602],[2.244,2.084],[2.312,2.352],[2.864,2.864],[2.476,2.535],[0.16,1.372],[-0.723,0.831],[-1.339,6.557],[13.183,3.921],[-1.018,-7.607],[-12.335,-13.97],[-1.509,-2.606],[0.413,-4.602],[3.955,-1.477],[2.275,0.538],[8.878,3.789],[3.458,-0.863],[0.467,-3.729],[-1.703,-1.875],[-1.654,-1.205],[-1.861,-1.028],[-2.371,-0.983],[-7.691,-2.147]],"v":[[-93,-111],[-102.945,-108.846],[-109,-105],[-114.773,-99.748],[-119,-94],[-120.921,-74.216],[-114,-58],[-104.525,-46.252],[-94,-36],[-87.197,-29.316],[-80,-22],[-71.526,-13.849],[-67,-7],[-68.18,-3.949],[-70,-1],[-64,12],[-47,-11],[-59,-30],[-99,-72],[-102,-81],[-94,-91],[-82,-91],[-75,-89],[-55,-79],[-48,-87],[-53,-98],[-58,-101],[-62,-105],[-69,-107],[-75,-110]],"c":true}],"h":1},{"t":25,"s":[{"i":[[6.363,-1.468],[2.979,-2.095],[1.84,-2.639],[0.408,-1.067],[0.408,-1.35],[0.465,-0.387],[0.082,-0.263],[-3.965,-7.542],[-4.029,-4.555],[-0.766,-0.479],[-0.438,-0.523],[-0.104,-0.568],[-0.27,-0.353],[-0.859,-0.529],[-0.842,-0.709],[-4.878,-5.799],[-0.092,-0.71],[0.419,-2.677],[-6.464,-0.238],[-1.53,2.112],[6.189,7.171],[5.82,5.82],[5.515,7.127],[-5.296,5.528],[-9.204,-2.345],[-3.834,-2.514],[-5.231,0.751],[-0.822,3.258],[5.25,2.566],[1.551,0.608]],"o":[[-4.243,0.979],[-2.98,2.096],[-0.962,1.38],[-0.408,1.067],[-0.057,0.189],[-0.465,0.387],[-2.889,9.276],[3.965,7.542],[0.497,0.561],[0.766,0.479],[0.313,0.374],[0.104,0.568],[1.053,1.378],[1.068,0.657],[6.494,5.469],[1.271,1.511],[0.356,2.738],[-1.191,7.598],[4.588,0.169],[8.605,-11.877],[-4.677,-5.419],[-6.151,-6.151],[-4.119,-5.322],[4.622,-4.825],[3.701,0.943],[4.525,2.967],[3.142,-0.451],[2.691,-10.661],[-1.826,-0.892],[-7.754,-3.037]],"v":[[-95,-111],[-105.802,-106.245],[-113,-99],[-114.915,-95.478],[-116,-92],[-116.981,-91.056],[-118,-90],[-114.689,-64.459],[-101,-46],[-98.956,-44.471],[-97,-43],[-96.468,-41.485],[-96,-40],[-92,-37],[-90,-35],[-71,-17],[-65,-8],[-68,-1],[-59,12],[-49,6],[-55,-29],[-72,-45],[-91,-66],[-97,-87],[-77,-91],[-66,-85],[-54,-79],[-46,-86],[-62,-106],[-67,-109]],"c":true}],"h":1},{"t":26,"s":[{"i":[[1.111,-0.113],[2.585,-1.009],[1.674,-1.559],[0.573,-0.084],[0.356,-0.336],[0.475,-0.926],[0.564,-0.79],[0.36,-0.281],[0.285,-0.493],[0.871,-2.718],[0.063,-2.618],[-3.733,-5.588],[-2.015,-2.521],[-0.872,-1.07],[-1.274,-1.411],[-7.648,-7.648],[-0.543,-5.007],[0.55,-2.542],[-2.362,-2.153],[-1.562,7.645],[2.913,4.566],[2.86,3.478],[9.12,11.123],[-1.11,7.282],[-2.157,0.726],[-4.835,-3.467],[-4.962,0.362],[-0.24,6.416],[7.573,3.176],[2.407,0.544]],"o":[[-3.987,0.405],[-2.585,1.009],[-0.352,0.328],[-0.573,0.084],[-0.536,0.507],[-0.475,0.926],[-0.273,0.382],[-0.36,0.281],[-1.452,2.508],[-0.871,2.718],[-0.253,10.508],[1.754,2.625],[0.961,1.203],[1.009,1.238],[6.895,7.635],[5.268,5.268],[0.253,2.337],[-0.918,4.241],[8.175,7.452],[1.806,-8.842],[-2.806,-4.398],[-8.695,-10.574],[-5.23,-6.378],[0.863,-5.666],[7.845,-2.641],[3.765,2.699],[4.671,-0.341],[0.327,-8.737],[-3.3,-1.384],[-5.338,-1.207]],"v":[[-85,-112],[-94.735,-109.865],[-101,-106],[-102.498,-105.506],[-104,-105],[-105.479,-102.712],[-107,-100],[-107.991,-99.083],[-109,-98],[-112.542,-90.083],[-114,-82],[-106,-58],[-100,-51],[-98,-47],[-94,-44],[-75,-23],[-63,-8],[-65,-1],[-63,9],[-43,-1],[-48,-23],[-58,-35],[-84,-62],[-94,-83],[-86,-92],[-64,-86],[-52,-79],[-43,-89],[-63,-108],[-72,-112]],"c":true}],"h":1},{"t":27,"s":[{"i":[[2.317,-0.535],[3.86,-4.04],[1.242,-4.613],[-2.12,-5.938],[-2.373,-3.37],[-0.492,-0.639],[-0.459,-0.632],[-0.8,-1.421],[-0.923,-1.114],[-0.951,-0.655],[-0.472,-0.507],[-0.081,-0.566],[-0.34,-0.374],[-1.019,-0.973],[-0.936,-1.02],[-0.997,-1.18],[-0.954,-1.127],[-0.458,-4.534],[0.591,-1.92],[-7.875,-0.144],[-0.943,9.517],[4.79,6.294],[7.791,10.593],[-2.16,8.125],[-1.994,0.531],[-4.781,-3.242],[-0.608,-0.61],[-0.949,-0.628],[-0.752,8.522],[12.133,3.114]],"o":[[-5.317,1.227],[-3.86,4.04],[-1.851,6.879],[2.12,5.938],[0.571,0.811],[0.492,0.639],[0.875,1.203],[0.8,1.421],[0.723,0.873],[0.951,0.655],[0.337,0.361],[0.081,0.566],[0.99,1.09],[1.018,0.973],[1.058,1.152],[0.997,1.18],[3.481,4.111],[0.271,2.68],[-2.255,7.329],[7.212,0.132],[1.112,-11.222],[-8.19,-10.762],[-4.476,-6.085],[0.814,-3.063],[5.149,-1.372],[0.641,0.434],[1.172,1.175],[7.025,4.649],[0.85,-9.635],[-5.404,-1.387]],"v":[[-82,-111],[-96.057,-102.54],[-104,-89],[-102.668,-69.368],[-95,-55],[-93.416,-52.866],[-92,-51],[-89.536,-46.934],[-87,-43],[-84.312,-40.725],[-82,-39],[-81.502,-37.509],[-81,-36],[-77.959,-32.947],[-75,-30],[-71.923,-26.481],[-69,-23],[-58,-8],[-60,-2],[-51,12],[-38,-5],[-48,-30],[-76,-62],[-84,-85],[-77,-92],[-60,-87],[-57,-85],[-55,-81],[-38,-88],[-65,-111]],"c":true}],"h":1},{"t":28,"s":[{"i":[[9.692,-1.405],[1.755,-0.653],[1.212,-0.989],[0.612,-0.799],[1.064,-1.588],[0.956,-1.834],[0.829,-2.651],[-1.471,-5.599],[-2.124,-4.18],[-2.534,-3.99],[-2.617,-3.613],[-1.064,-1.14],[-0.856,-1.216],[-1.667,-2.958],[-0.212,-2.155],[0.684,-2.633],[-1.441,-2.449],[-1.846,-1.035],[-3.412,0.49],[-1.764,4.555],[0.994,6.213],[1.271,2.573],[1.579,2.614],[5.214,6.995],[2.346,5.732],[-9.454,1.216],[-1.712,-1.097],[-10.861,3.613],[-0.352,2.926],[4.889,4.64]],"o":[[-2.767,0.401],[-1.755,0.653],[-1.489,1.215],[-0.612,0.799],[-1.348,2.013],[-0.956,1.834],[-2.008,6.423],[1.471,5.599],[2.249,4.426],[2.534,3.99],[0.888,1.226],[1.064,1.14],[2.12,3.012],[1.667,2.958],[0.297,3.021],[-0.684,2.633],[0.206,0.35],[1.846,1.035],[3.939,-0.566],[1.764,-4.555],[-0.335,-2.095],[-1.271,-2.573],[-3.799,-6.286],[-5.214,-6.995],[-3.248,-7.936],[1.752,-0.225],[5.75,3.685],[2.926,-0.973],[0.526,-4.371],[-6.865,-6.516]],"v":[[-63,-111],[-69.666,-109.441],[-74,-107],[-76.82,-104.28],[-79,-101],[-82.389,-95.478],[-85,-89],[-85.099,-70.817],[-79,-56],[-71.776,-43.39],[-64,-32],[-60.976,-28.493],[-58,-25],[-52.068,-15.857],[-49,-8],[-50.358,0.429],[-50,8],[-46.904,10.63],[-39,12],[-30.301,3.736],[-29,-13],[-31.567,-20.11],[-36,-28],[-50.589,-48.416],[-63,-68],[-59,-92],[-53,-89],[-34,-79],[-28,-87],[-36,-101]],"c":true}],"h":1},{"t":29,"s":[{"i":[[-2.683,7.317],[-0.869,-1.25],[-0.8,-0.658],[-1.031,-0.204],[-1.563,0.111],[-1.735,1.264],[-0.45,1.828],[3.728,4.807],[2.323,0.76],[3.865,-2.557],[1.585,-3.719],[-0.715,-8.537],[-2.363,-6.29],[-1.877,-4.04],[-1.844,-4.454],[-1.186,-3.102],[-0.134,-2.887],[0.496,-1.449],[0.053,-1.394],[-1.552,-2.05],[-2.93,-0.213],[-1.523,0.346],[-0.883,0.689],[-0.327,1.358],[-0.442,1.593],[1.215,6.113],[1.85,4.387],[0.684,1.501],[0.533,1.264],[2.41,7.866]],"o":[[1.239,1.98],[0.869,1.25],[0.8,0.658],[1.031,0.204],[1.61,-0.114],[1.735,-1.264],[1.234,-5.011],[-3.728,-4.807],[-5.956,-1.948],[-3.865,2.557],[-3.224,7.564],[0.715,8.537],[1.648,4.387],[1.877,4.04],[1.12,2.706],[1.186,3.102],[0.038,0.811],[-0.496,1.449],[-0.136,3.585],[1.552,2.05],[1.025,0.075],[1.523,-0.346],[1.249,-0.974],[0.327,-1.358],[1.71,-6.16],[-1.215,-6.113],[-0.73,-1.733],[-0.684,-1.501],[-3.044,-7.221],[-2.41,-7.866]],"v":[[-33,-87],[-29.914,-82.19],[-27.486,-79.364],[-24.816,-78.105],[-21,-78],[-15.63,-80.215],[-12,-85],[-17.332,-100.689],[-28,-110],[-42.778,-108.25],[-51,-98],[-54.191,-73.044],[-49,-50],[-43.647,-37.55],[-38,-25],[-34.26,-16.136],[-32,-7],[-32.932,-3.437],[-34,1],[-31.799,9.529],[-25,13],[-20.893,12.572],[-17,11],[-14.895,7.464],[-14,3],[-13.83,-15.829],[-19,-32],[-21.148,-36.851],[-23,-41],[-32.295,-63.928]],"c":true}],"h":1},{"t":30,"s":[{"i":[[3.333,-0.976],[1.133,-1.074],[0.545,-1.572],[0.264,-2.087],[0.291,-2.619],[-0.238,-6.751],[-0.732,-6.624],[-0.753,-6.186],[-0.301,-5.438],[0.011,-2.415],[-0.042,-2.238],[-0.282,-1.704],[-0.709,-0.814],[-1.027,-0.436],[-1.371,-0.226],[-1.409,0.198],[-1.139,0.835],[0.122,7.953],[0.958,8.662],[0.318,3.74],[0.375,3.844],[0.571,4.011],[-0.7,2.69],[-0.723,0.668],[-0.748,0.937],[-0.514,0.926],[-0.22,1.386],[0.663,2.516],[0.717,1.6],[2.195,1.193]],"o":[[-2.029,0.594],[-1.133,1.074],[-0.545,1.572],[-0.264,2.087],[-0.729,6.568],[0.238,6.751],[0.732,6.624],[0.753,6.186],[0.124,2.236],[-0.011,2.415],[0.042,2.238],[0.282,1.704],[0.377,0.433],[1.027,0.436],[1.371,0.226],[1.409,-0.198],[3.447,-2.528],[-0.122,-7.953],[-0.353,-3.197],[-0.318,-3.74],[-0.416,-4.266],[-0.571,-4.011],[0.361,-1.39],[0.723,-0.668],[0.726,-0.91],[0.514,-0.926],[0.37,-2.338],[-0.663,-2.516],[-1.885,-4.206],[-2.195,-1.193]],"v":[[-11,-109],[-15.667,-106.502],[-18.108,-102.537],[-19.245,-97.054],[-20,-90],[-20.619,-69.944],[-19.046,-49.805],[-16.7,-30.513],[-15,-13],[-14.877,-5.936],[-14.877,1.132],[-14.438,7.133],[-13,11],[-10.818,12.357],[-7.145,13.403],[-2.899,13.497],[1,12],[5.304,-4.9],[3,-31],[2.016,-41.514],[1,-53],[-0.837,-65.682],[-1,-76],[0.711,-78.84],[3,-81],[4.88,-83.643],[6,-87],[5.316,-94.554],[3,-101],[-2.914,-108.886]],"c":true}],"h":1},{"t":31,"s":[{"i":[[3.441,-0.647],[1.283,-0.953],[0.833,-1.422],[0.508,-1.758],[0.31,-1.96],[-0.087,-1.457],[-0.408,-1.039],[-0.617,-0.938],[-0.714,-1.153],[-0.141,-1.047],[0.131,-1.095],[0.206,-1.125],[0.086,-1.137],[0.415,-3.836],[0.451,-3.58],[0.436,-3.579],[0.369,-3.834],[0.163,-4.005],[-0.565,-3.399],[-1.82,-1.944],[-3.604,0.359],[-1.357,1.89],[-0.311,2.89],[0.076,3.305],[-0.195,3.135],[-0.864,6.178],[-0.881,7.07],[-0.327,7.107],[0.798,6.288],[2.455,2.453]],"o":[[-1.859,0.35],[-1.283,0.953],[-0.833,1.422],[-0.508,1.758],[-0.346,2.191],[0.087,1.457],[0.408,1.039],[0.617,0.938],[0.608,0.981],[0.141,1.047],[-0.131,1.095],[-0.206,1.125],[-0.328,4.346],[-0.415,3.836],[-0.451,3.58],[-0.436,3.579],[-0.363,3.764],[-0.163,4.005],[0.565,3.399],[1.82,1.944],[3.06,-0.305],[1.357,-1.89],[0.311,-2.89],[-0.076,-3.305],[0.275,-4.432],[0.864,-6.178],[0.881,-7.07],[0.327,-7.107],[-0.741,-5.836],[-2.455,-2.453]],"v":[[11,-109],[6.318,-107.013],[3.176,-103.416],[1.196,-98.612],[0,-93],[-0.361,-87.608],[0.409,-83.943],[1.975,-81.057],[4,-78],[5.074,-74.953],[5.041,-71.735],[4.487,-68.399],[4,-65],[2.874,-52.791],[1.563,-41.731],[0.22,-31.056],[-1,-20],[-1.921,-8.134],[-1.45,3.184],[1.995,11.41],[10,14],[16.461,10.561],[18.798,3.245],[18.986,-6.194],[19,-16],[20.851,-32.129],[23.61,-52.216],[25.564,-73.694],[25,-94],[20.025,-106.362]],"c":true}],"h":1},{"t":32,"s":[{"i":[[10.012,-0.857],[0.663,-0.066],[0.696,-0.133],[0.685,-0.25],[0.629,-0.419],[1.161,-1.785],[0.997,-2.24],[0.588,-2.086],[-0.068,-1.321],[-1.507,-1.219],[-0.877,-0.316],[-1.658,-0.093],[-0.11,-0.173],[0.768,-3.552],[0.803,-3.414],[1.144,-4.168],[1.192,-4.1],[1.234,-4.969],[-0.056,-4.277],[-2.293,-3.72],[-5.767,1.698],[-1.257,0.847],[-0.419,0.742],[0.306,2.867],[-0.16,2.647],[-0.757,2.689],[-0.84,3.026],[-1.59,5.162],[-1.1,4.811],[1.605,11.555]],"o":[[-0.585,0.05],[-0.663,0.066],[-0.696,0.133],[-0.685,0.25],[-1.079,0.718],[-1.161,1.785],[-0.997,2.24],[-0.588,2.086],[0.093,1.806],[1.507,1.219],[1.258,0.453],[1.658,0.093],[0.876,1.379],[-0.768,3.552],[-1.23,5.23],[-1.144,4.168],[-1.119,3.847],[-1.234,4.969],[0.062,4.817],[2.293,3.72],[-0.445,0.131],[1.257,-0.847],[1.063,-1.884],[-0.306,-2.867],[0.134,-2.219],[0.757,-2.689],[1.538,-5.541],[1.59,-5.162],[2.433,-10.639],[-1.605,-11.555]],"v":[[27,-109],[25.116,-108.839],[23.066,-108.553],[20.982,-107.991],[19,-107],[15.579,-103.093],[12.28,-96.903],[9.841,-90.262],[9,-85],[11.912,-80.383],[16,-78],[20.861,-77.29],[24,-77],[23.76,-69.026],[21,-58],[17.471,-44.153],[14,-32],[10.119,-18.323],[8,-4],[11.222,9.887],[23,14],[24.852,12.655],[28,10],[28.678,2.572],[28,-6],[29.471,-13.395],[32,-22],[36.828,-38.047],[41,-53],[43.334,-89.622]],"c":true}],"h":1},{"t":33,"s":[{"i":[[-1.692,-0.766],[1.471,-6.219],[2.553,-5.943],[1.166,-2.647],[1.155,-2.664],[1.283,-2.686],[0.627,-2.395],[-0.895,-5.6],[-2.8,-1.331],[-2.452,0.856],[-0.736,1.221],[0.438,2.954],[-0.248,2.689],[-1.182,2.599],[-0.96,2.214],[-0.473,1.359],[-0.575,1.331],[-0.379,0.478],[-0.227,0.489],[-0.098,0.91],[-0.285,0.66],[-1.26,2.459],[-0.88,2.66],[-0.699,3.206],[-0.248,3.282],[3.214,7.139],[7.793,-0.124],[1.212,-0.836],[-16.354,-1.393],[-1.724,3.235]],"o":[[1.434,6.183],[-1.471,6.218],[-1.18,2.747],[-1.166,2.647],[-1.24,2.86],[-1.283,2.686],[-1.379,5.271],[0.895,5.6],[3.456,1.643],[2.452,-0.856],[1.506,-2.497],[-0.438,-2.954],[0.312,-3.39],[1.182,-2.599],[0.535,-1.233],[0.473,-1.359],[0.258,-0.599],[0.379,-0.478],[0.325,-0.701],[0.098,-0.91],[1.079,-2.504],[1.26,-2.459],[0.958,-2.895],[0.698,-3.206],[0.647,-8.57],[-3.214,-7.139],[-2.471,0.04],[-7.887,5.438],[7.4,0.63],[0.183,-0.343]],"v":[[38,-85],[37.49,-66.32],[31,-48],[27.481,-39.938],[24,-32],[20.04,-23.651],[17,-16],[16.366,1.455],[22,13],[31.04,13.648],[36,10],[36.944,1.644],[36,-7],[38.514,-15.882],[42,-23],[43.47,-26.927],[45,-31],[46.023,-32.582],[47,-34],[47.53,-36.53],[48,-39],[51.649,-46.383],[55,-54],[57.532,-63.209],[59,-73],[55.33,-98.02],[39,-110],[28,-106],[26,-77],[36,-83]],"c":true}],"h":1},{"t":34,"s":[{"i":[[14.095,-1.272],[0.937,-0.322],[0.989,-0.507],[0.677,-0.062],[0.614,-0.406],[1.107,-1.421],[0.922,-1.224],[0.408,-0.14],[0.201,-0.216],[0.756,-2.55],[-0.329,-1.4],[-2.06,-1.454],[-3.378,0.441],[-2.23,3.005],[-1.843,-1.444],[-0.405,-1.932],[-0.025,-1.995],[0.392,-1.914],[0.456,-1.6],[2.132,-3.599],[2.472,-4.571],[0.616,-1.544],[0.771,-1.406],[0.656,-1.147],[0.679,-1.473],[-14.013,-1.683],[-0.906,4.713],[-0.286,3.517],[-3.246,5.959],[-1.098,19.932]],"o":[[-2.264,0.204],[-0.937,0.322],[-0.678,0.347],[-0.677,0.062],[-1.651,1.092],[-1.107,1.421],[-0.169,0.224],[-0.407,0.14],[-1.305,1.401],[-0.756,2.55],[0.382,1.624],[2.06,1.454],[3.235,-0.423],[2.23,-3.005],[0.353,0.276],[0.405,1.932],[0.022,1.745],[-0.392,1.914],[-2.002,7.031],[-2.132,3.6],[-0.767,1.416],[-0.616,1.544],[-0.631,1.151],[-0.757,1.324],[-5.008,10.858],[6.28,0.754],[0.78,-4.06],[0.405,-4.988],[8.39,-15.404],[1.187,-21.552]],"v":[[47,-109],[42.544,-108.226],[40,-107],[37.952,-106.544],[36,-106],[31.953,-102.099],[29,-98],[28.025,-97.494],[27,-97],[23.774,-90.499],[23,-84],[26.753,-78.951],[35,-77],[43.044,-83.9],[49,-88],[50.246,-84.288],[51,-78],[50.358,-72.392],[49,-67],[42.853,-52.155],[36,-41],[34.003,-36.492],[32,-32],[29,-29],[27,-24],[32,14],[44,6],[42,-6],[49,-23],[71,-75]],"c":true}],"h":1},{"t":35,"s":[{"i":[[8.043,-0.886],[1.416,-0.578],[2.278,-1.352],[1.236,-0.627],[0.831,-0.763],[0.311,-0.547],[0.347,-0.438],[0.627,-2.982],[-3.135,-2.767],[-3.115,2.252],[-2.844,2.136],[-1.146,-0.796],[-0.7,-3.858],[2.241,-5.103],[2.163,-3.381],[0.336,-0.745],[0.319,-0.495],[4.168,-7.075],[0.392,-6.585],[-1.84,-4.18],[-4.893,0],[-1.309,1.377],[-0.485,1.491],[0.519,2.006],[-0.269,2.498],[-1.51,2.797],[-1.745,2.835],[-3.084,4.604],[-2.081,10.716],[5.726,6.344]],"o":[[-3.056,0.337],[-1.416,0.578],[-1.225,0.727],[-1.236,0.627],[-0.408,0.374],[-0.311,0.547],[-2.626,3.307],[-0.627,2.981],[4.694,4.144],[3.115,-2.252],[2.716,-2.041],[1.146,0.796],[1.126,6.206],[-2.241,5.103],[-0.337,0.526],[-0.336,0.745],[-3.936,6.116],[-4.168,7.075],[-0.255,4.279],[1.841,4.18],[3.279,0],[1.309,-1.377],[0.806,-2.478],[-0.519,-2.006],[0.213,-1.978],[1.511,-2.797],[3.564,-5.791],[5.859,-8.748],[2.529,-13.025],[-5.149,-5.706]],"v":[[54,-109],[47.916,-107.762],[43,-105],[39.204,-103.026],[36,-101],[34.955,-99.548],[34,-98],[28.68,-88.595],[32,-80],[43.387,-78.79],[52,-87],[57.513,-88.924],[60,-82],[57.466,-64.882],[50,-52],[48.986,-49.976],[48,-48],[34.842,-27.852],[27,-7],[29.139,6.709],[39,14],[45.596,11.618],[48,7],[47.903,0.515],[47,-6],[49.851,-13.357],[55,-22],[67,-40],[80,-70],[72,-102]],"c":true}],"h":1},{"t":36,"s":[{"i":[[6.95,-0.74],[2.905,-1.098],[1.338,-1.282],[2.561,-3.354],[-1.377,-4.54],[-1.524,-1.099],[-2.451,-0.109],[-1.063,0.784],[-0.925,0.989],[-2.672,1.205],[-0.427,0.231],[-1.115,-0.377],[-0.221,-2.138],[2.655,-4.292],[0.852,-1.278],[0.353,-0.47],[0.554,-0.76],[0.376,-0.501],[0.552,-0.759],[1.638,-2.261],[2.119,-8.747],[-11.571,0.349],[0.102,-0.212],[-0.6,5.533],[-3.865,5.614],[-3.36,5.017],[-2.959,7.022],[-0.68,2.933],[8.858,3.993],[0.432,0.229]],"o":[[-3.041,0.324],[-2.905,1.098],[-2.132,2.042],[-2.561,3.354],[0.454,1.496],[1.524,1.099],[2.821,0.126],[1.064,-0.784],[2.484,-2.655],[0.357,-0.161],[1.608,-0.869],[1.776,0.601],[0.807,7.818],[-0.965,1.561],[-0.298,0.447],[-0.636,0.848],[-0.344,0.472],[-0.635,0.846],[-1.758,2.419],[-5.704,7.876],[-2.125,8.771],[5.64,-0.17],[2.416,-5.024],[0.401,-3.692],[4.493,-6.525],[4.477,-6.685],[1.175,-2.788],[3.523,-15.198],[-0.36,-0.162],[-3.543,-1.875]],"v":[[60,-109],[50.723,-106.719],[44,-103],[35.868,-94.873],[33,-83],[36.002,-78.96],[42,-77],[47.422,-78.164],[50,-81],[56,-86],[57,-88],[64,-90],[69,-80],[60,-57],[58,-52],[56,-51],[55,-48],[53,-47],[52,-44],[46,-37],[32,-11],[44,14],[53,9],[51,-6],[60,-21],[73,-39],[84,-59],[88,-68],[76,-105],[75,-107]],"c":true}],"h":1},{"t":37,"s":[{"i":[[6.233,-0.744],[3.673,-1.795],[2.536,-2.251],[1.463,-2.357],[-0.21,-2.367],[-1.895,-1.574],[-2.184,-0.066],[-3.996,3.091],[-3.467,-1.045],[-0.259,-4.087],[3.554,-5.557],[3.616,-4.531],[1.087,-12.139],[-9.903,1.954],[-0.682,3.684],[-0.71,4.488],[-0.964,1.734],[-0.939,1.446],[-0.345,0.461],[-0.554,0.76],[-0.376,0.501],[-0.596,0.775],[-1.455,2.193],[-0.623,0.94],[-0.623,0.94],[-0.506,0.737],[-0.962,1.445],[-0.981,1.538],[0.282,9.538],[7.628,4.07]],"o":[[-4.773,0.57],[-3.673,1.795],[-1.358,1.206],[-1.463,2.357],[0.234,2.637],[1.896,1.574],[6.258,0.189],[4.147,-3.208],[2.255,0.68],[0.397,6.272],[-3.899,6.097],[-9.421,11.805],[-1.145,12.792],[3.806,-0.751],[0.803,-4.341],[0.086,-0.546],[0.913,-1.643],[0.285,-0.438],[0.636,-0.848],[0.344,-0.472],[0.653,-0.87],[2.166,-2.819],[1.103,-1.662],[1.103,-1.662],[0.595,-0.897],[0.809,-1.177],[0.873,-1.312],[4.282,-6.717],[-0.374,-12.63],[-4.306,-2.297]],"v":[[65,-109],[52.323,-105.261],[43,-99],[38.324,-93.371],[36,-86],[39.537,-79.572],[46,-77],[57,-84],[70,-90],[76,-79],[68,-59],[56,-43],[35,-8],[50,14],[57,6],[55,-7],[58,-11],[60,-16],[62,-17],[63,-20],[65,-21],[66,-24],[74,-33],[77,-37],[80,-41],[82,-43],[84,-48],[87,-52],[96,-79],[81,-107]],"c":true}],"h":1},{"t":38,"s":[{"i":[[1.647,-0.211],[3.205,-1.278],[3.453,-2.54],[2.196,-2.853],[-1.45,-4.627],[-1.536,-1.121],[-2.416,-0.112],[-2.608,2.496],[-2.576,1.201],[-2.905,-1.487],[-0.312,-4.948],[1.855,-3.591],[1.77,-2.606],[2.135,-2.77],[2.131,-2.486],[4.115,-5.865],[0,-7.076],[-9.381,0.969],[0.945,4.244],[-0.331,2.895],[-2.557,3.213],[-1.725,2.071],[-2.393,3.051],[-0.908,1.137],[-1.532,2.219],[-0.672,1.075],[-1.198,3.937],[3.55,7.75],[6.098,3.388],[1.056,0.329]],"o":[[-2.727,0.349],[-3.205,1.278],[-2.665,1.96],[-2.196,2.853],[0.439,1.403],[1.536,1.121],[3.993,0.185],[2.608,-2.496],[4.069,-1.897],[2.905,1.487],[0.191,3.028],[-1.855,3.591],[-2.331,3.432],[-2.135,2.77],[-5.541,6.463],[-4.115,5.865],[0,10.245],[8.784,-0.907],[-0.459,-2.063],[0.63,-5.51],[1.77,-2.224],[3.326,-3.993],[0.909,-1.159],[1.718,-2.153],[0.596,-0.863],[2.908,-4.652],[3.053,-10.033],[-3.703,-8.083],[-0.968,-0.538],[-4.187,-1.303]],"v":[[69,-109],[60.044,-106.643],[50,-101],[41.914,-94.001],[40,-83],[43.017,-79.032],[49,-77],[58.562,-81.461],[66,-88],[76.817,-88.634],[82,-79],[78.971,-68.684],[73,-59],[66.35,-49.791],[60,-42],[44.844,-23.959],[38,-5],[52,14],[60,1],[58,-6],[68,-20],[73,-27],[82,-37],[84,-41],[90,-47],[92,-51],[100,-66],[98,-93],[86,-106],[82,-109]],"c":true}],"h":1},{"t":39,"s":[{"i":[[6.89,-0.815],[3.45,-1.257],[3.629,-2.669],[2.123,-2.867],[-1.331,-4.445],[-1.321,-1.204],[-2.45,-0.294],[-2.437,1.703],[-1.607,-13.824],[3.698,-5.29],[0.365,-0.487],[0.611,-0.78],[0.417,-0.448],[0.686,-0.803],[2.043,-2.273],[2.527,-10.638],[-10.055,1.039],[-0.351,0.734],[-0.86,5.47],[-1.778,2.471],[-3.623,4.033],[-1.799,1.999],[-1.727,2.054],[-0.83,1.103],[-0.403,0.433],[-0.679,0.936],[-0.711,1.062],[0.53,11.664],[4.486,4.612],[1.94,1.026]],"o":[[-2.573,0.304],[-3.45,1.257],[-2.84,2.088],[-2.123,2.867],[0.267,0.889],[1.321,1.204],[6.346,0.762],[8.352,-5.838],[0.651,5.598],[-0.322,0.461],[-0.659,0.878],[-0.361,0.46],[-0.697,0.749],[-2.476,2.9],[-10.39,11.564],[-3.517,14.807],[4.645,-0.48],[2.721,-5.69],[0.214,-1.358],[3.702,-5.144],[2.269,-2.525],[1.844,-2.05],[0.988,-1.175],[0.337,-0.448],[0.745,-0.8],[0.829,-1.142],[4.953,-7.395],[-0.454,-9.995],[-2.485,-2.555],[-5.035,-2.663]],"v":[[73,-109],[63.792,-106.773],[53,-101],[44.872,-93.767],[43,-83],[45.362,-79.554],[51,-77],[64,-84],[87,-80],[79,-60],[77,-59],[76,-56],[74,-55],[73,-52],[66,-45],[42,-12],[55,14],[63,9],[61,-7],[66,-14],[77,-27],[83,-34],[89,-40],[91,-44],[93,-45],[94,-48],[97,-51],[107,-80],[97,-101],[91,-107]],"c":true}],"h":1},{"t":40,"s":[{"i":[[4.56,-0.479],[2.024,-0.262],[1.95,-0.748],[0.607,-0.565],[0.799,-0.444],[1.906,-1.337],[1.585,-2.101],[0.779,-1.504],[-0.362,-2.331],[-4.248,-0.316],[-3.926,2.486],[-1.573,-12.837],[4.023,-5.229],[6.173,-6.924],[2.693,-4.652],[0.019,-5.058],[-8.53,1.272],[-0.963,3.04],[0.206,2.491],[-0.396,3.016],[-1.573,2.228],[-3.55,3.963],[-1.701,1.835],[-3.405,4.389],[-0.712,0.971],[-1.534,2.568],[-1.537,4.72],[-0.22,2.206],[6.333,4.201],[0.665,0.404]],"o":[[-1.954,0.206],[-2.024,0.262],[-0.772,0.296],[-0.607,0.565],[-2.79,1.549],[-1.906,1.337],[-1.247,1.653],[-0.779,1.504],[0.802,5.157],[6.652,0.495],[9.988,-6.325],[0.794,6.479],[-5.783,7.516],[-4.146,4.65],[-2.608,4.505],[-0.039,10.342],[5.007,-0.747],[0.615,-1.941],[-0.203,-2.447],[0.19,-1.452],[3.567,-5.052],[2.286,-2.552],[3.581,-3.864],[0.608,-0.783],[2.013,-2.745],[2.329,-3.899],[0.731,-2.245],[1.388,-13.905],[-0.823,-0.546],[-5.657,-3.436]],"v":[[77,-109],[70.996,-108.407],[65,-107],[63.02,-105.611],[61,-104],[54.097,-99.914],[49,-95],[45.793,-90.508],[45,-85],[54,-77],[67,-85],[91,-80],[82,-59],[60,-35],[48,-20],[43,-5],[58,14],[65,7],[66,2],[63,-6],[68,-13],[79,-26],[85,-33],[97,-45],[99,-49],[104,-56],[109,-67],[111,-74],[99,-104],[97,-106]],"c":true}],"h":1},{"t":41,"s":[{"i":[[5.891,-0.619],[7.054,-4.41],[-0.705,-7.194],[-5.031,-0.092],[-2.824,1.828],[-0.657,0.394],[-2.013,0.636],[-2.125,-0.552],[-0.418,-5.349],[2.56,-3.348],[0.67,-0.82],[2.544,-2.78],[1.253,-1.437],[1.207,-1.257],[0,-14.229],[-8.933,0.442],[-0.698,1.155],[0.579,3.606],[-0.339,2.536],[-0.446,0.479],[-0.686,0.877],[-0.919,0.987],[-0.682,0.766],[-5.785,6.618],[-0.808,0.985],[-0.432,0.464],[-0.687,0.872],[-1.654,2.521],[-1.364,6.519],[8.91,4.888]],"o":[[-6.357,0.668],[-5.62,3.514],[0.498,5.084],[5.931,0.109],[0.694,-0.449],[1.764,-1.058],[2.338,-0.739],[2.344,0.608],[0.506,6.467],[-0.612,0.801],[-2.183,2.67],[-1.231,1.345],[-1.246,1.429],[-12.272,12.774],[0,8.527],[2.795,-0.138],[0.685,-1.133],[-0.352,-2.197],[0.226,-1.696],[0.726,-0.779],[1.372,-1.753],[0.681,-0.732],[7.305,-8.207],[0.724,-0.829],[0.388,-0.473],[0.724,-0.777],[1.893,-2.404],[3.54,-5.395],[3.454,-16.502],[-6.286,-3.448]],"v":[[80,-109],[59,-102],[47,-86],[57,-77],[68,-84],[70,-85],[77,-89],[87,-90],[95,-79],[86,-61],[85,-58],[77,-51],[74,-46],[70,-42],[45,-5],[59,14],[67,10],[68,1],[65,-6],[69,-11],[70,-14],[75,-18],[76,-21],[96,-40],[98,-44],[100,-45],[101,-48],[107,-55],[114,-71],[99,-106]],"c":true}],"h":1},{"t":42,"s":[{"i":[[5.581,-0.632],[7.165,-4.053],[-1.142,-7.166],[-1.553,-1.381],[-2.421,-0.18],[-1.8,1.565],[-1.521,0.984],[-3.539,1.312],[-2.811,-0.562],[-1.738,-1.872],[-0.263,-3.155],[2.61,-4.713],[9.48,-10.878],[1.233,-1.504],[1.8,-3.22],[0.368,-5.272],[-1.843,-2.869],[-6.806,1.534],[-0.796,2.513],[0.246,2.439],[-0.489,3.636],[-0.368,0.49],[-0.662,0.792],[-3.438,3.791],[-5.613,8.462],[-1.561,2.724],[-1.17,6.131],[7.022,5.403],[0.481,0.217],[0.411,0.238]],"o":[[-5.48,0.621],[-7.166,4.053],[0.366,2.296],[1.553,1.381],[3.279,0.244],[1.8,-1.565],[2.031,-1.314],[3.54,-1.312],[1.785,0.357],[1.738,1.872],[0.371,4.452],[-8.341,15.06],[-1.324,1.52],[-2.438,2.974],[-1.597,2.857],[-0.327,4.68],[1.338,2.084],[3.671,-0.828],[0.639,-2.018],[-0.224,-2.216],[0.192,-1.43],[0.677,-0.903],[3.676,-4.395],[7.576,-8.354],[1.656,-2.497],[2.724,-4.753],[2.594,-13.588],[-0.41,-0.315],[-0.349,-0.157],[-5.679,-3.284]],"v":[[82,-109],[60.534,-101.908],[49,-85],[51.959,-79.413],[58,-77],[65.318,-79.579],[70,-84],[78.915,-88.407],[89,-90],[94.642,-86.599],[98,-79],[94,-68],[61,-32],[58,-27],[51,-19],[47,-7],[50,8],[63,14],[69,7],[70,2],[67,-6],[71,-11],[72,-14],[85,-27],[108,-52],[112,-59],[117,-72],[106,-104],[104,-104],[103,-106]],"c":true}],"h":1},{"t":43,"s":[{"i":[[4.927,-0.558],[3.542,-1.123],[3.644,-2.146],[2.59,-2.62],[-0.351,-3.759],[-1.728,-1.527],[-1.838,-0.203],[-1.693,1.27],[-1.334,1.242],[-1.264,0.661],[-0.923,0.407],[-1.021,0.334],[-1.016,0.265],[-2.77,-1.549],[-1.102,-4.061],[2.077,-3.671],[2.572,-3.066],[2.696,-2.589],[1.977,-1.977],[2.091,-10.911],[-10.779,1.307],[-1.057,1.787],[-1.051,5.198],[-2.459,2.941],[-0.93,1.141],[-3.209,2.975],[-2.158,2.158],[-2.093,2.308],[-2.789,7.123],[11.877,6.689]],"o":[[-3.189,0.361],[-3.542,1.123],[-2.991,1.761],[-2.59,2.62],[0.251,2.69],[1.728,1.527],[2.895,0.32],[1.693,-1.27],[1.02,-0.95],[1.264,-0.661],[0.9,-0.398],[1.021,-0.334],[4.816,-1.257],[2.77,1.549],[0.882,3.25],[-2.077,3.671],[-2.239,2.668],[-2.696,2.589],[-9.81,9.81],[-2.071,10.81],[1.08,-0.131],[3.745,-6.328],[0.488,-2.412],[0.944,-1.129],[2.981,-3.657],[2.545,-2.359],[2.037,-2.037],[5.832,-6.43],[6.585,-16.816],[-5.88,-3.312]],"v":[[84,-109],[73.841,-106.839],[63,-102],[53.993,-95.498],[50,-86],[53.309,-79.635],[59,-77],[65.671,-78.829],[70,-83],[73.573,-85.407],[77,-87],[79.913,-88.099],[83,-89],[94.285,-88.489],[100,-80],[97.591,-69.362],[90,-59],[82.303,-50.981],[75,-44],[49,-10],[63,14],[70,10],[69,-7],[75,-14],[77,-18],[87,-28],[95,-34],[101,-41],[118,-64],[105,-106]],"c":true}],"h":1},{"t":44,"s":[{"i":[[5.115,-0.566],[0.943,-0.154],[1.191,-0.267],[0.914,-0.327],[1.32,-0.463],[1.941,-0.604],[1.595,-1.01],[0.14,-0.424],[0.226,-0.169],[0.793,-0.671],[0.484,-0.704],[-9.232,-0.443],[-2.197,1.257],[-5.522,-1.014],[-0.547,-6.508],[2.724,-4.107],[3.049,-2.882],[3.51,-5.292],[1.328,-2.294],[0.363,-1.894],[-1.376,-2.868],[-5.947,0.179],[-0.63,1.987],[-1.123,6.386],[-1.55,1.785],[-9.427,10.506],[-1.063,1.345],[-1.763,2.75],[0.099,8.822],[7.131,4.016]],"o":[[-1.038,0.115],[-0.943,0.154],[-1.026,0.231],[-0.914,0.327],[-1.639,0.575],[-1.941,0.604],[-0.215,0.136],[-0.14,0.424],[-0.907,0.68],[-1.156,0.978],[-5.414,7.879],[4.964,0.238],[4.374,-2.503],[3.374,0.62],[0.679,8.082],[-3.097,4.669],[-5.604,5.298],[-2.105,3.173],[-1.447,2.499],[-1.606,8.373],[2.312,4.819],[6.404,-0.193],[2.077,-6.55],[0.135,-0.768],[10.333,-11.897],[0.89,-0.991],[2.309,-2.921],[3.774,-5.887],[-0.135,-11.968],[-6.041,-3.402]],"v":[[86,-109],[83.114,-108.614],[80,-108],[77.22,-107.174],[74,-106],[68.467,-104.326],[63,-102],[62.508,-101.025],[62,-100],[59,-99],[55,-95],[61,-77],[75,-85],[93,-90],[103,-79],[91,-58],[78,-44],[61,-28],[54,-19],[50,-11],[52,6],[63,14],[72,7],[70,-7],[75,-14],[107,-45],[109,-49],[115,-56],[123,-79],[107,-106]],"c":true}],"h":1},{"t":45,"s":[{"i":[[0.261,-0.024],[8.017,-4.31],[-2.441,-8.274],[-1.328,-1.18],[-2.397,-0.288],[-2.528,1.81],[-1.946,1.114],[-3.131,1.036],[-3.078,-0.516],[-1.994,-1.612],[-0.453,-3.289],[2.299,-3.506],[2.393,-2.681],[2.722,-2.635],[2.206,-2.206],[2.769,-2.663],[2.362,-2.786],[2.18,-3.757],[0.046,-4.341],[-2.666,-3.677],[-4.385,0.463],[1.056,5.63],[-0.76,4],[-2.669,2.987],[-6.78,6.435],[-3.89,11.92],[3.755,5.161],[2.881,1.299],[0.428,0.231],[7.572,0.345]],"o":[[-5.371,0.485],[-8.018,4.31],[0.293,0.992],[1.328,1.18],[2.942,0.353],[2.528,-1.81],[2.397,-1.371],[3.131,-1.036],[1.571,0.263],[1.994,1.612],[0.59,4.296],[-2.299,3.506],[-2.904,3.254],[-2.722,2.635],[-2.564,2.564],[-2.769,2.663],[-2.232,2.632],[-2.181,3.757],[-0.05,4.755],[2.666,3.677],[7.589,-0.801],[-0.441,-2.352],[0.115,-0.608],[6.683,-7.478],[10.619,-10.079],[3.595,-11.016],[-2.432,-3.343],[-0.358,-0.161],[-4.269,-2.305],[-0.985,-0.045]],"v":[[88,-109],[64.641,-101.842],[53,-83],[55.422,-79.472],[61,-77],[69.247,-79.9],[76,-85],[84.489,-88.916],[94,-90],[99.839,-87.269],[104,-80],[100.738,-68.289],[93,-59],[84.476,-50.214],[77,-43],[68.849,-35.167],[61,-27],[53.861,-17.282],[50,-5],[54.174,8.414],[65,14],[73,1],[71,-7],[77,-15],[99,-36],[123,-69],[118,-97],[110,-104],[109,-106],[90,-110]],"c":true}],"h":1},{"t":46,"s":[{"i":[[0.261,-0.024],[3.609,-1.108],[3.998,-2.245],[-0.756,-8.148],[-4.586,-0.172],[-3.519,2.276],[-3.334,0.953],[-4.742,-2.039],[-0.147,-6.015],[1.516,-2.355],[5.212,-4.928],[2.644,-2.52],[2.302,-2.807],[1.252,-1.683],[0.027,-7.326],[-9.201,1.116],[1.156,7.206],[-0.386,2.527],[-2.006,2.278],[-4.659,4.126],[-1.031,0.939],[-4.081,4.977],[-0.432,0.464],[-0.685,0.891],[-1.167,2.216],[0.124,6.681],[1.678,2.899],[2.982,2.713],[0.823,0.444],[6.03,0.275]],"o":[[-3.566,0.322],[-3.609,1.108],[-6.202,3.483],[0.531,5.729],[7.273,0.272],[2.833,-1.832],[3.97,-1.135],[1.682,0.723],[0.086,3.512],[-5.448,8.46],[-2.964,2.802],[-3.241,3.089],[-1.422,1.734],[-3.514,4.726],[-0.035,9.359],[6.251,-0.758],[-0.396,-2.471],[0.249,-1.627],[4.982,-5.658],[1.278,-1.132],[5.173,-4.711],[0.388,-0.473],[0.73,-0.785],[2.117,-2.754],[2.635,-5.004],[-0.117,-6.315],[-2.072,-3.579],[-2.029,-1.847],[-4.47,-2.413],[-0.985,-0.045]],"v":[[89,-109],[78.324,-106.942],[67,-102],[53,-86],[63,-77],[73,-83],[84,-88],[99,-89],[106,-78],[101,-67],[78,-42],[69,-35],[62,-26],[58,-22],[51,-5],[66,14],[74,1],[71,-6],[77,-13],[93,-29],[97,-32],[112,-49],[114,-50],[115,-53],[121,-60],[126,-79],[121,-94],[115,-101],[110,-106],[91,-110]],"c":true}],"h":1},{"t":47,"s":[{"i":[[0.256,-0.022],[2.254,-0.585],[3.206,-1.332],[1.791,-0.633],[1.243,-0.803],[0.459,-0.344],[0.796,-0.69],[0.578,-2.536],[-5.412,-0.37],[-5.389,2.961],[-5.612,-7.141],[4.373,-4.9],[5.997,-4.909],[3.55,-3.74],[1.212,-6.251],[-3.531,-3.523],[-4.555,1.079],[-0.436,0.887],[-0.723,4.885],[-1.207,1.819],[-1.929,1.28],[-0.83,0.751],[-7.632,9.27],[-2.211,5.316],[-0.595,2.568],[2.654,4.923],[0.466,0.486],[3.364,1.516],[0.428,0.231],[8.55,0.39]],"o":[[-3.78,0.33],[-2.254,0.585],[-1.595,0.663],[-1.791,0.633],[-0.437,0.283],[-0.915,0.687],[-2.477,2.147],[-1.81,7.935],[5.485,0.375],[9.229,-5.071],[7.221,9.187],[-7.227,8.097],[-4.679,3.831],[-5.316,5.6],[-1.874,9.669],[2.82,2.814],[1.501,-0.355],[3.521,-7.157],[0.248,-1.678],[1.85,-2.789],[1.033,-0.685],[9.235,-8.355],[4.474,-5.434],[0.69,-1.658],[2.05,-8.853],[-0.787,-1.461],[-2.878,-3.004],[-0.358,-0.161],[-4.426,-2.389],[-0.981,-0.045]],"v":[[90,-109],[81.569,-107.752],[74,-105],[68.736,-103.105],[64,-101],[63,-99],[60,-98],[54,-90],[63,-77],[78,-85],[104,-85],[96,-60],[77,-41],[65,-29],[52,-11],[57,10],[68,14],[74,9],[72,-6],[79,-15],[86,-22],[88,-24],[112,-48],[124,-64],[126,-71],[123,-92],[121,-96],[112,-104],[111,-106],[92,-110]],"c":true}],"h":1},{"t":48,"s":[{"i":[[5.707,-0.548],[3.607,-1.102],[4.005,-2.249],[2.819,-2.546],[-0.378,-4.074],[-1.662,-1.648],[-2.151,-0.238],[-1.707,1.271],[-1.321,1.23],[-4.305,1.431],[-2.995,-0.388],[-2.217,-1.571],[-0.439,-4.495],[2.592,-3.463],[2.102,-2.328],[3.975,-3.638],[4.112,-3.924],[2.697,-3.058],[1.5,-3.18],[0.57,-2.285],[-0.267,-2.771],[-10.363,1.257],[1.29,7.424],[-0.455,2.952],[-2.67,2.951],[-4.807,4.372],[-3.133,3.133],[-1.813,14.684],[3.516,5.047],[5.309,2.786]],"o":[[-3.566,0.342],[-3.607,1.102],[-3.101,1.742],[-2.819,2.546],[0.201,2.169],[1.662,1.648],[2.852,0.316],[1.707,-1.271],[1.786,-1.663],[4.305,-1.431],[1.695,0.22],[2.216,1.571],[0.471,4.82],[-2.592,3.463],[-4.989,5.524],[-3.975,3.638],[-2.714,2.59],[-2.697,3.058],[-0.985,2.088],[-0.571,2.285],[0.413,4.288],[6.161,-0.747],[-0.406,-2.339],[0.384,-2.489],[5.442,-6.015],[2.741,-2.493],[10.566,-10.566],[1.415,-11.462],[-3.734,-5.361],[-5.857,-3.073]],"v":[[90,-109],[79.329,-106.93],[68,-102],[58.391,-95.749],[54,-86],[57.038,-80.051],[63,-77],[69.648,-78.841],[74,-83],[84.094,-88.039],[96,-90],[102.442,-87.706],[107,-79],[102.93,-66.631],[95,-58],[81.841,-44.8],[70,-34],[61.59,-25.442],[55,-16],[52.561,-9.512],[52,-2],[67,14],[75,1],[72,-6],[79,-14],[94,-29],[102,-37],[127,-73],[121,-96],[111,-106]],"c":true}],"h":1},{"t":49,"s":[{"i":[[3.726,-0.269],[3.866,-1.088],[4.144,-2.403],[2.823,-2.612],[-0.276,-3.822],[-1.586,-1.63],[-2.499,-0.282],[-1.548,1.293],[-1.59,1.192],[-0.564,0.12],[-0.354,0.225],[-4.842,0.875],[-3.172,-2.701],[-0.118,-3.707],[1.76,-2.908],[6.757,-6.211],[3.75,-10.485],[-0.208,-3.713],[-1.091,-1.383],[2.067,12.881],[-0.361,2.289],[-1.78,2.037],[-2.428,2.305],[-7.457,6.16],[-3.098,4.195],[3.88,12.637],[0.571,0.761],[0.508,0.74],[2.927,1.694],[0.556,0.251]],"o":[[-3.391,0.245],[-3.866,1.088],[-2.984,1.73],[-2.823,2.612],[0.159,2.197],[1.586,1.63],[3.218,0.363],[1.548,-1.293],[0.392,-0.294],[0.564,-0.12],[3.46,-2.202],[4.842,-0.875],[2.825,2.405],[0.108,3.373],[-5.399,8.92],[-11.244,10.335],[-2.003,5.601],[0.162,2.889],[6.774,8.581],[-0.434,-2.706],[0.207,-1.314],[2.331,-2.667],[8.916,-8.463],[4.571,-3.776],[5.652,-7.653],[-0.646,-2.104],[-0.616,-0.821],[-3.524,-5.128],[-0.784,-0.454],[-4.8,-2.162]],"v":[[91,-109],[80.065,-107.118],[68,-102],[58.555,-95.569],[54,-86],[56.744,-80.063],[63,-77],[69.721,-78.833],[74,-83],[75.529,-83.551],[77,-84],[90.216,-89.177],[103,-87],[108,-78],[104,-68],[82,-45],[54,-13],[52,-3],[56,9],[75,1],[72,-6],[78,-13],[85,-20],[107,-41],[118,-54],[127,-87],[123,-93],[122,-96],[112,-105],[110,-107]],"c":true}],"h":1},{"t":50,"s":[{"i":[[10.014,-0.756],[0.334,0.01],[0.332,-0.013],[7.34,-4.098],[-0.047,-5.375],[-1.561,-1.849],[-2.735,-0.328],[-1.553,1.304],[-1.559,1.17],[-0.564,0.12],[-0.354,0.225],[-4.846,0.868],[-3.148,-2.679],[-0.605,-0.782],[-0.453,-0.912],[2.39,-3.712],[5.022,-4.916],[1.428,-1.092],[0.716,-0.65],[4.753,-5.254],[0.052,-8.184],[-9.639,1.902],[0.967,6.027],[-0.365,2.287],[-0.108,0.144],[-0.519,0.745],[-3.935,3.656],[-4.823,4.433],[-3.771,12.303],[9.393,5.781]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.69,0.232],[-7.339,4.098],[0.02,2.276],[1.561,1.849],[3.227,0.388],[1.553,-1.304],[0.392,-0.294],[0.564,-0.12],[3.47,-2.209],[4.846,-0.868],[1.127,0.96],[0.605,0.782],[2.799,5.636],[-4.487,6.967],[-2.004,1.962],[-0.743,0.568],[-5.103,4.635],[-4.541,5.021],[-0.059,9.36],[6.182,-1.22],[-0.434,-2.705],[0.005,-0.029],[0.621,-0.828],[3.433,-4.925],[6.122,-5.688],[10.354,-9.517],[4.825,-15.744],[-6.583,-4.051]],"v":[[91,-109],[90.001,-108.991],[89,-109],[67.197,-101.857],[54,-87],[56.464,-80.539],[63,-77],[69.751,-78.832],[74,-83],[75.529,-83.551],[77,-84],[90.242,-89.166],[103,-87],[105.506,-84.464],[107,-82],[103,-67],[88,-50],[82,-44],[79,-43],[63,-27],[52,-5],[68,14],[75,1],[72,-6],[74,-7],[75,-10],[86,-21],[101,-35],[126,-68],[114,-104]],"c":true}],"h":1},{"t":51,"s":[{"i":[[10.014,-0.756],[0.334,0.01],[0.332,-0.013],[7.385,-4.171],[-0.434,-6.076],[-1.652,-1.692],[-2.25,-0.27],[-3.046,2.238],[-3.59,1.436],[-3.744,0.083],[-2.191,-1.865],[-0.881,-1.501],[-0.057,-1.79],[1.061,-1.93],[0.82,-1.273],[2.519,-2.714],[2.61,-2.555],[1.071,-1.118],[0.714,-0.546],[0.716,-0.65],[4.921,-5.441],[0,-8.425],[-9.948,1.963],[0.967,6.027],[-0.361,2.289],[-1.893,2.135],[-2.46,2.285],[-4.891,4.496],[-3.77,12.301],[9.393,5.781]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.894,0.24],[-7.385,4.171],[0.141,1.96],[1.652,1.692],[3.227,0.388],[3.046,-2.238],[3.834,-1.534],[3.744,-0.083],[1.418,1.208],[0.881,1.501],[0.064,2.005],[-1.061,1.93],[-2.312,3.591],[-2.519,2.714],[-1.002,0.981],[-1.071,1.118],[-0.743,0.568],[-5.167,4.692],[-4.504,4.98],[0,9.142],[6.182,-1.22],[-0.434,-2.706],[0.105,-0.668],[2.217,-2.501],[6.163,-5.726],[10.351,-9.515],[4.825,-15.744],[-6.583,-4.051]],"v":[[91,-109],[90.001,-108.991],[89,-109],[66.754,-101.877],[54,-86],[56.918,-80.233],[63,-77],[72.227,-80.632],[82,-87],[93.732,-89.55],[103,-87],[106.521,-82.937],[108,-78],[106.163,-71.951],[103,-67],[95.723,-57.723],[88,-50],[84.784,-46.674],[82,-44],[79,-43],[63,-27],[52,-5],[68,14],[75,1],[72,-6],[78,-14],[86,-21],[101,-35],[126,-68],[114,-104]],"c":true}],"h":1},{"t":52,"s":[{"i":[[12.38,-0.935],[0.334,0.01],[0.332,-0.013],[7.395,-4.231],[-0.401,-5.84],[-1.589,-1.631],[-2.495,-0.281],[-1.556,1.296],[-1.569,1.177],[-0.708,0.451],[-6.344,-5.401],[-0.118,-3.707],[1.553,-2.412],[2.109,-2.261],[3.479,-3.032],[1.015,-0.925],[4.884,-5.399],[0.457,-0.597],[0.675,-0.881],[0.965,-1.73],[-4.653,-6.739],[-6.717,1.325],[0.967,6.027],[-0.361,2.289],[-1.893,2.135],[-2.46,2.285],[-4.813,4.442],[-3.763,12.278],[3.547,5.784],[3.366,3.062]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.822,0.237],[-7.395,4.231],[0.151,2.196],[1.589,1.631],[3.209,0.362],[1.556,-1.296],[0.784,-0.588],[6.92,-4.404],[2.825,2.405],[0.127,3.97],[-1.794,2.785],[-3.802,4.076],[-1.461,1.273],[-5.345,4.868],[-0.563,0.622],[-0.636,0.832],[-1.303,1.701],[-4.265,7.65],[2.215,3.208],[6.182,-1.22],[-0.434,-2.706],[0.105,-0.668],[2.217,-2.501],[6.121,-5.687],[10.358,-9.559],[2.752,-8.978],[-2.654,-4.329],[-7.422,-6.753]],"v":[[91,-109],[90.001,-108.991],[89,-109],[66.832,-101.702],[54,-86],[56.742,-80.064],[63,-77],[69.73,-78.846],[74,-83],[77,-84],[103,-87],[108,-78],[103,-67],[96,-58],[84,-46],[79,-43],[63,-27],[61,-25],[60,-22],[56,-18],[56,8],[68,14],[75,1],[72,-6],[78,-14],[86,-21],[101,-35],[126,-68],[124,-93],[117,-101]],"c":true}],"h":1},{"t":53,"s":[{"i":[[12.304,-0.929],[0.334,0.01],[0.332,-0.013],[7.404,-4.223],[-0.424,-5.87],[-1.586,-1.63],[-2.499,-0.282],[-1.556,1.296],[-1.569,1.177],[-0.564,0.12],[-0.354,0.225],[-4.842,0.875],[-3.172,-2.701],[-0.882,-1.486],[-0.059,-1.853],[1.066,-1.956],[0.799,-1.242],[3.42,-3.683],[2.926,-2.55],[1.015,-0.925],[4.644,-5.08],[-8.281,-11.995],[-6.717,1.325],[0.967,6.027],[-0.361,2.289],[-1.837,2.073],[-2.369,2.222],[-4.859,4.485],[13.718,22.37],[3.366,3.062]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.806,0.237],[-7.404,4.223],[0.159,2.197],[1.586,1.63],[3.209,0.362],[1.556,-1.296],[0.392,-0.294],[0.564,-0.12],[3.46,-2.202],[4.842,-0.875],[1.412,1.203],[0.882,1.486],[0.061,1.934],[-1.066,1.956],[-2.394,3.718],[-3.42,3.683],[-1.461,1.273],[-5.408,4.924],[-7.65,8.367],[2.215,3.208],[6.182,-1.22],[-0.434,-2.706],[0.097,-0.617],[2.18,-2.459],[6.391,-5.993],[13.758,-12.697],[-2.654,-4.329],[-7.482,-6.808]],"v":[[91,-109],[90.001,-108.991],[89,-109],[66.828,-101.724],[54,-86],[56.744,-80.063],[63,-77],[69.73,-78.846],[74,-83],[75.529,-83.551],[77,-84],[90.216,-89.177],[103,-87],[106.515,-82.988],[108,-78],[106.146,-71.981],[103,-67],[93.899,-55.624],[84,-46],[79,-43],[63,-27],[56,8],[68,14],[75,1],[72,-6],[78,-14],[86,-21],[101,-35],[124,-93],[117,-101]],"c":true}],"h":1},{"t":54,"s":[{"i":[[12.304,-0.929],[0.334,0.01],[0.332,-0.013],[7.404,-4.223],[-0.424,-5.87],[-1.586,-1.63],[-2.499,-0.282],[-1.556,1.296],[-1.569,1.177],[-0.564,0.12],[-0.354,0.225],[-4.842,0.875],[-3.172,-2.701],[-0.882,-1.486],[-0.059,-1.853],[1.066,-1.956],[0.799,-1.242],[3.42,-3.683],[2.926,-2.55],[1.015,-0.925],[4.644,-5.08],[-8.281,-11.995],[-6.717,1.325],[0.967,6.027],[-0.361,2.289],[-1.837,2.073],[-2.369,2.222],[-4.843,4.499],[13.699,22.339],[3.366,3.062]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.806,0.237],[-7.404,4.223],[0.159,2.197],[1.586,1.63],[3.209,0.362],[1.556,-1.296],[0.392,-0.294],[0.564,-0.12],[3.46,-2.202],[4.842,-0.875],[1.412,1.203],[0.882,1.486],[0.061,1.934],[-1.066,1.956],[-2.394,3.718],[-3.42,3.683],[-1.461,1.273],[-5.408,4.924],[-7.65,8.367],[2.215,3.208],[6.182,-1.22],[-0.434,-2.706],[0.097,-0.617],[2.18,-2.459],[6.39,-5.993],[13.777,-12.8],[-2.654,-4.329],[-7.482,-6.808]],"v":[[91,-109],[90.001,-108.991],[89,-109],[66.828,-101.724],[54,-86],[56.744,-80.063],[63,-77],[69.73,-78.846],[74,-83],[75.529,-83.551],[77,-84],[90.216,-89.177],[103,-87],[106.515,-82.988],[108,-78],[106.146,-71.981],[103,-67],[93.899,-55.624],[84,-46],[79,-43],[63,-27],[56,8],[68,14],[75,1],[72,-6],[78,-14],[86,-21],[101,-35],[124,-93],[117,-101]],"c":true}],"h":1},{"t":55,"s":[{"i":[[12.3,-0.929],[0.334,0.01],[0.332,-0.013],[7.404,-4.223],[-0.424,-5.87],[-1.586,-1.63],[-2.499,-0.282],[-1.556,1.296],[-1.569,1.177],[-0.564,0.12],[-0.354,0.225],[-6.344,-5.401],[-0.118,-3.707],[1.809,-2.989],[6.743,-6.198],[3.75,-10.485],[-0.208,-3.713],[-1.091,-1.383],[2.067,12.881],[-0.361,2.289],[-1.831,2.095],[-2.428,2.305],[-3.88,3.668],[-1.006,0.754],[-0.796,0.79],[-2.045,2.458],[-2.43,5.656],[-0.59,2.441],[2.858,4.937],[2.61,2.375]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.806,0.237],[-7.404,4.223],[0.159,2.197],[1.586,1.63],[3.209,0.362],[1.556,-1.296],[0.392,-0.294],[0.564,-0.12],[6.92,-4.404],[2.825,2.405],[0.107,3.347],[-5.399,8.921],[-11.244,10.335],[-2.003,5.601],[0.162,2.889],[6.774,8.581],[-0.434,-2.706],[0.213,-1.349],[2.331,-2.667],[5.725,-5.434],[1.796,-1.698],[0.952,-0.714],[2.279,-2.261],[4.543,-5.459],[0.729,-1.698],[2.288,-9.461],[-1.972,-3.406],[-7.468,-6.795]],"v":[[91,-109],[90.001,-108.991],[89,-109],[66.828,-101.724],[54,-86],[56.744,-80.063],[63,-77],[69.73,-78.846],[74,-83],[75.529,-83.551],[77,-84],[103,-87],[108,-78],[104,-68],[82,-45],[54,-13],[52,-3],[56,9],[75,1],[72,-6],[78,-13],[85,-20],[99,-34],[104,-39],[107,-40],[113,-48],[125,-64],[127,-71],[123,-94],[117,-101]],"c":true}],"h":1},{"t":56,"s":[{"i":[[12.291,-0.928],[0.334,0.01],[0.332,-0.013],[7.404,-4.223],[-0.424,-5.87],[-1.585,-1.63],[-2.5,-0.282],[-1.556,1.296],[-1.569,1.177],[-0.564,0.12],[-0.354,0.225],[-4.842,0.875],[-3.172,-2.701],[-0.118,-3.707],[1.809,-2.989],[6.743,-6.198],[3.75,-10.485],[-0.208,-3.713],[-1.091,-1.383],[2.067,12.881],[-0.361,2.289],[-1.78,2.037],[-2.428,2.305],[-5.434,5.059],[-0.68,0.664],[-2.148,2.482],[-1.904,2.579],[0.202,10.903],[1.709,2.951],[2.701,2.457]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.806,0.237],[-7.404,4.223],[0.159,2.197],[1.585,1.63],[3.209,0.362],[1.556,-1.296],[0.392,-0.294],[0.564,-0.12],[3.46,-2.202],[4.842,-0.875],[2.825,2.405],[0.107,3.347],[-5.399,8.921],[-11.244,10.335],[-2.003,5.601],[0.162,2.889],[6.774,8.581],[-0.434,-2.706],[0.207,-1.314],[2.331,-2.667],[7.595,-7.21],[0.69,-0.642],[2.343,-2.289],[2.151,-2.486],[4.975,-6.737],[-0.118,-6.386],[-1.971,-3.404],[-7.448,-6.777]],"v":[[91,-109],[90.001,-108.991],[89,-109],[66.828,-101.724],[54,-86],[56.744,-80.063],[63,-77],[69.73,-78.846],[74,-83],[75.529,-83.551],[77,-84],[90.216,-89.177],[103,-87],[108,-78],[104,-68],[82,-45],[54,-13],[52,-3],[56,9],[75,1],[72,-6],[78,-13],[85,-20],[103,-38],[106,-39],[112,-47],[118,-54],[128,-79],[123,-94],[117,-101]],"c":true}],"h":1},{"t":57,"s":[{"i":[[12.291,-0.928],[0.334,0.01],[0.332,-0.013],[7.406,-4.223],[-0.432,-5.871],[-1.584,-1.63],[-2.502,-0.282],[-1.556,1.296],[-1.569,1.177],[-0.564,0.12],[-0.354,0.225],[-4.842,0.875],[-3.172,-2.701],[-0.882,-1.486],[-0.059,-1.853],[0.787,-1.708],[0.904,-1.495],[6.663,-6.125],[3.75,-10.485],[-0.208,-3.713],[-1.091,-1.383],[2.067,12.881],[-0.361,2.289],[-1.78,2.037],[-2.428,2.305],[-7.457,6.16],[-0.666,0.747],[0.293,15.788],[1.709,2.951],[2.701,2.457]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.806,0.237],[-7.406,4.223],[0.162,2.198],[1.584,1.63],[3.209,0.362],[1.556,-1.296],[0.392,-0.294],[0.564,-0.12],[3.46,-2.202],[4.842,-0.875],[1.412,1.203],[0.882,1.486],[0.053,1.673],[-0.787,1.708],[-5.368,8.869],[-11.244,10.335],[-2.003,5.601],[0.162,2.889],[6.774,8.581],[-0.434,-2.706],[0.207,-1.314],[2.331,-2.667],[8.916,-8.463],[0.788,-0.651],[8.101,-9.083],[-0.118,-6.386],[-1.971,-3.404],[-7.448,-6.777]],"v":[[91,-109],[90.001,-108.991],[89,-109],[66.821,-101.725],[54,-86],[56.745,-80.063],[63,-77],[69.73,-78.846],[74,-83],[75.529,-83.551],[77,-84],[90.216,-89.177],[103,-87],[106.515,-82.988],[108,-78],[106.719,-72.866],[104,-68],[82,-45],[54,-13],[52,-3],[56,9],[75,1],[72,-6],[78,-13],[85,-20],[107,-41],[109,-43],[128,-79],[123,-94],[117,-101]],"c":true}],"h":1},{"t":58,"s":[{"i":[[12.291,-0.928],[0.334,0.01],[0.332,-0.013],[7.406,-4.223],[-0.432,-5.871],[-1.584,-1.63],[-2.502,-0.282],[-1.556,1.296],[-1.569,1.177],[-0.564,0.12],[-0.354,0.225],[-4.842,0.875],[-3.172,-2.701],[-0.882,-1.486],[-0.059,-1.853],[0.787,-1.708],[0.904,-1.495],[6.663,-6.125],[3.75,-10.485],[-0.208,-3.713],[-1.091,-1.383],[2.067,12.881],[-0.361,2.289],[-1.78,2.037],[-2.428,2.305],[-7.457,6.16],[-0.666,0.747],[0.293,15.788],[1.709,2.951],[2.701,2.457]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.806,0.237],[-7.406,4.223],[0.162,2.198],[1.584,1.63],[3.209,0.362],[1.556,-1.296],[0.392,-0.294],[0.564,-0.12],[3.46,-2.202],[4.842,-0.875],[1.412,1.203],[0.882,1.486],[0.053,1.673],[-0.787,1.708],[-5.368,8.869],[-11.244,10.335],[-2.003,5.601],[0.162,2.889],[6.774,8.581],[-0.434,-2.706],[0.207,-1.314],[2.331,-2.667],[8.916,-8.463],[0.788,-0.651],[8.101,-9.083],[-0.118,-6.386],[-1.971,-3.404],[-7.448,-6.777]],"v":[[91,-109],[90.001,-108.991],[89,-109],[66.821,-101.725],[54,-86],[56.745,-80.063],[63,-77],[69.73,-78.846],[74,-83],[75.529,-83.551],[77,-84],[90.216,-89.177],[103,-87],[106.515,-82.988],[108,-78],[106.719,-72.866],[104,-68],[82,-45],[54,-13],[52,-3],[56,9],[75,1],[72,-6],[78,-13],[85,-20],[107,-41],[109,-43],[128,-79],[123,-94],[117,-101]],"c":true}],"h":1},{"t":59,"s":[{"i":[[0.233,-0.018],[0.334,0.01],[0.332,-0.013],[7.34,-4.074],[-0.087,-5.471],[-1.568,-1.799],[-2.689,-0.303],[-1.556,1.296],[-1.569,1.177],[-0.564,0.12],[-0.354,0.225],[-4.842,0.875],[-3.172,-2.701],[-0.118,-3.707],[1.665,-2.75],[6.591,-6.058],[3.695,-10.333],[-0.208,-3.713],[-1.136,-1.439],[2.07,12.899],[-0.361,2.289],[-2.204,2.522],[-2.383,2.262],[-7.197,5.946],[-3.098,4.195],[0.194,10.463],[2.607,2.721],[3.203,1.444],[0.428,0.231],[6.327,0.288]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.729,0.234],[-7.339,4.074],[0.04,2.501],[1.568,1.799],[3.209,0.362],[1.556,-1.296],[0.392,-0.294],[0.564,-0.12],[3.46,-2.202],[4.842,-0.875],[2.825,2.405],[0.102,3.205],[-5.468,9.033],[-11.226,10.319],[-2.003,5.601],[0.162,2.892],[6.756,8.557],[-0.434,-2.706],[0.214,-1.36],[2.256,-2.581],[8.329,-7.906],[4.571,-3.776],[4.943,-6.693],[-0.111,-5.958],[-2.958,-3.088],[-0.358,-0.161],[-4.365,-2.357],[-0.963,-0.044]],"v":[[91,-109],[90.001,-108.991],[89,-109],[67.138,-101.928],[54,-87],[56.513,-80.352],[63,-77],[69.73,-78.846],[74,-83],[75.529,-83.551],[77,-84],[90.216,-89.177],[103,-87],[108,-78],[104,-68],[82,-45],[54,-13],[52,-3],[56,9],[75,1],[72,-6],[78,-13],[85,-20],[107,-41],[118,-54],[128,-79],[122,-96],[113,-104],[112,-106],[93,-110]],"c":true}],"h":1},{"t":60,"s":[{"i":[[12.291,-0.928],[0.334,0.01],[0.332,-0.013],[7.34,-4.074],[-0.087,-5.471],[-1.568,-1.799],[-2.689,-0.303],[-1.556,1.296],[-1.569,1.177],[-0.564,0.12],[-0.354,0.225],[-4.842,0.875],[-3.172,-2.701],[-0.882,-1.486],[-0.059,-1.853],[0.805,-1.756],[0.833,-1.375],[6.591,-6.058],[3.695,-10.333],[-0.208,-3.713],[-1.136,-1.439],[2.07,12.899],[-0.361,2.289],[-1.78,2.037],[-2.428,2.305],[-7.457,6.16],[-0.666,0.747],[0.293,15.788],[1.709,2.951],[2.701,2.457]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.729,0.234],[-7.339,4.074],[0.04,2.501],[1.568,1.799],[3.209,0.362],[1.556,-1.296],[0.392,-0.294],[0.564,-0.12],[3.46,-2.202],[4.842,-0.875],[1.412,1.203],[0.882,1.486],[0.051,1.602],[-0.805,1.756],[-5.468,9.033],[-11.226,10.319],[-2.003,5.601],[0.162,2.892],[6.756,8.557],[-0.434,-2.706],[0.207,-1.314],[2.331,-2.667],[8.916,-8.463],[0.788,-0.651],[8.101,-9.083],[-0.118,-6.386],[-1.971,-3.404],[-7.448,-6.777]],"v":[[91,-109],[90.001,-108.991],[89,-109],[67.138,-101.928],[54,-87],[56.513,-80.352],[63,-77],[69.73,-78.846],[74,-83],[75.529,-83.551],[77,-84],[90.216,-89.177],[103,-87],[106.515,-82.988],[108,-78],[106.663,-72.829],[104,-68],[82,-45],[54,-13],[52,-3],[56,9],[75,1],[72,-6],[78,-13],[85,-20],[107,-41],[109,-43],[128,-79],[123,-94],[117,-101]],"c":true}],"h":1}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.525490196078,0.270588235294,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":31,"op":300,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 4","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.016,54.049,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"t":50,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.525490196078,0.270588235294,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[43.313,-47.836],"ix":2},"a":{"a":0,"k":[43.313,-47.836],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":24,"op":31,"st":10,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 1","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.016,54.049,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"t":50,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.525490196078,0.270588235294,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[43.313,-47.836],"ix":2},"a":{"a":0,"k":[43.313,-47.836],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18,"st":10,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 5","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.016,54.049,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"t":50,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.525490196078,0.270588235294,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-78.173,-47.836],"ix":2},"a":{"a":0,"k":[-78.173,-47.836],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":18,"op":24,"st":10,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Cup 2","parent":15,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-11.815,0],[0,0],[1.176,-11.756],[0,0],[5.492,54.916],[0,0]],"o":[[0,0],[11.815,0],[0,0],[-5.492,54.916],[0,0],[-1.176,-11.756]],"v":[[-49.55,-73.91],[49.55,-73.91],[70.876,-52.583],[62.346,32.723],[-62.346,32.723],[-70.876,-52.583]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.705882370472,0.247058823705,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Cup","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":310,"st":10,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Star 4 :M","parent":15,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[-225,-6.953,0],"to":[75,0,0],"ti":[-75,0,0]},{"t":50,"s":[225,-6.953,0]}],"ix":2,"l":2},"a":{"a":0,"k":[24.984,188.998,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.278,-3.874],[6.547,-0.032],[5.316,3.822],[2.054,6.217],[-1.993,6.237],[-5.278,3.874],[-6.547,0.032],[-5.316,-3.822],[-2.054,-6.217],[1.993,-6.237]],"o":[[-5.278,3.874],[-6.547,0.032],[-5.316,-3.822],[-2.054,-6.217],[1.993,-6.237],[5.278,-3.874],[6.547,-0.033],[5.316,3.822],[2.054,6.217],[-1.993,6.237]],"v":[[19.304,28.834],[0.146,23.68],[-18.962,29.022],[-19.98,9.209],[-30.965,-7.313],[-12.436,-14.404],[-0.118,-29.957],[12.352,-14.526],[30.95,-7.617],[20.128,9.011]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,188.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-200.016,188.998],"ix":2},"a":{"a":0,"k":[249.984,188.998],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star 4","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.278,-3.874],[6.547,-0.032],[5.316,3.822],[2.054,6.217],[-1.993,6.237],[-5.278,3.874],[-6.547,0.032],[-5.316,-3.822],[-2.054,-6.217],[1.993,-6.237]],"o":[[-5.278,3.874],[-6.547,0.032],[-5.316,-3.822],[-2.054,-6.217],[1.993,-6.237],[5.278,-3.874],[6.547,-0.033],[5.316,3.822],[2.054,6.217],[-1.993,6.237]],"v":[[19.304,28.834],[0.146,23.68],[-18.962,29.022],[-19.98,9.209],[-30.965,-7.313],[-12.436,-14.404],[-0.118,-29.957],[12.352,-14.526],[30.95,-7.617],[20.128,9.011]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,188.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-50.016,188.998],"ix":2},"a":{"a":0,"k":[249.984,188.998],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star 3","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.278,-3.874],[6.547,-0.032],[5.316,3.822],[2.054,6.217],[-1.993,6.237],[-5.278,3.874],[-6.547,0.032],[-5.316,-3.822],[-2.054,-6.217],[1.993,-6.237]],"o":[[-5.278,3.874],[-6.547,0.032],[-5.316,-3.822],[-2.054,-6.217],[1.993,-6.237],[5.278,-3.874],[6.547,-0.033],[5.316,3.822],[2.054,6.217],[-1.993,6.237]],"v":[[19.304,28.834],[0.146,23.68],[-18.962,29.022],[-19.98,9.209],[-30.965,-7.313],[-12.436,-14.404],[-0.118,-29.957],[12.352,-14.526],[30.95,-7.617],[20.128,9.011]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,188.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[99.984,188.998],"ix":2},"a":{"a":0,"k":[249.984,188.998],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star 2","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.278,-3.874],[6.547,-0.032],[5.316,3.822],[2.054,6.217],[-1.993,6.237],[-5.278,3.874],[-6.547,0.032],[-5.316,-3.822],[-2.054,-6.217],[1.993,-6.237]],"o":[[-5.278,3.874],[-6.547,0.032],[-5.316,-3.822],[-2.054,-6.217],[1.993,-6.237],[5.278,-3.874],[6.547,-0.033],[5.316,3.822],[2.054,6.217],[-1.993,6.237]],"v":[[19.304,28.834],[0.146,23.68],[-18.962,29.022],[-19.98,9.209],[-30.965,-7.313],[-12.436,-14.404],[-0.118,-29.957],[12.352,-14.526],[30.95,-7.617],[20.128,9.011]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,188.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,188.998],"ix":2},"a":{"a":0,"k":[249.984,188.998],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":310,"st":10,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Black Stand 2","parent":14,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-24.605,0],[0,0],[18.303,0]],"o":[[-18.303,0],[0,0],[24.605,0],[0,0]],"v":[[-42.653,-29.114],[-53.962,29.114],[53.962,29.114],[42.653,-29.114]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.349019616842,0.345098048449,0.43137255311,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Black Stand","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":310,"st":10,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"White Stand 4 :M","parent":14,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[-225,-1.544,0],"to":[75,0,0],"ti":[-75,0,0]},{"t":50,"s":[225,-1.544,0]}],"ix":2,"l":2},"a":{"a":0,"k":[24.984,347.302,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.323,0],[0,0],[-1.582,-4.024],[0,0],[4.323,0],[0,0],[-1.582,4.024],[0,0]],"o":[[0,0],[4.323,0],[0,0],[1.582,4.024],[0,0],[-4.323,0],[0,0],[1.582,-4.024]],"v":[[-25.949,-12.268],[25.998,-12.268],[33.803,-4.464],[37.313,4.464],[31.758,12.268],[-32.174,12.268],[-37.263,4.464],[-33.753,-4.464]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,347.302],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-200.016,347.302],"ix":2},"a":{"a":0,"k":[249.984,347.302],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand 4","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.323,0],[0,0],[-1.582,-4.024],[0,0],[4.323,0],[0,0],[-1.582,4.024],[0,0]],"o":[[0,0],[4.323,0],[0,0],[1.582,4.024],[0,0],[-4.323,0],[0,0],[1.582,-4.024]],"v":[[-25.949,-12.268],[25.998,-12.268],[33.803,-4.464],[37.313,4.464],[31.758,12.268],[-32.174,12.268],[-37.263,4.464],[-33.753,-4.464]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,347.302],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-50.016,347.302],"ix":2},"a":{"a":0,"k":[249.984,347.302],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand 3","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.323,0],[0,0],[-1.582,-4.024],[0,0],[4.323,0],[0,0],[-1.582,4.024],[0,0]],"o":[[0,0],[4.323,0],[0,0],[1.582,4.024],[0,0],[-4.323,0],[0,0],[1.582,-4.024]],"v":[[-25.949,-12.268],[25.998,-12.268],[33.803,-4.464],[37.313,4.464],[31.758,12.268],[-32.174,12.268],[-37.263,4.464],[-33.753,-4.464]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,347.302],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[99.984,347.302],"ix":2},"a":{"a":0,"k":[249.984,347.302],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand 2","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.323,0],[0,0],[-1.582,-4.024],[0,0],[4.323,0],[0,0],[-1.582,4.024],[0,0]],"o":[[0,0],[4.323,0],[0,0],[1.582,4.024],[0,0],[-4.323,0],[0,0],[1.582,-4.024]],"v":[[-25.949,-12.268],[25.998,-12.268],[33.803,-4.464],[37.313,4.464],[31.758,12.268],[-32.174,12.268],[-37.263,4.464],[-33.753,-4.464]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,347.302],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,347.302],"ix":2},"a":{"a":0,"k":[249.984,347.302],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":310,"st":10,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Black Stand","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"k":[{"s":[90],"t":2,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[88.052],"t":3,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[83.09],"t":4,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[75.985],"t":5,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[67.277],"t":6,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[57.336],"t":7,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[46.447],"t":8,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[34.86],"t":9,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[10.836],"t":11,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0],"t":12,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-6.514],"t":13,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-10.253],"t":14,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-11.772],"t":15,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-11.657],"t":16,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-10.457],"t":17,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-8.646],"t":18,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-6.599],"t":19,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-4.592],"t":20,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-2.804],"t":21,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-1.336],"t":22,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.223],"t":23,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.544],"t":24,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[1.006],"t":25,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[1.219],"t":26,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[1.245],"t":27,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[1.142],"t":28,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.963],"t":29,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.75],"t":30,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.535],"t":31,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.34],"t":32,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.176],"t":33,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.049],"t":34,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.04],"t":35,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.097],"t":36,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.125],"t":37,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.132],"t":38,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.124],"t":39,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.107],"t":40,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.085],"t":41,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.062],"t":42,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.041],"t":43,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.023],"t":44,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.008],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.002],"t":46,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.009],"t":47,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.013],"t":48,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.014],"t":49,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.013],"t":50,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.012],"t":51,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.01],"t":52,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.007],"t":53,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.005],"t":54,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.003],"t":55,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.001],"t":56,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0],"t":57,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.001],"t":58,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.001],"t":59,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.001],"t":60,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.001],"t":61,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.001],"t":62,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.001],"t":63,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.001],"t":65,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0],"t":66,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0],"t":67,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0],"t":68,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0],"t":69,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"p":{"k":[{"s":[138.235,254.547,0],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[143.584,250.368,0],"t":1,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[157.812,240.556,0],"t":2,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[179.791,229.215,0],"t":3,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[209.087,221.759,0],"t":4,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[243.189,225.873,0],"t":5,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[274.404,246.799,0],"t":6,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[294.84,281.274,0],"t":7,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[299.502,322.507,0],"t":8,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[282.589,360.014,0],"t":9,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[249.984,377.959,0],"t":10,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[228.111,384.013,0],"t":11,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[215.555,387.488,0],"t":12,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[210.454,388.9,0],"t":13,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[210.841,388.792,0],"t":14,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[214.869,387.678,0],"t":15,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[220.951,385.994,0],"t":16,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[227.823,384.092,0],"t":17,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[234.564,382.227,0],"t":18,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[240.567,380.565,0],"t":19,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[245.498,379.201,0],"t":20,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[249.235,378.166,0],"t":21,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[251.813,377.453,0],"t":22,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[253.364,377.023,0],"t":23,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[254.079,376.826,0],"t":24,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[254.164,376.802,0],"t":25,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[253.818,376.898,0],"t":26,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[253.217,377.064,0],"t":27,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[252.503,377.262,0],"t":28,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[251.782,377.461,0],"t":29,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[251.126,377.643,0],"t":30,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[250.576,377.795,0],"t":31,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[250.15,377.913,0],"t":32,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[249.849,377.996,0],"t":33,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[249.66,378.049,0],"t":34,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[249.909,377.98,0],"t":42,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}],"l":2},"a":{"a":0,"k":[0,29.114,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[0,0,100]},{"t":10,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"Elastic Controller","np":5,"mn":"Pseudo/MDS Elastic Controller","ix":1,"en":1,"ef":[{"ty":0,"nm":"Amplitude","mn":"Pseudo/MDS Elastic Controller-0001","ix":1,"v":{"a":0,"k":20,"ix":1}},{"ty":0,"nm":"Frequency","mn":"Pseudo/MDS Elastic Controller-0002","ix":2,"v":{"a":0,"k":40,"ix":2}},{"ty":0,"nm":"Decay","mn":"Pseudo/MDS Elastic Controller-0003","ix":3,"v":{"a":0,"k":60,"ix":3}}]},{"ty":5,"nm":"Elastic Controller 2","np":5,"mn":"Pseudo/MDS Elastic Controller","ix":2,"en":1,"ef":[{"ty":0,"nm":"Amplitude","mn":"Pseudo/MDS Elastic Controller-0001","ix":1,"v":{"a":0,"k":20,"ix":1}},{"ty":0,"nm":"Frequency","mn":"Pseudo/MDS Elastic Controller-0002","ix":2,"v":{"a":0,"k":40,"ix":2}},{"ty":0,"nm":"Decay","mn":"Pseudo/MDS Elastic Controller-0003","ix":3,"v":{"a":0,"k":60,"ix":3}}]}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-24.605,0],[0,0],[18.303,0]],"o":[[-18.303,0],[0,0],[24.605,0],[0,0]],"v":[[-42.653,-29.114],[-53.962,29.114],[53.962,29.114],[42.653,-29.114]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.349019616842,0.345098048449,0.43137255311,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Black Stand","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":310,"st":10,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Cup","parent":14,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,-152.895,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-11.815,0],[0,0],[1.176,-11.756],[0,0],[5.492,54.916],[0,0]],"o":[[0,0],[11.815,0],[0,0],[-5.492,54.916],[0,0],[-1.176,-11.756]],"v":[[-49.55,-73.91],[49.55,-73.91],[70.876,-52.583],[62.346,32.723],[-62.346,32.723],[-70.876,-52.583]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.705882370472,0.247058823705,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Cup","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":310,"st":10,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Stand","parent":14,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,-56.636,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[19.235,36.65],[0,0],[-15.853,-38.082],[0,0]],"o":[[0,0],[-20.405,35.342],[0,0],[17.561,-38.659]],"v":[[-33.841,-56.55],[33.841,-56.55],[25.31,56.55],[-25.31,56.55]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.525490224361,0.270588248968,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Stand","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":310,"st":10,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Shape Layer 3","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.016,54.049,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"t":50,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.525490196078,0.270588235294,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[43.313,-47.836],"ix":2},"a":{"a":0,"k":[43.313,-47.836],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":18,"op":24,"st":10,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"Shape Layer 6","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.016,54.049,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"t":50,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.525490196078,0.270588235294,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-78.173,-47.836],"ix":2},"a":{"a":0,"k":[-78.173,-47.836],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":24,"op":310,"st":10,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"Shape Layer 2","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.016,54.049,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"t":50,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.525490196078,0.270588235294,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-78.173,-47.836],"ix":2},"a":{"a":0,"k":[-78.173,-47.836],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18,"st":10,"bm":0},{"ddd":0,"ind":21,"ty":0,"nm":"Pre-comp 1","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":60,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":16,"op":316,"st":16,"bm":0},{"ddd":0,"ind":22,"ty":0,"nm":"Pre-comp 1","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":11,"op":311,"st":11,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
index 756cdf3c3d..49f1d01e8e 100644
--- a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
+++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
@@ -1,4 +1,4 @@
-namespace SkiaSharp.Extended.UI.Controls;
+namespace SkiaSharp.Extended.UI.Controls;
///
/// A view that plays Lottie animations using the Skottie library.
@@ -78,11 +78,9 @@ public class SKLottieView : SKAnimatedSurfaceView
typeof(SKLottieView),
1.0);
- Skottie.Animation? animation;
- bool isInForwardPhase = true;
- int repeatsCompleted = 0;
- CancellationTokenSource? loadCancellation;
- bool isResetting;
+ private readonly SKSkottiePlayer player = new();
+ private CancellationTokenSource? loadCancellation;
+ private bool isSyncingFromPlayer;
///
/// Initializes a new instance of the class.
@@ -93,6 +91,8 @@ public SKLottieView()
IsAnimationEnabled = true;
+ player.AnimationCompleted += OnPlayerAnimationCompleted;
+
#if DEBUG
AnimationCompleted += (s, e) => DebugUtils.LogEvent(nameof(AnimationCompleted));
AnimationFailed += (s, e) => DebugUtils.LogEvent(nameof(AnimationFailed));
@@ -184,138 +184,53 @@ public double AnimationSpeed
///
protected override void Update(TimeSpan deltaTime)
{
- if (animation is null)
- return;
+ player.RepeatCount = RepeatCount;
+ player.RepeatMode = (SKSkottieRepeatMode)RepeatMode;
+ player.AnimationSpeed = AnimationSpeed;
+
+ player.Update(deltaTime);
- // Apply animation speed with overflow protection
- // Handle NaN and Infinity explicitly, and use safe bounds for long cast
- var scaledTicks = deltaTime.Ticks * AnimationSpeed;
- const long SafeMax = long.MaxValue - 1; // Avoid overflow when casting from double
- const long SafeMin = long.MinValue + 2; // Avoid overflow when negating TimeSpan
- if (!double.IsFinite(scaledTicks))
- scaledTicks = double.IsNaN(scaledTicks) || scaledTicks < 0 ? SafeMin : SafeMax;
- else if (scaledTicks > SafeMax)
- scaledTicks = SafeMax;
- else if (scaledTicks < SafeMin)
- scaledTicks = SafeMin;
- deltaTime = TimeSpan.FromTicks((long)scaledTicks);
-
- // Apply phase direction (for RepeatMode.Reverse ping-pong)
- if (!isInForwardPhase)
- deltaTime = -deltaTime;
-
- var newProgress = Progress + deltaTime;
- if (newProgress > Duration)
- newProgress = Duration;
- if (newProgress < TimeSpan.Zero)
- newProgress = TimeSpan.Zero;
-
- Progress = newProgress;
+ SyncFromPlayer();
}
///
protected override void OnPaintSurface(SKCanvas canvas, SKSize size)
{
- if (animation is null)
- return;
-
- animation.Render(canvas, SKRect.Create(SKPoint.Empty, size));
-
-#if DEBUG
- WriteDebugStatus($"Repeats: {repeatsCompleted}/{RepeatCount}");
- WriteDebugStatus($"Forward: {isInForwardPhase} ({RepeatMode})");
-#endif
+ player.Render(canvas, SKRect.Create(SKPoint.Empty, size));
}
- private void UpdateProgress(TimeSpan progress)
+ private void SyncFromPlayer()
{
- if (animation is null)
+ isSyncingFromPlayer = true;
+ try
{
- IsComplete = true;
- return;
+ Progress = player.Progress;
+ IsComplete = player.IsComplete;
}
-
- animation.SeekFrameTime(progress.TotalSeconds);
-
- // Skip completion/repeat logic during Reset to avoid spurious events
- if (isResetting)
- return;
-
- var repeatMode = RepeatMode;
- var duration = Duration;
-
- // Determine effective movement direction
- // Negative AnimationSpeed inverts the movement relative to the phase
- var movingForward = AnimationSpeed >= 0 ? isInForwardPhase : !isInForwardPhase;
-
- // Have we reached a boundary based on our movement direction?
- var atStart = !movingForward && progress <= TimeSpan.Zero;
- var atEnd = movingForward && progress >= duration;
-
- // A run is "finished" based on RepeatMode:
- // - Restart: finished when reaching the destination (end for forward, start for backward)
- // - Reverse: finished when completing full cycle (forward + back to start, or backward + back to end)
- // With positive speed: start -> end -> start (finish at start)
- // With negative speed: end -> start -> end (finish at end)
- var reverseFinishPoint = AnimationSpeed >= 0 ? atStart : atEnd;
- var isFinishedRun = repeatMode == SKLottieRepeatMode.Restart
- ? (movingForward ? atEnd : atStart)
- : reverseFinishPoint;
-
- // For Reverse mode: flip direction when hitting a boundary (but not the finish boundary)
- // With positive speed: flip at end (start going back toward start)
- // With negative speed: flip at start (start going back toward end)
- var needsFlip = repeatMode == SKLottieRepeatMode.Reverse &&
- (AnimationSpeed >= 0 ? atEnd : atStart) && !isFinishedRun;
-
- if (needsFlip)
+ finally
{
- // we need to reverse to finish the run
- isInForwardPhase = !isInForwardPhase;
+ isSyncingFromPlayer = false;
+ }
+ }
- IsComplete = false;
+ private void SyncFromLoad()
+ {
+ isSyncingFromPlayer = true;
+ try
+ {
+ Duration = player.Duration;
+ Progress = player.Progress;
+ IsComplete = player.IsComplete;
}
- else
+ finally
{
- // make sure repeats are positive to make things easier
- var totalRepeatCount = RepeatCount;
- if (totalRepeatCount < 0)
- totalRepeatCount = int.MaxValue;
-
- // infinite
- var infinite = totalRepeatCount == int.MaxValue;
- if (infinite)
- repeatsCompleted = 0;
-
- // if we are at the end and we are repeating, then repeat
- if (isFinishedRun && repeatsCompleted < totalRepeatCount)
- {
- if (!infinite)
- repeatsCompleted++;
-
- isFinishedRun = false;
-
- if (repeatMode == SKLottieRepeatMode.Restart)
- {
- // Restart at the beginning of the movement direction:
- // - Positive speed: restart at 0, move toward Duration
- // - Negative speed: restart at Duration, move toward 0
- Progress = AnimationSpeed >= 0 ? TimeSpan.Zero : Duration;
- }
- else if (repeatMode == SKLottieRepeatMode.Reverse)
- isInForwardPhase = !isInForwardPhase;
- }
-
- IsComplete =
- isFinishedRun &&
- repeatsCompleted >= totalRepeatCount;
-
- if (IsComplete)
- AnimationCompleted?.Invoke(this, EventArgs.Empty);
+ isSyncingFromPlayer = false;
}
+ }
- if (!IsAnimationEnabled)
- Invalidate();
+ private void OnPlayerAnimationCompleted(object? sender, EventArgs e)
+ {
+ AnimationCompleted?.Invoke(this, EventArgs.Empty);
}
private async Task LoadAnimationAsync(SKLottieImageSource? imageSource)
@@ -328,22 +243,22 @@ private async Task LoadAnimationAsync(SKLottieImageSource? imageSource)
if (imageSource is null || imageSource.IsEmpty)
{
- animation = null;
- Reset();
+ player.SetAnimation(null);
+ SyncFromLoad();
}
else
{
Exception? exception;
+ SKLottieAnimation? loadResult = null;
try
{
- var loadResult = await Task.Run(() => imageSource.LoadAnimationAsync(cancellationToken), cancellationToken);
+ loadResult = await Task.Run(() => imageSource.LoadAnimationAsync(cancellationToken), cancellationToken);
// Check if cancelled before applying result
if (cancellationToken.IsCancellationRequested)
return;
exception = null;
- animation = loadResult.Animation;
}
catch (OperationCanceledException)
{
@@ -353,39 +268,20 @@ private async Task LoadAnimationAsync(SKLottieImageSource? imageSource)
catch (Exception ex)
{
exception = ex;
- animation = null;
+ loadResult = null;
}
- Reset();
+ player.SetAnimation(loadResult?.Animation);
+ SyncFromLoad();
- if (animation is null)
- AnimationFailed?.Invoke(this, new SKLottieAnimationFailedEventArgs(exception));
+ if (player.HasAnimation)
+ AnimationLoaded?.Invoke(this, SKLottieAnimationLoadedEventArgs.Create(loadResult!.Animation!));
else
- AnimationLoaded?.Invoke(this, SKLottieAnimationLoadedEventArgs.Create(animation));
+ AnimationFailed?.Invoke(this, new SKLottieAnimationFailedEventArgs(exception));
}
if (!IsAnimationEnabled)
Invalidate();
-
- void Reset()
- {
- isResetting = true;
- try
- {
- isInForwardPhase = true;
- repeatsCompleted = 0;
-
- // Initialize Progress based on AnimationSpeed:
- // - Positive/zero speed: start at 0, move toward Duration
- // - Negative speed: start at Duration, move toward 0
- Duration = animation?.Duration ?? TimeSpan.Zero;
- Progress = AnimationSpeed < 0 ? Duration : TimeSpan.Zero;
- }
- finally
- {
- isResetting = false;
- }
- }
}
private static async void OnSourcePropertyChanged(BindableObject bindable, object? oldValue, object? newValue)
@@ -411,6 +307,16 @@ private static void OnProgressDurationPropertyChanged(BindableObject bindable, o
if (bindable is not SKLottieView lv)
return;
- lv.UpdateProgress(lv.Progress);
+ // Skip if we are syncing from the player to avoid re-entrant seeks
+ if (lv.isSyncingFromPlayer)
+ return;
+
+ // User-driven change (e.g. scrubbing): propagate to player
+ lv.player.Progress = lv.Progress;
+
+ // Trigger repaint if animation is disabled (e.g. user is scrubbing a paused animation)
+ if (!lv.IsAnimationEnabled)
+ lv.Invalidate();
}
+
}
diff --git a/source/SkiaSharp.Extended/Lottie/SKSkottiePlayer.cs b/source/SkiaSharp.Extended/Lottie/SKSkottiePlayer.cs
new file mode 100644
index 0000000000..a818737235
--- /dev/null
+++ b/source/SkiaSharp.Extended/Lottie/SKSkottiePlayer.cs
@@ -0,0 +1,195 @@
+using System;
+
+namespace SkiaSharp.Extended;
+
+///
+/// A platform-agnostic Skottie (Lottie) animation player that manages playback state
+/// and rendering. Can be shared between MAUI, Blazor, and other platforms.
+///
+public class SKSkottiePlayer
+{
+ private Skottie.Animation? animation;
+ private bool isInForwardPhase = true;
+ private int repeatsCompleted = 0;
+ private bool isResetting;
+
+ private TimeSpan _progress;
+
+ /// Gets the total duration of the loaded animation.
+ public TimeSpan Duration { get; private set; } = TimeSpan.Zero;
+
+ ///
+ /// Gets or sets the current playback position. Setting this seeks the animation
+ /// and may trigger completion logic (repeats, AnimationCompleted event).
+ ///
+ public TimeSpan Progress
+ {
+ get => _progress;
+ set
+ {
+ _progress = value;
+ UpdateProgress(_progress);
+ }
+ }
+
+ /// Gets whether the animation has completed all repeats.
+ public bool IsComplete { get; private set; } = false;
+
+ /// Gets or sets the number of additional times the animation repeats after the first play. Use -1 for infinite.
+ public int RepeatCount { get; set; } = 0;
+
+ /// Gets or sets the repeat mode (Restart or Reverse ping-pong).
+ public SKSkottieRepeatMode RepeatMode { get; set; } = SKSkottieRepeatMode.Restart;
+
+ ///
+ /// Gets or sets the playback speed multiplier.
+ /// 1.0 = normal speed, 2.0 = double speed, 0.5 = half speed, negative = reverse.
+ ///
+ public double AnimationSpeed { get; set; } = 1.0;
+
+ /// Gets whether an animation is currently loaded.
+ public bool HasAnimation => animation is not null;
+
+ /// Fires when the animation completes all repeats.
+ public event EventHandler? AnimationCompleted;
+
+ ///
+ /// Sets the animation to play. Pass null to clear the current animation.
+ /// Resets playback state (Progress, IsComplete, repeat counters).
+ ///
+ public void SetAnimation(Skottie.Animation? newAnimation)
+ {
+ animation = newAnimation;
+ Reset();
+ }
+
+ ///
+ /// Advances the animation by the given time delta, applying AnimationSpeed and RepeatMode.
+ /// Call this on each frame tick.
+ ///
+ public void Update(TimeSpan deltaTime)
+ {
+ if (animation is null)
+ return;
+
+ // Apply animation speed with overflow protection
+ var scaledTicks = deltaTime.Ticks * AnimationSpeed;
+ const long SafeMax = long.MaxValue - 1;
+ const long SafeMin = long.MinValue + 2;
+ if (double.IsNaN(scaledTicks) || double.IsInfinity(scaledTicks))
+ scaledTicks = double.IsNaN(scaledTicks) || scaledTicks < 0 ? SafeMin : SafeMax;
+ else if (scaledTicks > SafeMax)
+ scaledTicks = SafeMax;
+ else if (scaledTicks < SafeMin)
+ scaledTicks = SafeMin;
+ deltaTime = TimeSpan.FromTicks((long)scaledTicks);
+
+ // Apply phase direction (for RepeatMode.Reverse ping-pong)
+ if (!isInForwardPhase)
+ deltaTime = -deltaTime;
+
+ var newProgress = Progress + deltaTime;
+ if (newProgress > Duration)
+ newProgress = Duration;
+ if (newProgress < TimeSpan.Zero)
+ newProgress = TimeSpan.Zero;
+
+ Progress = newProgress;
+ }
+
+ /// Renders the current animation frame to the given canvas within the specified rectangle.
+ public void Render(SKCanvas canvas, SKRect rect)
+ {
+ animation?.Render(canvas, rect);
+ }
+
+ private void UpdateProgress(TimeSpan progress)
+ {
+ if (animation is null)
+ {
+ IsComplete = true;
+ return;
+ }
+
+ animation.SeekFrameTime(progress.TotalSeconds);
+
+ // Skip completion/repeat logic during Reset to avoid spurious events
+ if (isResetting)
+ return;
+
+ var repeatMode = RepeatMode;
+ var duration = Duration;
+
+ // Determine effective movement direction
+ var movingForward = AnimationSpeed >= 0 ? isInForwardPhase : !isInForwardPhase;
+
+ // Have we reached a boundary based on our movement direction?
+ var atStart = !movingForward && progress <= TimeSpan.Zero;
+ var atEnd = movingForward && progress >= duration;
+
+ // A run is "finished" based on RepeatMode
+ var reverseFinishPoint = AnimationSpeed >= 0 ? atStart : atEnd;
+ var isFinishedRun = repeatMode == SKSkottieRepeatMode.Restart
+ ? (movingForward ? atEnd : atStart)
+ : reverseFinishPoint;
+
+ // For Reverse mode: flip direction when hitting a boundary (but not the finish boundary)
+ var needsFlip = repeatMode == SKSkottieRepeatMode.Reverse &&
+ (AnimationSpeed >= 0 ? atEnd : atStart) && !isFinishedRun;
+
+ if (needsFlip)
+ {
+ isInForwardPhase = !isInForwardPhase;
+ IsComplete = false;
+ }
+ else
+ {
+ var totalRepeatCount = RepeatCount;
+ if (totalRepeatCount < 0)
+ totalRepeatCount = int.MaxValue;
+
+ var infinite = totalRepeatCount == int.MaxValue;
+ if (infinite)
+ repeatsCompleted = 0;
+
+ if (isFinishedRun && repeatsCompleted < totalRepeatCount)
+ {
+ if (!infinite)
+ repeatsCompleted++;
+
+ isFinishedRun = false;
+
+ if (repeatMode == SKSkottieRepeatMode.Restart)
+ {
+ Progress = AnimationSpeed >= 0 ? TimeSpan.Zero : Duration;
+ }
+ else if (repeatMode == SKSkottieRepeatMode.Reverse)
+ isInForwardPhase = !isInForwardPhase;
+ }
+
+ IsComplete =
+ isFinishedRun &&
+ repeatsCompleted >= totalRepeatCount;
+
+ if (IsComplete)
+ AnimationCompleted?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ private void Reset()
+ {
+ isResetting = true;
+ try
+ {
+ isInForwardPhase = true;
+ repeatsCompleted = 0;
+
+ Duration = animation?.Duration ?? TimeSpan.Zero;
+ Progress = AnimationSpeed < 0 ? Duration : TimeSpan.Zero;
+ }
+ finally
+ {
+ isResetting = false;
+ }
+ }
+}
diff --git a/source/SkiaSharp.Extended/Lottie/SKSkottieRepeatMode.cs b/source/SkiaSharp.Extended/Lottie/SKSkottieRepeatMode.cs
new file mode 100644
index 0000000000..a1f4ab0dbd
--- /dev/null
+++ b/source/SkiaSharp.Extended/Lottie/SKSkottieRepeatMode.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace SkiaSharp.Extended;
+
+public enum SKSkottieRepeatMode
+{
+ Restart,
+ Reverse
+}
diff --git a/source/SkiaSharp.Extended/SkiaSharp.Extended.csproj b/source/SkiaSharp.Extended/SkiaSharp.Extended.csproj
index 5e76557727..51e82f3367 100644
--- a/source/SkiaSharp.Extended/SkiaSharp.Extended.csproj
+++ b/source/SkiaSharp.Extended/SkiaSharp.Extended.csproj
@@ -15,6 +15,7 @@
+
\ No newline at end of file
From a963a8b8eb89e1ae8713a3d359379dba9c2c999d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Feb 2026 16:42:22 +0000
Subject: [PATCH 03/31] =?UTF-8?q?Rename=20SKSkottiePlayer=E2=86=92SKLottie?=
=?UTF-8?q?Player,=20move=20shared=20Lottie=20types=20to=20core=20SkiaShar?=
=?UTF-8?q?p.Extended?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
---
.../SkiaSharpDemo.Blazor/Pages/Lottie.razor | 10 +++---
.../Demos/Lottie/LottiePage.xaml.cs | 1 +
.../Lottie/SKLottieAnimation.shared.cs | 34 +------------------
...SKLottieAnimationFailedEventArgs.shared.cs | 29 +---------------
.../Lottie/SKLottieRepeatMode.shared.cs | 17 +---------
.../Controls/Lottie/SKLottieView.shared.cs | 4 +--
.../Lottie/SKLottieAnimation.cs | 17 ++++++++++
.../SKLottieAnimationFailedEventArgs.cs | 17 ++++++++++
.../{SKSkottiePlayer.cs => SKLottiePlayer.cs} | 12 +++----
...tieRepeatMode.cs => SKLottieRepeatMode.cs} | 2 +-
10 files changed, 52 insertions(+), 91 deletions(-)
create mode 100644 source/SkiaSharp.Extended/Lottie/SKLottieAnimation.cs
create mode 100644 source/SkiaSharp.Extended/Lottie/SKLottieAnimationFailedEventArgs.cs
rename source/SkiaSharp.Extended/Lottie/{SKSkottiePlayer.cs => SKLottiePlayer.cs} (93%)
rename source/SkiaSharp.Extended/Lottie/{SKSkottieRepeatMode.cs => SKLottieRepeatMode.cs} (68%)
diff --git a/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor b/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
index b8a7de0ea4..50f0b63e65 100644
--- a/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
+++ b/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
@@ -8,7 +8,7 @@
Lottie Animation
- Play Lottie animations using SKSkottiePlayer from
+ Play Lottie animations using SKLottiePlayer from
SkiaSharp.Extended. The same player logic is shared between
this Blazor component and the .NET MAUI SKLottieView.
@@ -17,8 +17,8 @@
@@ -74,11 +74,11 @@
@code {
private SKCanvasView? canvas;
- private readonly SKSkottiePlayer player = new() { RepeatCount = -1 };
+ private readonly SKLottiePlayer player = new() { RepeatCount = -1 };
private Animation? loadedAnimation;
private CancellationTokenSource? animationCts;
private bool isLoading = true;
- private SKSkottieRepeatMode repeatMode = SKSkottieRepeatMode.Restart;
+ private SKLottieRepeatMode repeatMode = SKLottieRepeatMode.Restart;
private double animationSpeed = 1.0;
private int repeatCount = -1;
private DateTime lastFrame;
diff --git a/samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml.cs b/samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml.cs
index b2ab605435..7725adc739 100644
--- a/samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml.cs
+++ b/samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml.cs
@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Windows.Input;
+using SkiaSharp.Extended;
using SkiaSharp.Extended.UI.Controls;
namespace SkiaSharpDemo.Demos;
diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimation.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimation.shared.cs
index 325ae3a2d8..5c1a4278a7 100644
--- a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimation.shared.cs
+++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimation.shared.cs
@@ -1,33 +1 @@
-namespace SkiaSharp.Extended.UI.Controls;
-
-///
-/// Wraps a loaded Skottie animation instance.
-///
-public class SKLottieAnimation
-{
- ///
- /// Initializes a new instance of the class with no animation loaded.
- ///
- public SKLottieAnimation()
- {
- }
-
- ///
- /// Initializes a new instance of the class with the specified animation.
- ///
- /// The Skottie animation instance, or .
- public SKLottieAnimation(Skottie.Animation? animation)
- {
- Animation = animation;
- }
-
- ///
- /// Gets the underlying Skottie animation instance, or if not loaded.
- ///
- public Skottie.Animation? Animation { get; }
-
- ///
- /// Gets a value indicating whether the animation is loaded.
- ///
- public bool IsLoaded => Animation is not null;
-}
+global using SKLottieAnimation = SkiaSharp.Extended.SKLottieAnimation;
diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimationFailedEventArgs.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimationFailedEventArgs.shared.cs
index eab0a988e7..771a75a869 100644
--- a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimationFailedEventArgs.shared.cs
+++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimationFailedEventArgs.shared.cs
@@ -1,28 +1 @@
-namespace SkiaSharp.Extended.UI.Controls;
-
-///
-/// Event arguments for when a Lottie animation fails to load.
-///
-public class SKLottieAnimationFailedEventArgs : EventArgs
-{
- ///
- /// Initializes a new instance of the class.
- ///
- public SKLottieAnimationFailedEventArgs()
- {
- }
-
- ///
- /// Initializes a new instance of the class with the specified exception.
- ///
- /// The exception that caused the failure, or .
- public SKLottieAnimationFailedEventArgs(Exception? exception)
- {
- Exception = exception;
- }
-
- ///
- /// Gets the exception that caused the failure, or .
- ///
- public Exception? Exception { get; }
-}
+global using SKLottieAnimationFailedEventArgs = SkiaSharp.Extended.SKLottieAnimationFailedEventArgs;
diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieRepeatMode.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieRepeatMode.shared.cs
index 0107f8b7c9..b170962146 100644
--- a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieRepeatMode.shared.cs
+++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieRepeatMode.shared.cs
@@ -1,16 +1 @@
-namespace SkiaSharp.Extended.UI.Controls;
-
-///
-/// Specifies how a Lottie animation repeats.
-///
-public enum SKLottieRepeatMode
-{
- ///
- /// The animation restarts from the beginning when repeating.
- ///
- Restart,
- ///
- /// The animation reverses direction when repeating (ping-pong).
- ///
- Reverse
-}
+global using SKLottieRepeatMode = SkiaSharp.Extended.SKLottieRepeatMode;
diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
index 49f1d01e8e..04b20d94ae 100644
--- a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
+++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
@@ -78,7 +78,7 @@ public class SKLottieView : SKAnimatedSurfaceView
typeof(SKLottieView),
1.0);
- private readonly SKSkottiePlayer player = new();
+ private readonly SKLottiePlayer player = new();
private CancellationTokenSource? loadCancellation;
private bool isSyncingFromPlayer;
@@ -185,7 +185,7 @@ public double AnimationSpeed
protected override void Update(TimeSpan deltaTime)
{
player.RepeatCount = RepeatCount;
- player.RepeatMode = (SKSkottieRepeatMode)RepeatMode;
+ player.RepeatMode = RepeatMode;
player.AnimationSpeed = AnimationSpeed;
player.Update(deltaTime);
diff --git a/source/SkiaSharp.Extended/Lottie/SKLottieAnimation.cs b/source/SkiaSharp.Extended/Lottie/SKLottieAnimation.cs
new file mode 100644
index 0000000000..bf17be1dd5
--- /dev/null
+++ b/source/SkiaSharp.Extended/Lottie/SKLottieAnimation.cs
@@ -0,0 +1,17 @@
+namespace SkiaSharp.Extended;
+
+public class SKLottieAnimation
+{
+ public SKLottieAnimation()
+ {
+ }
+
+ public SKLottieAnimation(Skottie.Animation? animation)
+ {
+ Animation = animation;
+ }
+
+ public Skottie.Animation? Animation { get; }
+
+ public bool IsLoaded => Animation is not null;
+}
diff --git a/source/SkiaSharp.Extended/Lottie/SKLottieAnimationFailedEventArgs.cs b/source/SkiaSharp.Extended/Lottie/SKLottieAnimationFailedEventArgs.cs
new file mode 100644
index 0000000000..a3d0d423ae
--- /dev/null
+++ b/source/SkiaSharp.Extended/Lottie/SKLottieAnimationFailedEventArgs.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace SkiaSharp.Extended;
+
+public class SKLottieAnimationFailedEventArgs : EventArgs
+{
+ public SKLottieAnimationFailedEventArgs()
+ {
+ }
+
+ public SKLottieAnimationFailedEventArgs(Exception? exception)
+ {
+ Exception = exception;
+ }
+
+ public Exception? Exception { get; }
+}
diff --git a/source/SkiaSharp.Extended/Lottie/SKSkottiePlayer.cs b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
similarity index 93%
rename from source/SkiaSharp.Extended/Lottie/SKSkottiePlayer.cs
rename to source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
index a818737235..6cbea822d1 100644
--- a/source/SkiaSharp.Extended/Lottie/SKSkottiePlayer.cs
+++ b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
@@ -6,7 +6,7 @@ namespace SkiaSharp.Extended;
/// A platform-agnostic Skottie (Lottie) animation player that manages playback state
/// and rendering. Can be shared between MAUI, Blazor, and other platforms.
///
-public class SKSkottiePlayer
+public class SKLottiePlayer
{
private Skottie.Animation? animation;
private bool isInForwardPhase = true;
@@ -39,7 +39,7 @@ public TimeSpan Progress
public int RepeatCount { get; set; } = 0;
/// Gets or sets the repeat mode (Restart or Reverse ping-pong).
- public SKSkottieRepeatMode RepeatMode { get; set; } = SKSkottieRepeatMode.Restart;
+ public SKLottieRepeatMode RepeatMode { get; set; } = SKLottieRepeatMode.Restart;
///
/// Gets or sets the playback speed multiplier.
@@ -129,12 +129,12 @@ private void UpdateProgress(TimeSpan progress)
// A run is "finished" based on RepeatMode
var reverseFinishPoint = AnimationSpeed >= 0 ? atStart : atEnd;
- var isFinishedRun = repeatMode == SKSkottieRepeatMode.Restart
+ var isFinishedRun = repeatMode == SKLottieRepeatMode.Restart
? (movingForward ? atEnd : atStart)
: reverseFinishPoint;
// For Reverse mode: flip direction when hitting a boundary (but not the finish boundary)
- var needsFlip = repeatMode == SKSkottieRepeatMode.Reverse &&
+ var needsFlip = repeatMode == SKLottieRepeatMode.Reverse &&
(AnimationSpeed >= 0 ? atEnd : atStart) && !isFinishedRun;
if (needsFlip)
@@ -159,11 +159,11 @@ private void UpdateProgress(TimeSpan progress)
isFinishedRun = false;
- if (repeatMode == SKSkottieRepeatMode.Restart)
+ if (repeatMode == SKLottieRepeatMode.Restart)
{
Progress = AnimationSpeed >= 0 ? TimeSpan.Zero : Duration;
}
- else if (repeatMode == SKSkottieRepeatMode.Reverse)
+ else if (repeatMode == SKLottieRepeatMode.Reverse)
isInForwardPhase = !isInForwardPhase;
}
diff --git a/source/SkiaSharp.Extended/Lottie/SKSkottieRepeatMode.cs b/source/SkiaSharp.Extended/Lottie/SKLottieRepeatMode.cs
similarity index 68%
rename from source/SkiaSharp.Extended/Lottie/SKSkottieRepeatMode.cs
rename to source/SkiaSharp.Extended/Lottie/SKLottieRepeatMode.cs
index a1f4ab0dbd..de4e59aee0 100644
--- a/source/SkiaSharp.Extended/Lottie/SKSkottieRepeatMode.cs
+++ b/source/SkiaSharp.Extended/Lottie/SKLottieRepeatMode.cs
@@ -2,7 +2,7 @@
namespace SkiaSharp.Extended;
-public enum SKSkottieRepeatMode
+public enum SKLottieRepeatMode
{
Restart,
Reverse
From 9d3c657599a668c177343a1cf3be3abe3fd1f6f1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Feb 2026 17:04:48 +0000
Subject: [PATCH 04/31] Replace global using aliases with [assembly:
TypeForwardedTo] for moved Lottie types
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
---
samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml | 7 ++++---
.../Controls/Lottie/SKLottieAnimation.shared.cs | 4 +++-
.../Lottie/SKLottieAnimationFailedEventArgs.shared.cs | 4 +++-
.../Controls/Lottie/SKLottieRepeatMode.shared.cs | 4 +++-
.../SkiaSharp.Extended.UI.Maui.csproj | 1 +
5 files changed, 14 insertions(+), 6 deletions(-)
diff --git a/samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml b/samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml
index 6cc4bd0a3d..9d9d2a8e6c 100644
--- a/samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml
+++ b/samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml
@@ -1,6 +1,7 @@
@@ -55,9 +56,9 @@
-
- Restart
- Reverse
+
+ Restart
+ Reverse
diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimation.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimation.shared.cs
index 5c1a4278a7..f177949538 100644
--- a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimation.shared.cs
+++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimation.shared.cs
@@ -1 +1,3 @@
-global using SKLottieAnimation = SkiaSharp.Extended.SKLottieAnimation;
+using System.Runtime.CompilerServices;
+
+[assembly: TypeForwardedTo(typeof(SkiaSharp.Extended.SKLottieAnimation))]
diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimationFailedEventArgs.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimationFailedEventArgs.shared.cs
index 771a75a869..6add7b6328 100644
--- a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimationFailedEventArgs.shared.cs
+++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimationFailedEventArgs.shared.cs
@@ -1 +1,3 @@
-global using SKLottieAnimationFailedEventArgs = SkiaSharp.Extended.SKLottieAnimationFailedEventArgs;
+using System.Runtime.CompilerServices;
+
+[assembly: TypeForwardedTo(typeof(SkiaSharp.Extended.SKLottieAnimationFailedEventArgs))]
diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieRepeatMode.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieRepeatMode.shared.cs
index b170962146..726825af14 100644
--- a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieRepeatMode.shared.cs
+++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieRepeatMode.shared.cs
@@ -1 +1,3 @@
-global using SKLottieRepeatMode = SkiaSharp.Extended.SKLottieRepeatMode;
+using System.Runtime.CompilerServices;
+
+[assembly: TypeForwardedTo(typeof(SkiaSharp.Extended.SKLottieRepeatMode))]
diff --git a/source/SkiaSharp.Extended.UI.Maui/SkiaSharp.Extended.UI.Maui.csproj b/source/SkiaSharp.Extended.UI.Maui/SkiaSharp.Extended.UI.Maui.csproj
index 1c0365a82a..6c8c14cc89 100644
--- a/source/SkiaSharp.Extended.UI.Maui/SkiaSharp.Extended.UI.Maui.csproj
+++ b/source/SkiaSharp.Extended.UI.Maui/SkiaSharp.Extended.UI.Maui.csproj
@@ -44,6 +44,7 @@
+
From 7f2fc9125c61fdbb5cdbecae0e7d567b2d8a03e0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Feb 2026 17:12:11 +0000
Subject: [PATCH 05/31] Keep SKLottieAnimation and
SKLottieAnimationFailedEventArgs in MAUI, remove from core
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
---
.../Lottie/SKLottieAnimation.shared.cs | 18 ++++++++++++++++--
.../SKLottieAnimationFailedEventArgs.shared.cs | 16 ++++++++++++++--
.../Lottie/SKLottieAnimation.cs | 17 -----------------
.../Lottie/SKLottieAnimationFailedEventArgs.cs | 17 -----------------
4 files changed, 30 insertions(+), 38 deletions(-)
delete mode 100644 source/SkiaSharp.Extended/Lottie/SKLottieAnimation.cs
delete mode 100644 source/SkiaSharp.Extended/Lottie/SKLottieAnimationFailedEventArgs.cs
diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimation.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimation.shared.cs
index f177949538..e8953a9032 100644
--- a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimation.shared.cs
+++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimation.shared.cs
@@ -1,3 +1,17 @@
-using System.Runtime.CompilerServices;
+namespace SkiaSharp.Extended.UI.Controls;
-[assembly: TypeForwardedTo(typeof(SkiaSharp.Extended.SKLottieAnimation))]
+public class SKLottieAnimation
+{
+public SKLottieAnimation()
+{
+}
+
+public SKLottieAnimation(Skottie.Animation? animation)
+{
+Animation = animation;
+}
+
+public Skottie.Animation? Animation { get; }
+
+public bool IsLoaded => Animation is not null;
+}
diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimationFailedEventArgs.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimationFailedEventArgs.shared.cs
index 6add7b6328..10cc40c154 100644
--- a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimationFailedEventArgs.shared.cs
+++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieAnimationFailedEventArgs.shared.cs
@@ -1,3 +1,15 @@
-using System.Runtime.CompilerServices;
+namespace SkiaSharp.Extended.UI.Controls;
-[assembly: TypeForwardedTo(typeof(SkiaSharp.Extended.SKLottieAnimationFailedEventArgs))]
+public class SKLottieAnimationFailedEventArgs : EventArgs
+{
+public SKLottieAnimationFailedEventArgs()
+{
+}
+
+public SKLottieAnimationFailedEventArgs(Exception? exception)
+{
+Exception = exception;
+}
+
+public Exception? Exception { get; }
+}
diff --git a/source/SkiaSharp.Extended/Lottie/SKLottieAnimation.cs b/source/SkiaSharp.Extended/Lottie/SKLottieAnimation.cs
deleted file mode 100644
index bf17be1dd5..0000000000
--- a/source/SkiaSharp.Extended/Lottie/SKLottieAnimation.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace SkiaSharp.Extended;
-
-public class SKLottieAnimation
-{
- public SKLottieAnimation()
- {
- }
-
- public SKLottieAnimation(Skottie.Animation? animation)
- {
- Animation = animation;
- }
-
- public Skottie.Animation? Animation { get; }
-
- public bool IsLoaded => Animation is not null;
-}
diff --git a/source/SkiaSharp.Extended/Lottie/SKLottieAnimationFailedEventArgs.cs b/source/SkiaSharp.Extended/Lottie/SKLottieAnimationFailedEventArgs.cs
deleted file mode 100644
index a3d0d423ae..0000000000
--- a/source/SkiaSharp.Extended/Lottie/SKLottieAnimationFailedEventArgs.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System;
-
-namespace SkiaSharp.Extended;
-
-public class SKLottieAnimationFailedEventArgs : EventArgs
-{
- public SKLottieAnimationFailedEventArgs()
- {
- }
-
- public SKLottieAnimationFailedEventArgs(Exception? exception)
- {
- Exception = exception;
- }
-
- public Exception? Exception { get; }
-}
From 5143ccad5a5103e09fcc15fbf43e6a446b012ae5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Feb 2026 21:31:06 +0000
Subject: [PATCH 06/31] Replace SKLottieRepeatMode enum with SKLottieRepeat
readonly struct; move enum back to MAUI
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
---
.../SkiaSharpDemo.Blazor/Pages/Lottie.razor | 27 +++-----
.../Demos/Lottie/LottiePage.xaml | 7 +-
.../Demos/Lottie/LottiePage.xaml.cs | 1 -
.../Lottie/SKLottieRepeatMode.shared.cs | 8 ++-
.../Controls/Lottie/SKLottieView.shared.cs | 5 +-
.../Lottie/SKLottiePlayer.cs | 25 +++----
.../Lottie/SKLottieRepeat.cs | 67 +++++++++++++++++++
.../Lottie/SKLottieRepeatMode.cs | 9 ---
8 files changed, 99 insertions(+), 50 deletions(-)
create mode 100644 source/SkiaSharp.Extended/Lottie/SKLottieRepeat.cs
delete mode 100644 source/SkiaSharp.Extended/Lottie/SKLottieRepeatMode.cs
diff --git a/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor b/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
index 50f0b63e65..d2711de348 100644
--- a/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
+++ b/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
@@ -16,9 +16,9 @@
-
@@ -29,7 +29,7 @@
-
+
@@ -74,11 +74,11 @@
@code {
private SKCanvasView? canvas;
- private readonly SKLottiePlayer player = new() { RepeatCount = -1 };
+ private readonly SKLottiePlayer player = new();
private Animation? loadedAnimation;
private CancellationTokenSource? animationCts;
private bool isLoading = true;
- private SKLottieRepeatMode repeatMode = SKLottieRepeatMode.Restart;
+ private string repeatKind = "restart";
private double animationSpeed = 1.0;
private int repeatCount = -1;
private DateTime lastFrame;
@@ -147,21 +147,11 @@
player.Render(skCanvas, SKRect.Create(0, 0, info.Width, info.Height));
}
- private void OnRepeatModeChanged()
- {
- player.RepeatMode = repeatMode;
- }
-
private void OnSpeedChanged()
{
player.AnimationSpeed = animationSpeed;
}
- private void OnRepeatCountChanged()
- {
- player.RepeatCount = repeatCount;
- }
-
private void Restart()
{
ApplySettings();
@@ -171,9 +161,10 @@
private void ApplySettings()
{
- player.RepeatMode = repeatMode;
+ player.Repeat = repeatKind == "reverse"
+ ? SKLottieRepeat.Reverse(repeatCount)
+ : SKLottieRepeat.Restart(repeatCount);
player.AnimationSpeed = animationSpeed;
- player.RepeatCount = repeatCount;
}
public ValueTask DisposeAsync()
diff --git a/samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml b/samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml
index 9d9d2a8e6c..6cc4bd0a3d 100644
--- a/samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml
+++ b/samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml
@@ -1,7 +1,6 @@
@@ -56,9 +55,9 @@
-
- Restart
- Reverse
+
+ Restart
+ Reverse
diff --git a/samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml.cs b/samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml.cs
index 7725adc739..b2ab605435 100644
--- a/samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml.cs
+++ b/samples/SkiaSharpDemo/Demos/Lottie/LottiePage.xaml.cs
@@ -1,6 +1,5 @@
using System.Diagnostics;
using System.Windows.Input;
-using SkiaSharp.Extended;
using SkiaSharp.Extended.UI.Controls;
namespace SkiaSharpDemo.Demos;
diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieRepeatMode.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieRepeatMode.shared.cs
index 726825af14..46848c3e22 100644
--- a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieRepeatMode.shared.cs
+++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieRepeatMode.shared.cs
@@ -1,3 +1,7 @@
-using System.Runtime.CompilerServices;
+namespace SkiaSharp.Extended.UI.Controls;
-[assembly: TypeForwardedTo(typeof(SkiaSharp.Extended.SKLottieRepeatMode))]
+public enum SKLottieRepeatMode
+{
+Restart,
+Reverse
+}
diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
index 04b20d94ae..39bab9bf41 100644
--- a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
+++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
@@ -184,8 +184,9 @@ public double AnimationSpeed
///
protected override void Update(TimeSpan deltaTime)
{
- player.RepeatCount = RepeatCount;
- player.RepeatMode = RepeatMode;
+ player.Repeat = RepeatMode == SKLottieRepeatMode.Reverse
+ ? SKLottieRepeat.Reverse(RepeatCount)
+ : SKLottieRepeat.Restart(RepeatCount);
player.AnimationSpeed = AnimationSpeed;
player.Update(deltaTime);
diff --git a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
index 6cbea822d1..bb26741e61 100644
--- a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
+++ b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
@@ -35,11 +35,8 @@ public TimeSpan Progress
/// Gets whether the animation has completed all repeats.
public bool IsComplete { get; private set; } = false;
- /// Gets or sets the number of additional times the animation repeats after the first play. Use -1 for infinite.
- public int RepeatCount { get; set; } = 0;
-
- /// Gets or sets the repeat mode (Restart or Reverse ping-pong).
- public SKLottieRepeatMode RepeatMode { get; set; } = SKLottieRepeatMode.Restart;
+ /// Gets or sets how the animation repeats. Defaults to .
+ public SKLottieRepeat Repeat { get; set; } = SKLottieRepeat.Never;
///
/// Gets or sets the playback speed multiplier.
@@ -64,7 +61,7 @@ public void SetAnimation(Skottie.Animation? newAnimation)
}
///
- /// Advances the animation by the given time delta, applying AnimationSpeed and RepeatMode.
+ /// Advances the animation by the given time delta, applying AnimationSpeed and Repeat.
/// Call this on each frame tick.
///
public void Update(TimeSpan deltaTime)
@@ -84,7 +81,7 @@ public void Update(TimeSpan deltaTime)
scaledTicks = SafeMin;
deltaTime = TimeSpan.FromTicks((long)scaledTicks);
- // Apply phase direction (for RepeatMode.Reverse ping-pong)
+ // Apply phase direction (for Reverse ping-pong)
if (!isInForwardPhase)
deltaTime = -deltaTime;
@@ -117,7 +114,7 @@ private void UpdateProgress(TimeSpan progress)
if (isResetting)
return;
- var repeatMode = RepeatMode;
+ var repeat = Repeat;
var duration = Duration;
// Determine effective movement direction
@@ -127,14 +124,14 @@ private void UpdateProgress(TimeSpan progress)
var atStart = !movingForward && progress <= TimeSpan.Zero;
var atEnd = movingForward && progress >= duration;
- // A run is "finished" based on RepeatMode
+ // A run is "finished" based on repeat kind
var reverseFinishPoint = AnimationSpeed >= 0 ? atStart : atEnd;
- var isFinishedRun = repeatMode == SKLottieRepeatMode.Restart
+ var isFinishedRun = repeat.IsRestartRepeating
? (movingForward ? atEnd : atStart)
: reverseFinishPoint;
// For Reverse mode: flip direction when hitting a boundary (but not the finish boundary)
- var needsFlip = repeatMode == SKLottieRepeatMode.Reverse &&
+ var needsFlip = repeat.IsReverseRepeating &&
(AnimationSpeed >= 0 ? atEnd : atStart) && !isFinishedRun;
if (needsFlip)
@@ -144,7 +141,7 @@ private void UpdateProgress(TimeSpan progress)
}
else
{
- var totalRepeatCount = RepeatCount;
+ var totalRepeatCount = repeat.Count;
if (totalRepeatCount < 0)
totalRepeatCount = int.MaxValue;
@@ -159,11 +156,11 @@ private void UpdateProgress(TimeSpan progress)
isFinishedRun = false;
- if (repeatMode == SKLottieRepeatMode.Restart)
+ if (repeat.IsRestartRepeating)
{
Progress = AnimationSpeed >= 0 ? TimeSpan.Zero : Duration;
}
- else if (repeatMode == SKLottieRepeatMode.Reverse)
+ else if (repeat.IsReverseRepeating)
isInForwardPhase = !isInForwardPhase;
}
diff --git a/source/SkiaSharp.Extended/Lottie/SKLottieRepeat.cs b/source/SkiaSharp.Extended/Lottie/SKLottieRepeat.cs
new file mode 100644
index 0000000000..92342582fb
--- /dev/null
+++ b/source/SkiaSharp.Extended/Lottie/SKLottieRepeat.cs
@@ -0,0 +1,67 @@
+using System;
+
+namespace SkiaSharp.Extended;
+
+///
+/// Describes how a Lottie animation repeats. Use the static factory members
+/// , , and to
+/// create instances.
+///
+public readonly struct SKLottieRepeat : IEquatable
+{
+ private enum RepeatKind { Never, Restart, Reverse }
+
+ private readonly RepeatKind kind;
+ private readonly int count;
+
+ private SKLottieRepeat(RepeatKind kind, int count)
+ {
+ this.kind = kind;
+ this.count = count;
+ }
+
+ /// The animation plays once without repeating.
+ public static SKLottieRepeat Never => new(RepeatKind.Never, 0);
+
+ ///
+ /// The animation repeats by restarting from the beginning.
+ ///
+ /// Number of additional plays after the first. Use -1 for infinite.
+ public static SKLottieRepeat Restart(int count = -1) => new(RepeatKind.Restart, count);
+
+ ///
+ /// The animation repeats by reversing direction (ping-pong).
+ ///
+ /// Number of additional plays after the first. Use -1 for infinite.
+ public static SKLottieRepeat Reverse(int count = -1) => new(RepeatKind.Reverse, count);
+
+ /// Gets whether the animation repeats at all.
+ public bool IsRepeating => kind != RepeatKind.Never;
+
+ /// Gets whether the animation repeats by restarting from the beginning.
+ public bool IsRestartRepeating => kind == RepeatKind.Restart;
+
+ /// Gets whether the animation repeats by reversing direction (ping-pong).
+ public bool IsReverseRepeating => kind == RepeatKind.Reverse;
+
+ ///
+ /// Gets the number of additional plays after the first. -1 means infinite.
+ /// Returns 0 for .
+ ///
+ public int Count => count;
+
+ ///
+ public bool Equals(SKLottieRepeat other) => kind == other.kind && count == other.count;
+
+ ///
+ public override bool Equals(object? obj) => obj is SKLottieRepeat other && Equals(other);
+
+ ///
+ public override int GetHashCode() => (int)kind * 397 ^ count;
+
+ ///
+ public static bool operator ==(SKLottieRepeat left, SKLottieRepeat right) => left.Equals(right);
+
+ ///
+ public static bool operator !=(SKLottieRepeat left, SKLottieRepeat right) => !left.Equals(right);
+}
diff --git a/source/SkiaSharp.Extended/Lottie/SKLottieRepeatMode.cs b/source/SkiaSharp.Extended/Lottie/SKLottieRepeatMode.cs
deleted file mode 100644
index de4e59aee0..0000000000
--- a/source/SkiaSharp.Extended/Lottie/SKLottieRepeatMode.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using System;
-
-namespace SkiaSharp.Extended;
-
-public enum SKLottieRepeatMode
-{
- Restart,
- Reverse
-}
From 184c54d9ec95c508aa5a4e8c0a032b49c731459a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Feb 2026 21:56:36 +0000
Subject: [PATCH 07/31] Redesign SKLottieView/Player sync: event-driven, remove
isSyncingFromPlayer guard
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
---
.../Controls/Lottie/SKLottieView.shared.cs | 94 +++++++++----------
.../Lottie/SKLottiePlayer.cs | 8 ++
2 files changed, 54 insertions(+), 48 deletions(-)
diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
index 39bab9bf41..ed4d60eaf6 100644
--- a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
+++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
@@ -20,8 +20,7 @@ public class SKLottieView : SKAnimatedSurfaceView
typeof(TimeSpan),
typeof(SKLottieView),
TimeSpan.Zero,
- defaultBindingMode: BindingMode.OneWayToSource,
- propertyChanged: OnProgressDurationPropertyChanged);
+ defaultBindingMode: BindingMode.OneWayToSource);
///
/// Identifies the bindable property.
@@ -37,7 +36,7 @@ public class SKLottieView : SKAnimatedSurfaceView
typeof(SKLottieView),
TimeSpan.Zero,
BindingMode.TwoWay,
- propertyChanged: OnProgressDurationPropertyChanged);
+ propertyChanged: OnProgressPropertyChanged);
private static readonly BindablePropertyKey IsCompletePropertyKey = BindableProperty.CreateReadOnly(
nameof(IsComplete),
@@ -58,7 +57,8 @@ public class SKLottieView : SKAnimatedSurfaceView
nameof(RepeatCount),
typeof(int),
typeof(SKLottieView),
- 0);
+ 0,
+ propertyChanged: OnRepeatPropertyChanged);
///
/// Identifies the bindable property.
@@ -67,7 +67,8 @@ public class SKLottieView : SKAnimatedSurfaceView
nameof(RepeatMode),
typeof(SKLottieRepeatMode),
typeof(SKLottieView),
- SKLottieRepeatMode.Restart);
+ SKLottieRepeatMode.Restart,
+ propertyChanged: OnRepeatPropertyChanged);
///
/// Identifies the bindable property.
@@ -76,11 +77,11 @@ public class SKLottieView : SKAnimatedSurfaceView
nameof(AnimationSpeed),
typeof(double),
typeof(SKLottieView),
- 1.0);
+ 1.0,
+ propertyChanged: OnAnimationSpeedPropertyChanged);
private readonly SKLottiePlayer player = new();
private CancellationTokenSource? loadCancellation;
- private bool isSyncingFromPlayer;
///
/// Initializes a new instance of the class.
@@ -91,6 +92,12 @@ public SKLottieView()
IsAnimationEnabled = true;
+ // Initialize player from default property values (propertyChanged callbacks don't
+ // fire for the initial default, so we push the defaults explicitly here).
+ player.Repeat = SKLottieRepeat.Restart(RepeatCount);
+ player.AnimationSpeed = AnimationSpeed;
+
+ player.AnimationUpdated += OnPlayerAnimationUpdated;
player.AnimationCompleted += OnPlayerAnimationCompleted;
#if DEBUG
@@ -184,14 +191,7 @@ public double AnimationSpeed
///
protected override void Update(TimeSpan deltaTime)
{
- player.Repeat = RepeatMode == SKLottieRepeatMode.Reverse
- ? SKLottieRepeat.Reverse(RepeatCount)
- : SKLottieRepeat.Restart(RepeatCount);
- player.AnimationSpeed = AnimationSpeed;
-
player.Update(deltaTime);
-
- SyncFromPlayer();
}
///
@@ -200,33 +200,11 @@ protected override void OnPaintSurface(SKCanvas canvas, SKSize size)
player.Render(canvas, SKRect.Create(SKPoint.Empty, size));
}
- private void SyncFromPlayer()
+ private void OnPlayerAnimationUpdated(object? sender, EventArgs e)
{
- isSyncingFromPlayer = true;
- try
- {
- Progress = player.Progress;
- IsComplete = player.IsComplete;
- }
- finally
- {
- isSyncingFromPlayer = false;
- }
- }
-
- private void SyncFromLoad()
- {
- isSyncingFromPlayer = true;
- try
- {
- Duration = player.Duration;
- Progress = player.Progress;
- IsComplete = player.IsComplete;
- }
- finally
- {
- isSyncingFromPlayer = false;
- }
+ Duration = player.Duration;
+ Progress = player.Progress;
+ IsComplete = player.IsComplete;
}
private void OnPlayerAnimationCompleted(object? sender, EventArgs e)
@@ -245,7 +223,6 @@ private async Task LoadAnimationAsync(SKLottieImageSource? imageSource)
if (imageSource is null || imageSource.IsEmpty)
{
player.SetAnimation(null);
- SyncFromLoad();
}
else
{
@@ -273,7 +250,6 @@ private async Task LoadAnimationAsync(SKLottieImageSource? imageSource)
}
player.SetAnimation(loadResult?.Animation);
- SyncFromLoad();
if (player.HasAnimation)
AnimationLoaded?.Invoke(this, SKLottieAnimationLoadedEventArgs.Create(loadResult!.Animation!));
@@ -303,21 +279,43 @@ private async void OnSourceChanged(object? sender, EventArgs e)
await LoadAnimationAsync(sender as SKLottieImageSource);
}
- private static void OnProgressDurationPropertyChanged(BindableObject bindable, object? oldValue, object? newValue)
+ private static void OnProgressPropertyChanged(BindableObject bindable, object? oldValue, object? newValue)
{
if (bindable is not SKLottieView lv)
return;
- // Skip if we are syncing from the player to avoid re-entrant seeks
- if (lv.isSyncingFromPlayer)
+ var newProgress = (TimeSpan)newValue!;
+
+ // Skip if the player already has this value (e.g. set via AnimationUpdated event).
+ if (lv.player.Progress == newProgress)
return;
- // User-driven change (e.g. scrubbing): propagate to player
- lv.player.Progress = lv.Progress;
+ // User-driven change (e.g. scrubbing): propagate to player.
+ // AnimationUpdated fires from the Progress setter, which syncs Duration, Progress,
+ // and IsComplete back to the view via OnPlayerAnimationUpdated.
+ lv.player.Progress = newProgress;
- // Trigger repaint if animation is disabled (e.g. user is scrubbing a paused animation)
+ // Trigger repaint if animation is disabled (e.g. user is scrubbing a paused animation).
if (!lv.IsAnimationEnabled)
lv.Invalidate();
}
+ private static void OnRepeatPropertyChanged(BindableObject bindable, object? oldValue, object? newValue)
+ {
+ if (bindable is not SKLottieView lv)
+ return;
+
+ lv.player.Repeat = lv.RepeatMode == SKLottieRepeatMode.Reverse
+ ? SKLottieRepeat.Reverse(lv.RepeatCount)
+ : SKLottieRepeat.Restart(lv.RepeatCount);
+ }
+
+ private static void OnAnimationSpeedPropertyChanged(BindableObject bindable, object? oldValue, object? newValue)
+ {
+ if (bindable is not SKLottieView lv)
+ return;
+
+ lv.player.AnimationSpeed = (double)newValue!;
+ }
+
}
diff --git a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
index bb26741e61..08b357ea88 100644
--- a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
+++ b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
@@ -21,6 +21,7 @@ public class SKLottiePlayer
///
/// Gets or sets the current playback position. Setting this seeks the animation
/// and may trigger completion logic (repeats, AnimationCompleted event).
+ /// Raises after each change.
///
public TimeSpan Progress
{
@@ -29,6 +30,7 @@ public TimeSpan Progress
{
_progress = value;
UpdateProgress(_progress);
+ AnimationUpdated?.Invoke(this, EventArgs.Empty);
}
}
@@ -50,6 +52,12 @@ public TimeSpan Progress
/// Fires when the animation completes all repeats.
public event EventHandler? AnimationCompleted;
+ ///
+ /// Fires after each change to , notifying subscribers of
+ /// updated state (Progress, Duration, IsComplete).
+ ///
+ public event EventHandler? AnimationUpdated;
+
///
/// Sets the animation to play. Pass null to clear the current animation.
/// Resets playback state (Progress, IsComplete, repeat counters).
From 739836d083a9387777609507683ec2a986d9e3d2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Feb 2026 22:15:06 +0000
Subject: [PATCH 08/31] Make SKLottiePlayer.Progress getter-only; add public
Seek(TimeSpan) method
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
---
.../Controls/Lottie/SKLottieView.shared.cs | 4 +--
.../Lottie/SKLottiePlayer.cs | 36 +++++++++----------
2 files changed, 19 insertions(+), 21 deletions(-)
diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
index ed4d60eaf6..26b1eea7ae 100644
--- a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
+++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
@@ -291,9 +291,9 @@ private static void OnProgressPropertyChanged(BindableObject bindable, object? o
return;
// User-driven change (e.g. scrubbing): propagate to player.
- // AnimationUpdated fires from the Progress setter, which syncs Duration, Progress,
+ // AnimationUpdated fires from Seek(), which syncs Duration, Progress,
// and IsComplete back to the view via OnPlayerAnimationUpdated.
- lv.player.Progress = newProgress;
+ lv.player.Seek(newProgress);
// Trigger repaint if animation is disabled (e.g. user is scrubbing a paused animation).
if (!lv.IsAnimationEnabled)
diff --git a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
index 08b357ea88..db10840fcd 100644
--- a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
+++ b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
@@ -18,21 +18,8 @@ public class SKLottiePlayer
/// Gets the total duration of the loaded animation.
public TimeSpan Duration { get; private set; } = TimeSpan.Zero;
- ///
- /// Gets or sets the current playback position. Setting this seeks the animation
- /// and may trigger completion logic (repeats, AnimationCompleted event).
- /// Raises after each change.
- ///
- public TimeSpan Progress
- {
- get => _progress;
- set
- {
- _progress = value;
- UpdateProgress(_progress);
- AnimationUpdated?.Invoke(this, EventArgs.Empty);
- }
- }
+ /// Gets the current playback position.
+ public TimeSpan Progress => _progress;
/// Gets whether the animation has completed all repeats.
public bool IsComplete { get; private set; } = false;
@@ -53,7 +40,7 @@ public TimeSpan Progress
public event EventHandler? AnimationCompleted;
///
- /// Fires after each change to , notifying subscribers of
+ /// Fires after each call, notifying subscribers of
/// updated state (Progress, Duration, IsComplete).
///
public event EventHandler? AnimationUpdated;
@@ -68,6 +55,17 @@ public void SetAnimation(Skottie.Animation? newAnimation)
Reset();
}
+ ///
+ /// Seeks the animation to the specified position and raises .
+ /// Completion and repeat logic is applied as part of the seek.
+ ///
+ public void Seek(TimeSpan position)
+ {
+ _progress = position;
+ UpdateProgress(_progress);
+ AnimationUpdated?.Invoke(this, EventArgs.Empty);
+ }
+
///
/// Advances the animation by the given time delta, applying AnimationSpeed and Repeat.
/// Call this on each frame tick.
@@ -99,7 +97,7 @@ public void Update(TimeSpan deltaTime)
if (newProgress < TimeSpan.Zero)
newProgress = TimeSpan.Zero;
- Progress = newProgress;
+ Seek(newProgress);
}
/// Renders the current animation frame to the given canvas within the specified rectangle.
@@ -166,7 +164,7 @@ private void UpdateProgress(TimeSpan progress)
if (repeat.IsRestartRepeating)
{
- Progress = AnimationSpeed >= 0 ? TimeSpan.Zero : Duration;
+ Seek(AnimationSpeed >= 0 ? TimeSpan.Zero : Duration);
}
else if (repeat.IsReverseRepeating)
isInForwardPhase = !isInForwardPhase;
@@ -190,7 +188,7 @@ private void Reset()
repeatsCompleted = 0;
Duration = animation?.Duration ?? TimeSpan.Zero;
- Progress = AnimationSpeed < 0 ? Duration : TimeSpan.Zero;
+ Seek(AnimationSpeed < 0 ? Duration : TimeSpan.Zero);
}
finally
{
From a16bf72dc7da68d11b9c7ff1d32432bb34ce9a5a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Feb 2026 22:35:03 +0000
Subject: [PATCH 09/31] Remove isResetting guard field; fix AnimationCompleted
transition; add SKLottiePlayerTest
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
---
.../Lottie/SKLottiePlayer.cs | 29 +-
.../Lottie/SKLottiePlayerTest.cs | 348 ++++++++++++++++++
.../SkiaSharp.Extended.Tests.csproj | 1 +
3 files changed, 360 insertions(+), 18 deletions(-)
create mode 100644 tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs
diff --git a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
index db10840fcd..65e3ebd10d 100644
--- a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
+++ b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
@@ -11,7 +11,6 @@ public class SKLottiePlayer
private Skottie.Animation? animation;
private bool isInForwardPhase = true;
private int repeatsCompleted = 0;
- private bool isResetting;
private TimeSpan _progress;
@@ -116,10 +115,6 @@ private void UpdateProgress(TimeSpan progress)
animation.SeekFrameTime(progress.TotalSeconds);
- // Skip completion/repeat logic during Reset to avoid spurious events
- if (isResetting)
- return;
-
var repeat = Repeat;
var duration = Duration;
@@ -170,29 +165,27 @@ private void UpdateProgress(TimeSpan progress)
isInForwardPhase = !isInForwardPhase;
}
+ var prevIsComplete = IsComplete;
IsComplete =
isFinishedRun &&
repeatsCompleted >= totalRepeatCount;
- if (IsComplete)
+ if (IsComplete && !prevIsComplete)
AnimationCompleted?.Invoke(this, EventArgs.Empty);
}
}
private void Reset()
{
- isResetting = true;
- try
- {
- isInForwardPhase = true;
- repeatsCompleted = 0;
+ isInForwardPhase = true;
+ repeatsCompleted = 0;
+ IsComplete = false;
- Duration = animation?.Duration ?? TimeSpan.Zero;
- Seek(AnimationSpeed < 0 ? Duration : TimeSpan.Zero);
- }
- finally
- {
- isResetting = false;
- }
+ Duration = animation?.Duration ?? TimeSpan.Zero;
+
+ // Directly set the initial position without triggering completion logic.
+ _progress = AnimationSpeed < 0 ? Duration : TimeSpan.Zero;
+ animation?.SeekFrameTime(_progress.TotalSeconds);
+ AnimationUpdated?.Invoke(this, EventArgs.Empty);
}
}
diff --git a/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs b/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs
new file mode 100644
index 0000000000..d7fe169da5
--- /dev/null
+++ b/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs
@@ -0,0 +1,348 @@
+using System;
+using SkiaSharp.Skottie;
+using Xunit;
+
+namespace SkiaSharp.Extended.Tests;
+
+public class SKLottiePlayerTest
+{
+ // Minimal valid Lottie JSON: 60 frames at 60 fps → 1-second duration.
+ private static readonly string MinimalLottieJson =
+ """{"v":"5.7.4","fr":60,"ip":0,"op":60,"w":100,"h":100,"nm":"test","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"layer","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[],"ip":0,"op":60,"st":0,"bm":0}]}""";
+
+ private static Animation CreateAnimation() =>
+ Animation.Parse(MinimalLottieJson)
+ ?? throw new InvalidOperationException("Failed to parse test animation.");
+
+ // ── Initial state ────────────────────────────────────────────────────────
+
+ [Fact]
+ public void InitialState_IsEmpty()
+ {
+ var player = new SKLottiePlayer();
+
+ Assert.Equal(TimeSpan.Zero, player.Progress);
+ Assert.Equal(TimeSpan.Zero, player.Duration);
+ Assert.False(player.IsComplete);
+ Assert.False(player.HasAnimation);
+ }
+
+ // ── SetAnimation ─────────────────────────────────────────────────────────
+
+ [Fact]
+ public void SetAnimation_Null_ClearsState()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+ player.SetAnimation(null);
+
+ Assert.Equal(TimeSpan.Zero, player.Duration);
+ Assert.Equal(TimeSpan.Zero, player.Progress);
+ Assert.False(player.HasAnimation);
+ }
+
+ [Fact]
+ public void SetAnimation_SetsHasAnimation()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+
+ Assert.True(player.HasAnimation);
+ }
+
+ [Fact]
+ public void SetAnimation_SetsDuration()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+
+ Assert.Equal(TimeSpan.FromSeconds(1), player.Duration);
+ }
+
+ [Fact]
+ public void SetAnimation_ResetsProgress()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+ player.Seek(TimeSpan.FromSeconds(0.5));
+
+ player.SetAnimation(anim);
+
+ Assert.Equal(TimeSpan.Zero, player.Progress);
+ Assert.False(player.IsComplete);
+ }
+
+ [Fact]
+ public void SetAnimation_RaisesAnimationUpdated()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ var raised = 0;
+ player.AnimationUpdated += (_, _) => raised++;
+
+ player.SetAnimation(anim);
+
+ Assert.Equal(1, raised);
+ }
+
+ // ── Seek ─────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Seek_UpdatesProgress()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+
+ player.Seek(TimeSpan.FromSeconds(0.5));
+
+ Assert.Equal(TimeSpan.FromSeconds(0.5), player.Progress);
+ }
+
+ [Fact]
+ public void Seek_RaisesAnimationUpdated()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+ var raised = 0;
+ player.AnimationUpdated += (_, _) => raised++;
+
+ player.Seek(TimeSpan.FromSeconds(0.5));
+
+ Assert.Equal(1, raised);
+ }
+
+ // ── Update / playback ────────────────────────────────────────────────────
+
+ [Fact]
+ public void Update_AdvancesProgress()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+
+ player.Update(TimeSpan.FromSeconds(0.5));
+
+ Assert.Equal(TimeSpan.FromSeconds(0.5), player.Progress);
+ Assert.False(player.IsComplete);
+ }
+
+ [Fact]
+ public void Update_DoesNothing_WhenNoAnimation()
+ {
+ var player = new SKLottiePlayer();
+ player.Update(TimeSpan.FromSeconds(1));
+
+ Assert.Equal(TimeSpan.Zero, player.Progress);
+ }
+
+ [Fact]
+ public void Update_CapsProgressAtDuration_WithRepeatNever()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+ player.Repeat = SKLottieRepeat.Never;
+
+ player.Update(TimeSpan.FromSeconds(10));
+
+ Assert.Equal(player.Duration, player.Progress);
+ }
+
+ // ── Repeat.Never ─────────────────────────────────────────────────────────
+
+ [Fact]
+ public void RepeatNever_CompletesAfterOnePlay()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+ player.Repeat = SKLottieRepeat.Never;
+ var completed = 0;
+ player.AnimationCompleted += (_, _) => completed++;
+
+ player.Update(TimeSpan.FromSeconds(10));
+
+ Assert.True(player.IsComplete);
+ Assert.Equal(1, completed);
+ }
+
+ [Fact]
+ public void RepeatNever_DoesNotFireCompletedTwice()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+ player.Repeat = SKLottieRepeat.Never;
+ var completed = 0;
+ player.AnimationCompleted += (_, _) => completed++;
+
+ player.Update(TimeSpan.FromSeconds(10));
+ player.Update(TimeSpan.FromSeconds(10));
+
+ Assert.Equal(1, completed);
+ }
+
+ // ── Repeat.Restart ───────────────────────────────────────────────────────
+
+ [Fact]
+ public void RepeatRestart_InfiniteLoop_NeverCompletes()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+ player.Repeat = SKLottieRepeat.Restart();
+ var completed = 0;
+ player.AnimationCompleted += (_, _) => completed++;
+
+ // Advance well past end multiple times
+ for (var i = 0; i < 5; i++)
+ player.Update(TimeSpan.FromSeconds(10));
+
+ Assert.False(player.IsComplete);
+ Assert.Equal(0, completed);
+ }
+
+ [Fact]
+ public void RepeatRestart_FiniteCount_CompletesAfterNPlus1Plays()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+ player.Repeat = SKLottieRepeat.Restart(count: 2); // 3 plays total
+ var completed = 0;
+ player.AnimationCompleted += (_, _) => completed++;
+
+ // Play 1 — not done
+ player.Update(TimeSpan.FromSeconds(10));
+ Assert.False(player.IsComplete);
+
+ // Play 2 — not done
+ player.Update(TimeSpan.FromSeconds(10));
+ Assert.False(player.IsComplete);
+
+ // Play 3 — done
+ player.Update(TimeSpan.FromSeconds(10));
+ Assert.True(player.IsComplete);
+ Assert.Equal(1, completed);
+ }
+
+ [Fact]
+ public void RepeatRestart_ResetsProgressToZero()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+ player.Repeat = SKLottieRepeat.Restart(count: 1);
+
+ // Seek exactly to the end to trigger restart
+ player.Seek(player.Duration);
+
+ Assert.Equal(TimeSpan.Zero, player.Progress);
+ Assert.False(player.IsComplete);
+ }
+
+ // ── Repeat.Reverse (ping-pong) ───────────────────────────────────────────
+
+ [Fact]
+ public void RepeatReverse_InfiniteLoop_NeverCompletes()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+ player.Repeat = SKLottieRepeat.Reverse();
+ var completed = 0;
+ player.AnimationCompleted += (_, _) => completed++;
+
+ for (var i = 0; i < 10; i++)
+ player.Update(TimeSpan.FromSeconds(10));
+
+ Assert.False(player.IsComplete);
+ Assert.Equal(0, completed);
+ }
+
+ [Fact]
+ public void RepeatReverse_BouncesDirectionAtEnd()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+ player.Repeat = SKLottieRepeat.Reverse();
+
+ // Play forward to end
+ player.Update(player.Duration + TimeSpan.FromTicks(1));
+ var progressAtBounce = player.Progress;
+
+ // After bounce, next update should move backward
+ player.Update(TimeSpan.FromSeconds(0.3));
+
+ Assert.True(player.Progress < progressAtBounce,
+ $"Expected progress to decrease after bounce, but got {player.Progress} >= {progressAtBounce}");
+ }
+
+ // ── AnimationSpeed ────────────────────────────────────────────────────────
+
+ [Fact]
+ public void NegativeSpeed_StartsAtDuration()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.AnimationSpeed = -1.0;
+ player.SetAnimation(anim);
+
+ Assert.Equal(player.Duration, player.Progress);
+ }
+
+ [Fact]
+ public void NegativeSpeed_MovesProgressBackward()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.AnimationSpeed = -1.0;
+ player.Repeat = SKLottieRepeat.Never;
+ player.SetAnimation(anim);
+
+ player.Update(TimeSpan.FromSeconds(0.5));
+
+ Assert.Equal(player.Duration - TimeSpan.FromSeconds(0.5), player.Progress);
+ Assert.False(player.IsComplete);
+ }
+
+ [Fact]
+ public void NegativeSpeed_CompletesAtZero()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.AnimationSpeed = -1.0;
+ player.Repeat = SKLottieRepeat.Never;
+ player.SetAnimation(anim);
+ var completed = 0;
+ player.AnimationCompleted += (_, _) => completed++;
+
+ player.Update(TimeSpan.FromSeconds(10));
+
+ Assert.True(player.IsComplete);
+ Assert.Equal(1, completed);
+ }
+
+ // ── AnimationUpdated event ────────────────────────────────────────────────
+
+ [Fact]
+ public void AnimationUpdated_FiredOnUpdate()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+ var raised = 0;
+ player.AnimationUpdated += (_, _) => raised++;
+
+ player.Update(TimeSpan.FromSeconds(0.1));
+
+ Assert.True(raised >= 1);
+ }
+}
diff --git a/tests/SkiaSharp.Extended.Tests/SkiaSharp.Extended.Tests.csproj b/tests/SkiaSharp.Extended.Tests/SkiaSharp.Extended.Tests.csproj
index 76c95dbdf4..4c6613d323 100644
--- a/tests/SkiaSharp.Extended.Tests/SkiaSharp.Extended.Tests.csproj
+++ b/tests/SkiaSharp.Extended.Tests/SkiaSharp.Extended.Tests.csproj
@@ -9,6 +9,7 @@
+
From 8f56013664dc82ddec2caab992e8c610450f32d7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Feb 2026 22:42:07 +0000
Subject: [PATCH 10/31] Simplify Progress: auto-property with private set, no
backing field
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
---
source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
index 65e3ebd10d..1d75233387 100644
--- a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
+++ b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
@@ -12,13 +12,11 @@ public class SKLottiePlayer
private bool isInForwardPhase = true;
private int repeatsCompleted = 0;
- private TimeSpan _progress;
-
/// Gets the total duration of the loaded animation.
public TimeSpan Duration { get; private set; } = TimeSpan.Zero;
/// Gets the current playback position.
- public TimeSpan Progress => _progress;
+ public TimeSpan Progress { get; private set; }
/// Gets whether the animation has completed all repeats.
public bool IsComplete { get; private set; } = false;
@@ -60,8 +58,8 @@ public void SetAnimation(Skottie.Animation? newAnimation)
///
public void Seek(TimeSpan position)
{
- _progress = position;
- UpdateProgress(_progress);
+ Progress = position;
+ UpdateProgress(Progress);
AnimationUpdated?.Invoke(this, EventArgs.Empty);
}
@@ -184,8 +182,8 @@ private void Reset()
Duration = animation?.Duration ?? TimeSpan.Zero;
// Directly set the initial position without triggering completion logic.
- _progress = AnimationSpeed < 0 ? Duration : TimeSpan.Zero;
- animation?.SeekFrameTime(_progress.TotalSeconds);
+ Progress = AnimationSpeed < 0 ? Duration : TimeSpan.Zero;
+ animation?.SeekFrameTime(Progress.TotalSeconds);
AnimationUpdated?.Invoke(this, EventArgs.Empty);
}
}
From cf45b350dbeee9d0386354204c5d777136fd4823 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 1 Mar 2026 21:41:59 +0000
Subject: [PATCH 11/31] feat: XML docs, core playback tests, Blazor animated
canvas, slim MAUI tests
- Add GenerateDocumentationFile=true to core and MAUI csproj files
- Enhance XML docs on SKLottiePlayer (class summary, remarks, Render params,
SetAnimation/Seek/Update remarks) and SKLottieRepeat (already complete)
- Add 9 new unit tests to SKLottiePlayerTest covering negative deltas,
speed variants (double/half/zero/dynamic/mid-playback-negative),
finite Reverse(count:0) cycle completion, and negative-speed Restart loop
- Slim SKLottieViewTest from ~25 tests to 4 MAUI-specific ones (loading,
reset, default speed, source cancel)
- Create SkiaSharp.Extended.UI.Blazor library (net8/9/10) with
SKAnimatedCanvasView: PeriodicTimer 60fps loop, IsAnimationEnabled,
OnPaintSurface EventCallback, AdditionalAttributes, virtual UpdateAsync
- Create SkiaSharp.Extended.UI.Blazor.Tests with bunit smoke tests
- Add SKLottiePlayer cross-platform section to docs/docs/lottie.md
- Add Lottie Player entry to SkiaSharp.Extended section in toc.yml
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
docs/docs/lottie.md | 45 ++
docs/docs/toc.yml | 2 +
.../Controls/SKAnimatedCanvasView.razor | 4 +
.../Controls/SKAnimatedCanvasView.razor.cs | 132 +++++
.../SkiaSharp.Extended.UI.Blazor.csproj | 27 +
.../SkiaSharp.Extended.UI.Maui.csproj | 1 +
.../Lottie/SKLottiePlayer.cs | 61 +-
.../SkiaSharp.Extended.csproj | 1 +
.../Lottie/SKLottiePlayerTest.cs | 147 +++++
.../Controls/SKAnimatedCanvasViewTest.cs | 66 +++
.../SkiaSharp.Extended.UI.Blazor.Tests.csproj | 26 +
.../Controls/Lottie/SKLottieViewTest.cs | 547 +-----------------
12 files changed, 511 insertions(+), 548 deletions(-)
create mode 100644 source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor
create mode 100644 source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor.cs
create mode 100644 source/SkiaSharp.Extended.UI.Blazor/SkiaSharp.Extended.UI.Blazor.csproj
create mode 100644 tests/SkiaSharp.Extended.UI.Blazor.Tests/Controls/SKAnimatedCanvasViewTest.cs
create mode 100644 tests/SkiaSharp.Extended.UI.Blazor.Tests/SkiaSharp.Extended.UI.Blazor.Tests.csproj
diff --git a/docs/docs/lottie.md b/docs/docs/lottie.md
index aa4c4a2072..b1fb4590ae 100644
--- a/docs/docs/lottie.md
+++ b/docs/docs/lottie.md
@@ -140,6 +140,51 @@ You can customize the rendering surface by overriding the control template:
The `PART_DrawingSurface` name is required—it can be either `SKCanvasView` (software) or `SKGLView` (GPU).
+## Cross-Platform Usage with SKLottiePlayer
+
+The `SKLottiePlayer` class in `SkiaSharp.Extended` provides all the playback logic without any MAUI or Blazor dependency, so you can integrate Lottie animations into any .NET host — including Blazor WebAssembly, console renderers, or custom frameworks.
+
+### SKLottieRepeat
+
+`SKLottieRepeat` is a lightweight value type (struct) that describes how an animation repeats:
+
+| Factory | Behaviour |
+| :------ | :-------- |
+| `SKLottieRepeat.Never` | Play once, then stop (default). |
+| `SKLottieRepeat.Restart(count)` | Restart from the beginning. `count = -1` for infinite. |
+| `SKLottieRepeat.Reverse(count)` | Ping-pong (play forward, then backward). `count = -1` for infinite. |
+
+### Using SKLottiePlayer directly
+
+```csharp
+using SkiaSharp;
+using SkiaSharp.Extended;
+using SkiaSharp.Skottie;
+
+// 1. Load the animation from a stream
+using var stream = File.OpenRead("animation.json");
+var animation = Animation.Create(stream);
+
+// 2. Create a player and configure it
+var player = new SKLottiePlayer
+{
+ AnimationSpeed = 1.5,
+ Repeat = SKLottieRepeat.Restart(count: -1) // loop forever
+};
+
+player.AnimationCompleted += (_, _) => Console.WriteLine("Done!");
+player.SetAnimation(animation);
+
+// 3. In your frame/paint loop:
+// a) advance the playback position
+player.Update(deltaTime);
+
+// b) draw the current frame
+player.Render(canvas, SKRect.Create(0, 0, 400, 400));
+```
+
+> **Blazor note:** The Blazor sample at `samples/SkiaSharpDemo.Blazor/` demonstrates `SKLottiePlayer` inside a `PeriodicTimer`-based animation loop. The `SKAnimatedCanvasView` component in `SkiaSharp.Extended.UI.Blazor` wraps this pattern as a reusable Blazor component.
+
## Learn More
- [Lottie by Airbnb](https://airbnb.design/lottie/) — Official project page
diff --git a/docs/docs/toc.yml b/docs/docs/toc.yml
index 6fb3c47b97..d46d719f58 100644
--- a/docs/docs/toc.yml
+++ b/docs/docs/toc.yml
@@ -8,6 +8,8 @@ items:
href: blurhash.md
- name: Geometry Helpers
href: geometry.md
+- name: Lottie Player
+ href: lottie.md
- name: Path Interpolation
href: path-interpolation.md
- name: Pixel Comparer
diff --git a/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor b/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor
new file mode 100644
index 0000000000..041f52a599
--- /dev/null
+++ b/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor
@@ -0,0 +1,4 @@
+@namespace SkiaSharp.Extended.UI.Blazor.Controls
+
+
diff --git a/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor.cs b/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor.cs
new file mode 100644
index 0000000000..9e96bbc7cf
--- /dev/null
+++ b/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor.cs
@@ -0,0 +1,132 @@
+using Microsoft.AspNetCore.Components;
+using SkiaSharp.Views.Blazor;
+
+namespace SkiaSharp.Extended.UI.Blazor.Controls;
+
+///
+/// A Blazor component that wraps and drives a
+/// frame-update loop using a , mirroring the
+/// MAUI SKAnimatedSurfaceView pattern for Blazor applications.
+///
+///
+///
+/// Subclass this component and override to update
+/// your animation state on each frame, and subscribe to
+/// to render your content.
+///
+///
+/// The animation loop runs at approximately 60 fps while
+/// is . Setting it to
+/// stops the loop; setting it back to
+/// restarts it.
+///
+///
+public partial class SKAnimatedCanvasView : ComponentBase, IAsyncDisposable
+{
+ private bool _isAnimationEnabled;
+ private CancellationTokenSource? _cts;
+ private Task? _loopTask;
+
+ ///
+ /// Gets or sets whether the animation loop is running.
+ /// Defaults to .
+ ///
+ [Parameter]
+ public bool IsAnimationEnabled { get; set; } = true;
+
+ ///
+ /// Callback invoked each time the canvas needs to be redrawn.
+ /// Subscribe here to render your content onto the .
+ ///
+ [Parameter]
+ public EventCallback OnPaintSurface { get; set; }
+
+ ///
+ /// Additional HTML attributes to be applied to the underlying
+ /// element (e.g., style, class).
+ ///
+ [Parameter(CaptureUnmatchedValues = true)]
+ public IDictionary? AdditionalAttributes { get; set; }
+
+ ///
+ protected override void OnAfterRender(bool firstRender)
+ {
+ if (firstRender && IsAnimationEnabled)
+ StartLoop();
+ }
+
+ ///
+ protected override void OnParametersSet()
+ {
+ if (_isAnimationEnabled == IsAnimationEnabled)
+ return;
+
+ _isAnimationEnabled = IsAnimationEnabled;
+ if (_isAnimationEnabled)
+ StartLoop();
+ else
+ StopLoop();
+ }
+
+ ///
+ /// Called once per frame before the canvas is invalidated. Override this
+ /// method in a subclass to update animation state.
+ ///
+ /// Time elapsed since the previous frame.
+ /// A representing the asynchronous update work.
+ protected virtual Task UpdateAsync(TimeSpan deltaTime) => Task.CompletedTask;
+
+ private void HandlePaintSurface(SKPaintSurfaceEventArgs e)
+ {
+ if (OnPaintSurface.HasDelegate)
+ _ = OnPaintSurface.InvokeAsync(e);
+ }
+
+ private void StartLoop()
+ {
+ StopLoop();
+ _cts = new CancellationTokenSource();
+ _loopTask = RunLoopAsync(_cts.Token);
+ }
+
+ private void StopLoop()
+ {
+ _cts?.Cancel();
+ _cts?.Dispose();
+ _cts = null;
+ }
+
+ private async Task RunLoopAsync(CancellationToken ct)
+ {
+ using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1000.0 / 60));
+ var lastTick = DateTime.UtcNow;
+
+ try
+ {
+ while (await timer.WaitForNextTickAsync(ct))
+ {
+ var now = DateTime.UtcNow;
+ var delta = now - lastTick;
+ lastTick = now;
+
+ await UpdateAsync(delta);
+ await InvokeAsync(StateHasChanged);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Normal shutdown — ignore.
+ }
+ }
+
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ StopLoop();
+ if (_loopTask is not null)
+ {
+ try { await _loopTask; }
+ catch (OperationCanceledException) { }
+ }
+ }
+}
diff --git a/source/SkiaSharp.Extended.UI.Blazor/SkiaSharp.Extended.UI.Blazor.csproj b/source/SkiaSharp.Extended.UI.Blazor/SkiaSharp.Extended.UI.Blazor.csproj
new file mode 100644
index 0000000000..f7d061d4ad
--- /dev/null
+++ b/source/SkiaSharp.Extended.UI.Blazor/SkiaSharp.Extended.UI.Blazor.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net8.0;net9.0;net10.0
+ SkiaSharp.Extended.UI.Blazor
+ SkiaSharp.Extended.UI.Blazor
+ enable
+ enable
+ true
+
+
+
+ SkiaSharp.Extended.UI.Blazor
+ Additional SkiaSharp controls for Blazor
+ This package adds additional SkiaSharp controls and utilities for Blazor applications.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/source/SkiaSharp.Extended.UI.Maui/SkiaSharp.Extended.UI.Maui.csproj b/source/SkiaSharp.Extended.UI.Maui/SkiaSharp.Extended.UI.Maui.csproj
index 6c8c14cc89..cdc7157d00 100644
--- a/source/SkiaSharp.Extended.UI.Maui/SkiaSharp.Extended.UI.Maui.csproj
+++ b/source/SkiaSharp.Extended.UI.Maui/SkiaSharp.Extended.UI.Maui.csproj
@@ -10,6 +10,7 @@
SkiaSharp.Extended.UISkiaSharp.Extended.UIfalse
+ true
diff --git a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
index 1d75233387..a35dd86510 100644
--- a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
+++ b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
@@ -4,8 +4,23 @@ namespace SkiaSharp.Extended;
///
/// A platform-agnostic Skottie (Lottie) animation player that manages playback state
-/// and rendering. Can be shared between MAUI, Blazor, and other platforms.
+/// and rendering. Can be used directly from any .NET host including .NET MAUI, Blazor,
+/// console apps, or custom renderers.
///
+///
+///
+/// Typical usage:
+///
+/// Create a player and set and .
+/// Call with a loaded .
+/// On each frame tick, call with the elapsed time.
+/// Call inside your paint/draw callback.
+///
+///
+///
+/// The player is not thread-safe; all calls should occur on the same thread (typically the UI thread).
+///
+///
public class SKLottiePlayer
{
private Skottie.Animation? animation;
@@ -43,9 +58,18 @@ public class SKLottiePlayer
public event EventHandler? AnimationUpdated;
///
- /// Sets the animation to play. Pass null to clear the current animation.
+ /// Sets the animation to play. Pass to clear the current animation.
/// Resets playback state (Progress, IsComplete, repeat counters).
///
+ ///
+ /// The to play, or to clear.
+ /// The player does not take ownership of the animation; the caller is responsible for disposing it.
+ ///
+ ///
+ /// Calling this method always resets to
+ /// (or when is negative) and clears
+ /// . It also raises .
+ ///
public void SetAnimation(Skottie.Animation? newAnimation)
{
animation = newAnimation;
@@ -56,6 +80,14 @@ public void SetAnimation(Skottie.Animation? newAnimation)
/// Seeks the animation to the specified position and raises .
/// Completion and repeat logic is applied as part of the seek.
///
+ /// The absolute playback position to seek to.
+ ///
+ /// Unlike , Seek sets an absolute position rather than advancing
+ /// by a delta. The position is clamped to [, ]
+ /// and repeat/completion state is evaluated immediately.
+ /// Setting to a boundary via Seek does not increment
+ /// the internal repeat counter; use for frame-by-frame playback.
+ ///
public void Seek(TimeSpan position)
{
Progress = position;
@@ -64,9 +96,26 @@ public void Seek(TimeSpan position)
}
///
- /// Advances the animation by the given time delta, applying AnimationSpeed and Repeat.
+ /// Advances the animation by the given time delta, applying and .
/// Call this on each frame tick.
///
+ ///
+ /// The time elapsed since the last call. A positive value advances forward; a negative value
+ /// moves the position backward (subject to clamping at the boundaries).
+ ///
+ ///
+ ///
+ /// The effective delta is scaled by before being applied:
+ /// a speed of 2.0 doubles the rate, 0.5 halves it, and -1.0 plays in reverse.
+ ///
+ ///
+ /// When is , the internal direction
+ /// is flipped automatically when the animation reaches a boundary, producing a ping-pong effect.
+ ///
+ ///
+ /// Has no effect when no animation is loaded ( is ).
+ ///
+ ///
public void Update(TimeSpan deltaTime)
{
if (animation is null)
@@ -98,6 +147,12 @@ public void Update(TimeSpan deltaTime)
}
/// Renders the current animation frame to the given canvas within the specified rectangle.
+ /// The to draw onto.
+ /// The destination rectangle within the canvas.
+ ///
+ /// Has no effect when no animation is loaded ( is ).
+ /// Call this inside your paint/draw callback after has been called for the current frame.
+ ///
public void Render(SKCanvas canvas, SKRect rect)
{
animation?.Render(canvas, rect);
diff --git a/source/SkiaSharp.Extended/SkiaSharp.Extended.csproj b/source/SkiaSharp.Extended/SkiaSharp.Extended.csproj
index 51e82f3367..b5e804645e 100644
--- a/source/SkiaSharp.Extended/SkiaSharp.Extended.csproj
+++ b/source/SkiaSharp.Extended/SkiaSharp.Extended.csproj
@@ -4,6 +4,7 @@
netstandard2.0;net9.0;net10.0SkiaSharp.ExtendedSkiaSharp.Extended
+ true
diff --git a/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs b/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs
index d7fe169da5..f7d61b8e0d 100644
--- a/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs
+++ b/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs
@@ -345,4 +345,151 @@ public void AnimationUpdated_FiredOnUpdate()
Assert.True(raised >= 1);
}
+
+ // ── Negative deltaTime ────────────────────────────────────────────────────
+
+ [Fact]
+ public void Update_NegativeDelta_ClampsToZero()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+
+ player.Update(TimeSpan.FromSeconds(-1));
+
+ Assert.Equal(TimeSpan.Zero, player.Progress);
+ Assert.False(player.IsComplete);
+ }
+
+ [Fact]
+ public void Update_NegativeDeltaAfterPositive_MovesBack()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+ player.Update(TimeSpan.FromSeconds(0.5));
+
+ player.Update(TimeSpan.FromSeconds(-0.3));
+
+ Assert.Equal(TimeSpan.FromSeconds(0.2), player.Progress);
+ Assert.False(player.IsComplete);
+ }
+
+ // ── AnimationSpeed variants ───────────────────────────────────────────────
+
+ [Fact]
+ public void AnimationSpeed_Double_AdvancesAtDoubleRate()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.AnimationSpeed = 2.0;
+ player.SetAnimation(anim);
+
+ player.Update(TimeSpan.FromSeconds(0.3));
+
+ Assert.Equal(TimeSpan.FromSeconds(0.6), player.Progress);
+ }
+
+ [Fact]
+ public void AnimationSpeed_Half_AdvancesAtHalfRate()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.AnimationSpeed = 0.5;
+ player.SetAnimation(anim);
+
+ player.Update(TimeSpan.FromSeconds(0.5));
+
+ Assert.Equal(TimeSpan.FromSeconds(0.25), player.Progress);
+ }
+
+ [Fact]
+ public void AnimationSpeed_Zero_DoesNotAdvance()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.AnimationSpeed = 0;
+ player.SetAnimation(anim);
+
+ player.Update(TimeSpan.FromSeconds(1));
+
+ Assert.Equal(TimeSpan.Zero, player.Progress);
+ Assert.False(player.IsComplete);
+ }
+
+ [Fact]
+ public void AnimationSpeed_CanBeChangedDynamically()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.AnimationSpeed = 1.0;
+ player.SetAnimation(anim);
+
+ player.Update(TimeSpan.FromSeconds(0.3));
+ player.AnimationSpeed = 2.0;
+ player.Update(TimeSpan.FromSeconds(0.2));
+
+ // 0.3 + (0.2 × 2.0) = 0.7
+ Assert.Equal(TimeSpan.FromSeconds(0.7), player.Progress);
+ }
+
+ [Fact]
+ public void AnimationSpeed_NegativeMidPlayback_MovesBackward()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.AnimationSpeed = 1.0;
+ player.Repeat = SKLottieRepeat.Never;
+ player.SetAnimation(anim);
+
+ player.Update(TimeSpan.FromSeconds(0.5));
+ var progressBefore = player.Progress;
+
+ player.AnimationSpeed = -1.0;
+ player.Update(TimeSpan.FromSeconds(0.2));
+
+ Assert.True(player.Progress < progressBefore);
+ }
+
+ // ── Repeat.Reverse finite (count 0) ──────────────────────────────────────
+
+ [Fact]
+ public void RepeatReverse_FiniteCount_ZeroCount_CompletesAfterOneCycle()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+ player.Repeat = SKLottieRepeat.Reverse(count: 0);
+ var completed = 0;
+ player.AnimationCompleted += (_, _) => completed++;
+
+ // Forward to end — triggers direction flip, not completion
+ player.Update(TimeSpan.FromSeconds(10));
+ Assert.False(player.IsComplete);
+
+ // Backward to start — completes
+ player.Update(TimeSpan.FromSeconds(10));
+ Assert.True(player.IsComplete);
+ Assert.Equal(1, completed);
+ }
+
+ // ── Negative speed + Restart infinite ────────────────────────────────────
+
+ [Fact]
+ public void NegativeSpeed_RepeatRestart_Infinite_NeverCompletes()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.AnimationSpeed = -1.0;
+ player.Repeat = SKLottieRepeat.Restart();
+ player.SetAnimation(anim);
+ var completed = 0;
+ player.AnimationCompleted += (_, _) => completed++;
+
+ for (var i = 0; i < 5; i++)
+ player.Update(TimeSpan.FromSeconds(10));
+
+ Assert.False(player.IsComplete);
+ Assert.Equal(0, completed);
+ }
}
diff --git a/tests/SkiaSharp.Extended.UI.Blazor.Tests/Controls/SKAnimatedCanvasViewTest.cs b/tests/SkiaSharp.Extended.UI.Blazor.Tests/Controls/SKAnimatedCanvasViewTest.cs
new file mode 100644
index 0000000000..a230a59362
--- /dev/null
+++ b/tests/SkiaSharp.Extended.UI.Blazor.Tests/Controls/SKAnimatedCanvasViewTest.cs
@@ -0,0 +1,66 @@
+using Bunit;
+using Microsoft.Extensions.DependencyInjection;
+using SkiaSharp.Extended.UI.Blazor.Controls;
+
+namespace SkiaSharp.Extended.UI.Blazor.Tests.Controls;
+
+public class SKAnimatedCanvasViewTest
+{
+ ///
+ /// Verifies that has IsAnimationEnabled
+ /// defaulting to and that it exposes the
+ /// OnPaintSurface callback parameter.
+ ///
+ [Fact]
+ public void DefaultParameters_IsAnimationEnabled_IsTrue()
+ {
+ // Arrange – use a test-double subclass so we never need the real JS canvas.
+ var component = new SKAnimatedCanvasViewAccessor();
+
+ // Assert defaults are sensible before rendering.
+ Assert.True(component.IsAnimationEnabled);
+ Assert.Equal(default, component.OnPaintSurface);
+ Assert.Null(component.AdditionalAttributes);
+ }
+
+ ///
+ /// Verifies that toggling IsAnimationEnabled from true → false → true
+ /// does not throw and leaves the flag in the expected state.
+ ///
+ [Fact]
+ public void ToggleIsAnimationEnabled_DoesNotThrow()
+ {
+ var component = new SKAnimatedCanvasViewAccessor();
+
+ // Start enabled (default)
+ Assert.True(component.IsAnimationEnabled);
+
+ // Disable
+ var ex1 = Record.Exception(() => component.IsAnimationEnabled = false);
+ Assert.Null(ex1);
+ Assert.False(component.IsAnimationEnabled);
+
+ // Re-enable
+ var ex2 = Record.Exception(() => component.IsAnimationEnabled = true);
+ Assert.Null(ex2);
+ Assert.True(component.IsAnimationEnabled);
+ }
+
+ ///
+ /// A minimal subclass that exposes protected members for unit-testing
+ /// without rendering into a real Blazor/WASM environment.
+ ///
+ private sealed class SKAnimatedCanvasViewAccessor : SKAnimatedCanvasView
+ {
+ // Expose the loop-start / stop calls as no-ops by overriding UpdateAsync
+ // so the base class can be exercised without a running event loop.
+ protected override Task UpdateAsync(TimeSpan deltaTime) => Task.CompletedTask;
+
+ // Shadow the auto-property so the setter logic can be exercised.
+ public new bool IsAnimationEnabled
+ {
+ get => base.IsAnimationEnabled;
+ set => base.IsAnimationEnabled = value;
+ }
+ }
+}
diff --git a/tests/SkiaSharp.Extended.UI.Blazor.Tests/SkiaSharp.Extended.UI.Blazor.Tests.csproj b/tests/SkiaSharp.Extended.UI.Blazor.Tests/SkiaSharp.Extended.UI.Blazor.Tests.csproj
new file mode 100644
index 0000000000..e904c75db5
--- /dev/null
+++ b/tests/SkiaSharp.Extended.UI.Blazor.Tests/SkiaSharp.Extended.UI.Blazor.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/SkiaSharp.Extended.UI.Maui.Tests/Controls/Lottie/SKLottieViewTest.cs b/tests/SkiaSharp.Extended.UI.Maui.Tests/Controls/Lottie/SKLottieViewTest.cs
index 5e7b8bc343..3255d70daf 100644
--- a/tests/SkiaSharp.Extended.UI.Maui.Tests/Controls/Lottie/SKLottieViewTest.cs
+++ b/tests/SkiaSharp.Extended.UI.Maui.Tests/Controls/Lottie/SKLottieViewTest.cs
@@ -40,183 +40,6 @@ public async Task EnsureNewAnimationResetsProgress()
Assert.False(lottie.IsComplete);
}
- [Fact]
- public async Task UpdatesMoveProgress()
- {
- // create
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source };
- var animationCompleted = 0;
- lottie.AnimationCompleted += (s, e) => animationCompleted++;
- await lottie.LoadedTask;
-
- // update
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
-
- // test
- Assert.Equal(TimeSpan.FromSeconds(1), lottie.Progress);
- Assert.False(lottie.IsComplete);
- Assert.Equal(0, animationCompleted);
- }
-
- [Fact]
- public async Task MultipleUpdatesMoveProgressUntilDurationMax()
- {
- // create
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source };
- var animationCompleted = 0;
- lottie.AnimationCompleted += (s, e) => animationCompleted++;
- await lottie.LoadedTask;
-
- // update & test
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
- Assert.Equal(TimeSpan.FromSeconds(1), lottie.Progress);
- Assert.False(lottie.IsComplete);
- Assert.Equal(0, animationCompleted);
-
- // update & test
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
- Assert.Equal(TimeSpan.FromSeconds(2), lottie.Progress);
- Assert.False(lottie.IsComplete);
- Assert.Equal(0, animationCompleted);
-
- // update & test
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
- Assert.Equal(TimeSpan.FromSeconds(2.3666665), lottie.Progress);
- Assert.True(lottie.IsComplete);
- Assert.Equal(1, animationCompleted);
- }
-
- [Fact]
- public async Task UpdatesLargerThanDurationHasMax()
- {
- // create
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source };
- var animationCompleted = 0;
- lottie.AnimationCompleted += (s, e) => animationCompleted++;
- await lottie.LoadedTask;
-
- // update
- lottie.CallUpdate(TimeSpan.FromSeconds(5));
-
- // test
- Assert.Equal(TimeSpan.FromSeconds(2.3666665), lottie.Progress);
- Assert.True(lottie.IsComplete);
- Assert.Equal(1, animationCompleted);
- }
-
- [Fact]
- public async Task NegativeUpdatesDoesNothing()
- {
- // create
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source };
- var animationCompleted = 0;
- lottie.AnimationCompleted += (s, e) => animationCompleted++;
- await lottie.LoadedTask;
-
- // update
- lottie.CallUpdate(TimeSpan.FromSeconds(-1));
-
- // test
- Assert.Equal(TimeSpan.Zero, lottie.Progress);
- Assert.False(lottie.IsComplete);
- Assert.Equal(0, animationCompleted);
- }
-
- [Fact]
- public async Task NegativeUpdatesAfterPositiveGoesBack()
- {
- // create
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source };
- var animationCompleted = 0;
- lottie.AnimationCompleted += (s, e) => animationCompleted++;
- await lottie.LoadedTask;
-
- // update
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
- lottie.CallUpdate(TimeSpan.FromSeconds(-1));
-
- // test
- Assert.Equal(TimeSpan.FromSeconds(1), lottie.Progress);
- Assert.False(lottie.IsComplete);
- Assert.Equal(0, animationCompleted);
- }
-
- [Theory]
- [InlineData(SKLottieRepeatMode.Restart, 1, 1)]
- [InlineData(SKLottieRepeatMode.Restart, 2, 2)]
- [InlineData(SKLottieRepeatMode.Restart, 3, 0)]
- [InlineData(SKLottieRepeatMode.Restart, 4, 1)]
- [InlineData(SKLottieRepeatMode.Restart, 5, 2)]
- [InlineData(SKLottieRepeatMode.Restart, 6, 0)]
- [InlineData(SKLottieRepeatMode.Restart, 7, 1)]
- [InlineData(SKLottieRepeatMode.Reverse, 1, 1)]
- [InlineData(SKLottieRepeatMode.Reverse, 2, 2)]
- [InlineData(SKLottieRepeatMode.Reverse, 3, 2.3666665)]
- [InlineData(SKLottieRepeatMode.Reverse, 4, 1.3666665)]
- [InlineData(SKLottieRepeatMode.Reverse, 5, 0.3666665)]
- [InlineData(SKLottieRepeatMode.Reverse, 6, 0)]
- [InlineData(SKLottieRepeatMode.Reverse, 7, 1)]
- public async Task ReachingTheEndAndThenMoreWithRepeat(SKLottieRepeatMode repeatMode, int steps, double progress)
- {
- // create
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source, RepeatMode = repeatMode, RepeatCount = -1 };
- var animationCompleted = 0;
- lottie.AnimationCompleted += (s, e) => animationCompleted++;
- await lottie.LoadedTask;
-
- // update
- for (var i = 0; i < steps; i++)
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
-
- // test
- Assert.Equal(TimeSpan.FromSeconds(progress), lottie.Progress);
- Assert.False(lottie.IsComplete);
- Assert.Equal(0, animationCompleted);
- }
-
- [Theory]
- [InlineData(SKLottieRepeatMode.Restart, 1, 1, false)]
- [InlineData(SKLottieRepeatMode.Restart, 2, 2, false)]
- [InlineData(SKLottieRepeatMode.Restart, 3, 2.3666665, true)]
- [InlineData(SKLottieRepeatMode.Restart, 4, 2.3666665, true)]
- [InlineData(SKLottieRepeatMode.Restart, 5, 2.3666665, true)]
- [InlineData(SKLottieRepeatMode.Reverse, 1, 1, false)]
- [InlineData(SKLottieRepeatMode.Reverse, 2, 2, false)]
- [InlineData(SKLottieRepeatMode.Reverse, 3, 2.3666665, false)]
- [InlineData(SKLottieRepeatMode.Reverse, 4, 1.3666665, false)]
- [InlineData(SKLottieRepeatMode.Reverse, 5, 0.3666665, false)]
- [InlineData(SKLottieRepeatMode.Reverse, 6, 0, true)]
- [InlineData(SKLottieRepeatMode.Reverse, 7, 0, true)]
- [InlineData(SKLottieRepeatMode.Reverse, 8, 0, true)]
- public async Task ReachingTheEndAndThenMoreWithRepeatModeButZeroCount(SKLottieRepeatMode repeatMode, int steps, double progress, bool isComplete)
- {
- // create
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source, RepeatMode = repeatMode, RepeatCount = 0 };
- var animationCompleted = 0;
- lottie.AnimationCompleted += (s, e) => animationCompleted++;
- await lottie.LoadedTask;
-
- // update
- for (var i = 0; i < steps; i++)
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
-
- // test
- Assert.Equal(TimeSpan.FromSeconds(progress), lottie.Progress);
- Assert.Equal(isComplete, lottie.IsComplete);
- if (isComplete)
- Assert.Equal(1, animationCompleted);
- else
- Assert.Equal(0, animationCompleted);
- }
-
[Fact]
public async Task DefaultAnimationSpeedIsOne()
{
@@ -229,373 +52,6 @@ public async Task DefaultAnimationSpeedIsOne()
Assert.Equal(1.0, lottie.AnimationSpeed);
}
- [Fact]
- public async Task AnimationSpeedDoubleMakesItTwiceAsFast()
- {
- // create
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source, AnimationSpeed = 2.0 };
- await lottie.LoadedTask;
-
- // update with 1 second, but should progress 2 seconds
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
-
- // test
- Assert.Equal(TimeSpan.FromSeconds(2), lottie.Progress);
- Assert.False(lottie.IsComplete);
- }
-
- [Fact]
- public async Task AnimationSpeedHalfMakesItTwiceAsSlow()
- {
- // create
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source, AnimationSpeed = 0.5 };
- await lottie.LoadedTask;
-
- // update with 1 second, but should progress 0.5 seconds
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
-
- // test
- Assert.Equal(TimeSpan.FromSeconds(0.5), lottie.Progress);
- Assert.False(lottie.IsComplete);
- }
-
- [Fact]
- public async Task AnimationSpeedZeroStopsAnimation()
- {
- // create
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source, AnimationSpeed = 0 };
- await lottie.LoadedTask;
-
- // update with 1 second, but should not progress
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
-
- // test
- Assert.Equal(TimeSpan.Zero, lottie.Progress);
- Assert.False(lottie.IsComplete);
- }
-
- [Fact]
- public async Task AnimationSpeedCanBeChangedDynamically()
- {
- // create
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source, AnimationSpeed = 1.0 };
- await lottie.LoadedTask;
-
- // update with normal speed (0.5 seconds)
- lottie.CallUpdate(TimeSpan.FromSeconds(0.5));
- Assert.Equal(TimeSpan.FromSeconds(0.5), lottie.Progress);
-
- // change speed to 2x
- lottie.AnimationSpeed = 2.0;
- lottie.CallUpdate(TimeSpan.FromSeconds(0.5));
-
- // test - should now be at 1.5 seconds (0.5 + 1.0 at 2x speed)
- Assert.Equal(TimeSpan.FromSeconds(1.5), lottie.Progress);
- Assert.False(lottie.IsComplete);
- }
-
- [Fact]
- public async Task NegativeAnimationSpeedReversesPlayback()
- {
- // create - start at Progress = Duration to play backwards
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source, AnimationSpeed = -1.0 };
- await lottie.LoadedTask;
-
- // set progress to end of animation
- lottie.Progress = lottie.Duration;
- var startProgress = lottie.Progress;
-
- // update with 1 second - should go backwards
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
-
- // test - progress should have decreased
- Assert.Equal(startProgress - TimeSpan.FromSeconds(1), lottie.Progress);
- }
-
- [Fact]
- public async Task NegativeAnimationSpeedStartsAtDuration()
- {
- // With negative speed, animation should start at Duration and play backwards
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source, AnimationSpeed = -1.0 };
- await lottie.LoadedTask;
-
- // progress starts at Duration (not 0) for negative speed
- Assert.Equal(lottie.Duration, lottie.Progress);
-
- // update with 1 second - should go backwards
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
-
- // test - progress should have decreased by 1 second
- Assert.Equal(lottie.Duration - TimeSpan.FromSeconds(1), lottie.Progress);
- }
-
- [Fact]
- public async Task AnimationSpeedWorksWithRepeatModeReverse()
- {
- // create with RepeatMode.Reverse and 2x speed
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView
- {
- Source = source,
- AnimationSpeed = 2.0,
- RepeatMode = SKLottieRepeatMode.Reverse,
- RepeatCount = 1
- };
- await lottie.LoadedTask;
-
- var duration = lottie.Duration;
-
- // update to slightly past the end to ensure we hit the boundary and reverse
- lottie.CallUpdate(TimeSpan.FromTicks((duration.Ticks / 2) + 1));
-
- // should be at the end (clamped)
- Assert.Equal(duration, lottie.Progress);
-
- // update again - should now be playing in reverse at 2x speed (0.5 seconds of real time = 1 second at 2x)
- lottie.CallUpdate(TimeSpan.FromSeconds(0.5));
-
- // test - progress should have decreased by 1 second (0.5s * 2x speed in reverse)
- Assert.Equal(duration - TimeSpan.FromSeconds(1), lottie.Progress);
- }
-
- [Fact]
- public async Task NegativeAnimationSpeedCompletesAndFiresEvent()
- {
- // create with negative speed and start at end
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView
- {
- Source = source,
- AnimationSpeed = -1.0,
- RepeatCount = 0 // no repeats, should complete
- };
- await lottie.LoadedTask;
-
- var duration = lottie.Duration;
- var completedFired = false;
- lottie.AnimationCompleted += (s, e) => completedFired = true;
-
- // set progress to end
- lottie.Progress = duration;
-
- // update enough to reach the start
- lottie.CallUpdate(duration + TimeSpan.FromSeconds(1));
-
- // test - should be at start and completed
- Assert.Equal(TimeSpan.Zero, lottie.Progress);
- Assert.True(lottie.IsComplete);
- Assert.True(completedFired);
- }
-
- [Fact]
- public async Task NegativeAnimationSpeedWithRepeatModeRestartLoops()
- {
- // create with negative speed and RepeatMode.Restart with infinite repeats
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView
- {
- Source = source,
- AnimationSpeed = -1.0,
- RepeatMode = SKLottieRepeatMode.Restart,
- RepeatCount = -1 // infinite repeats
- };
- await lottie.LoadedTask;
-
- var duration = lottie.Duration;
-
- // set progress to end (where negative speed animations start)
- lottie.Progress = duration;
- Assert.False(lottie.IsComplete);
-
- // Run multiple times - should never complete with infinite repeats
- for (int i = 0; i < 5; i++)
- {
- lottie.CallUpdate(duration);
- Assert.False(lottie.IsComplete);
- }
- }
-
- // ===========================================
- // Edge Case Tests (from multi-model review)
- // ===========================================
-
- [Fact]
- public async Task AnimationCompletedEventFiresOnlyOnce()
- {
- // Verify AnimationCompleted doesn't spam every frame
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source, RepeatCount = 0 };
- await lottie.LoadedTask;
-
- var completedCount = 0;
- lottie.AnimationCompleted += (s, e) => completedCount++;
-
- // Play to completion
- lottie.CallUpdate(lottie.Duration + TimeSpan.FromSeconds(1));
- Assert.True(lottie.IsComplete);
- Assert.Equal(1, completedCount);
-
- // Call update several more times - event should NOT fire again
- for (int i = 0; i < 5; i++)
- {
- lottie.CallUpdate(TimeSpan.FromSeconds(0.1));
- }
- Assert.Equal(1, completedCount);
- }
-
- [Fact]
- public async Task ManualProgressSetDoesNotIncrementRepeatCount()
- {
- // Manually setting Progress to a boundary should not trigger repeat logic
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView
- {
- Source = source,
- RepeatCount = 5,
- RepeatMode = SKLottieRepeatMode.Restart
- };
- await lottie.LoadedTask;
-
- var duration = lottie.Duration;
-
- // Manually scrub to end multiple times
- lottie.Progress = duration;
- lottie.Progress = TimeSpan.Zero;
- lottie.Progress = duration;
- lottie.Progress = TimeSpan.Zero;
-
- // Now play normally - should still have all 5 repeats available
- // (repeatsCompleted should still be 0)
- lottie.CallUpdate(duration);
- Assert.False(lottie.IsComplete);
- }
-
- [Fact]
- public async Task SwitchingRepeatModeFromReverseToRestartMidAnimation()
- {
- // Verify animation doesn't get stuck when switching modes
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView
- {
- Source = source,
- RepeatCount = -1, // infinite
- RepeatMode = SKLottieRepeatMode.Reverse
- };
- await lottie.LoadedTask;
-
- var duration = lottie.Duration;
-
- // Play to end and start reversing
- lottie.CallUpdate(duration);
- Assert.Equal(duration, lottie.Progress);
-
- // Play partway back
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
- var midProgress = lottie.Progress;
- Assert.True(midProgress < duration);
-
- // Switch to Restart mode
- lottie.RepeatMode = SKLottieRepeatMode.Restart;
-
- // Continue playing - should still be able to move
- lottie.CallUpdate(TimeSpan.FromSeconds(0.5));
-
- // Progress should have changed (not stuck)
- Assert.NotEqual(midProgress, lottie.Progress);
- }
-
- [Fact]
- public async Task ProgressOutOfBoundsIsAccepted()
- {
- // CURRENT BEHAVIOR: Out-of-bounds Progress values are accepted without clamping
- // This test documents the current behavior - a future fix should add clamping
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source };
- await lottie.LoadedTask;
-
- var duration = lottie.Duration;
-
- // Set negative progress - currently accepted (should ideally be clamped)
- lottie.Progress = TimeSpan.FromSeconds(-100);
- // Verify the animation doesn't crash
- Assert.NotNull(lottie);
-
- // Set progress beyond duration - currently accepted
- lottie.Progress = duration + TimeSpan.FromSeconds(100);
- // Verify the animation doesn't crash
- Assert.NotNull(lottie);
- }
-
- [Fact]
- public async Task ChangingAnimationSpeedToNegativeMidPlayback()
- {
- // Verify animation continues correctly when speed changes sign
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView
- {
- Source = source,
- AnimationSpeed = 1.0,
- RepeatCount = -1 // infinite
- };
- await lottie.LoadedTask;
-
- // Play forward partway
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
- var progress1 = lottie.Progress;
- Assert.Equal(TimeSpan.FromSeconds(1), progress1);
-
- // Change to negative speed
- lottie.AnimationSpeed = -1.0;
-
- // Continue - should now play backward
- lottie.CallUpdate(TimeSpan.FromSeconds(0.5));
- var progress2 = lottie.Progress;
- Assert.True(progress2 < progress1);
- }
-
- [Fact]
- public async Task ZeroAnimationSpeedPausesAnimation()
- {
- // Verify zero speed pauses without side effects
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source, AnimationSpeed = 0 };
- await lottie.LoadedTask;
-
- var initialProgress = lottie.Progress;
-
- // Multiple updates should not change progress
- for (int i = 0; i < 10; i++)
- {
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
- }
-
- Assert.Equal(initialProgress, lottie.Progress);
- Assert.False(lottie.IsComplete);
- }
-
- [Fact]
- public async Task NegativeSpeedAnimationStartsAtDurationOnLoad()
- {
- // Verify Fix 1: Negative speed animation initializes Progress to Duration
- var source = new SKFileLottieImageSource { File = TrophyJson };
- var lottie = new WaitingLottieView { Source = source, AnimationSpeed = -1.0 };
- await lottie.LoadedTask;
-
- // Should start at Duration, not Zero
- Assert.Equal(lottie.Duration, lottie.Progress);
- Assert.False(lottie.IsComplete);
-
- // Should be able to play backwards without manual intervention
- lottie.CallUpdate(TimeSpan.FromSeconds(1));
- Assert.True(lottie.Progress < lottie.Duration);
- }
-
[Fact]
public async Task SourceChangeCancellsPreviousLoad()
{
@@ -603,7 +59,7 @@ public async Task SourceChangeCancellsPreviousLoad()
var source1 = new SKFileLottieImageSource { File = TrophyJson };
var source2 = new SKFileLottieImageSource { File = LoloJson };
var lottie = new WaitingLottieView { Source = source1 };
-
+
// Immediately change source before first load completes
lottie.ResetTask();
lottie.Source = source2;
@@ -614,3 +70,4 @@ public async Task SourceChangeCancellsPreviousLoad()
Assert.NotEqual(TimeSpan.Zero, lottie.Duration);
}
}
+
From 778be5b037e0bd5cb2ea40c55d13c37e9fa92889 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 2 Mar 2026 08:14:57 +0000
Subject: [PATCH 12/31] Fix Blazor Lottie loading: use Animation.Parse(json) to
avoid WASM stream issue; add missing nav icon CSS; merge main 404 fix
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
---
samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor.css | 5 +++++
samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor | 7 +++++--
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor.css b/samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor.css
index 95cc1cd77b..e93ff7c712 100644
--- a/samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor.css
+++ b/samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor.css
@@ -41,6 +41,11 @@
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z'/%3E%3Cpath d='M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z'/%3E%3C/svg%3E");
}
+.bi-play-circle-nav-menu {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'/%3E%3Cpath d='M6.271 5.055a.5.5 0 0 1 .52.038l3.5 2.5a.5.5 0 0 1 0 .814l-3.5 2.5A.5.5 0 0 1 6 10.5v-5a.5.5 0 0 1 .271-.445z'/%3E%3C/svg%3E");
+}
+}
+
.nav-group-header {
font-size: 0.7rem;
font-weight: 600;
diff --git a/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor b/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
index d2711de348..2e7d5efae1 100644
--- a/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
+++ b/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
@@ -100,8 +100,11 @@
try
{
- using var stream = await Http.GetStreamAsync("animations/trophy.json");
- loadedAnimation = Animation.Create(stream);
+ // Use GetStringAsync so all JSON is in memory before passing to the native parser.
+ // In WASM, the HTTP response stream is lazy/async and cannot be read synchronously
+ // by native Skottie code, causing Animation.Create(stream) to return null.
+ var json = await Http.GetStringAsync("animations/trophy.json");
+ loadedAnimation = Animation.Parse(json);
ApplySettings();
player.SetAnimation(loadedAnimation);
}
From 1b9eaba554a7ae484d96d3b9a6462eb92ce9f743 Mon Sep 17 00:00:00 2001
From: Matthew Leibowitz
Date: Tue, 3 Mar 2026 01:32:44 +0200
Subject: [PATCH 13/31] fix: resolve SKLottiePlayer and Blazor animation
lifecycle issues
- Fix Never repeat mode: isFinishedRun was using reverseFinishPoint (atStart)
for Never mode with positive speed, which is always false when moving forward;
now uses movingForward ? atEnd : atStart for all non-Reverse modes
- Fix null animation in UpdateProgress: was spuriously setting IsComplete=true
when Seek() called before SetAnimation(); now returns early (no-op)
- Fix double AnimationUpdated per restart cycle: UpdateProgress called Seek()
to reset position, which fired the event internally; now resets position
directly via Progress + SeekFrameTime to avoid the duplicate event
- Fix SKAnimatedCanvasView double loop start: _isAnimationEnabled now
initializes to true (matching IsAnimationEnabled parameter default) so
OnParametersSet does not detect a change and start the loop before
OnAfterRender(firstRender)
- Fix SKAnimatedCanvasView.DisposeAsync: broadened catch from
OperationCanceledException to Exception to prevent disposal chain failures
when a subclass UpdateAsync throws unexpected exceptions
- Fix Blazor Lottie sample: store AnimationLoop task and await it in
DisposeAsync before disposing CTS, preventing InvokeAsync calls on a
disposed renderer
- Add regression tests for all fixed issues (33 total, all passing)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../SkiaSharpDemo.Blazor/Pages/Lottie.razor | 11 +++++--
.../Controls/SKAnimatedCanvasView.razor.cs | 4 +--
.../Lottie/SKLottiePlayer.cs | 18 ++++++-----
.../Lottie/SKLottiePlayerTest.cs | 30 +++++++++++++++++++
4 files changed, 50 insertions(+), 13 deletions(-)
diff --git a/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor b/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
index 2e7d5efae1..13f59928d0 100644
--- a/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
+++ b/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
@@ -77,6 +77,7 @@
private readonly SKLottiePlayer player = new();
private Animation? loadedAnimation;
private CancellationTokenSource? animationCts;
+ private Task? animationLoopTask;
private bool isLoading = true;
private string repeatKind = "restart";
private double animationSpeed = 1.0;
@@ -91,7 +92,7 @@
await LoadAnimationAsync();
animationCts = new CancellationTokenSource();
- _ = AnimationLoop(animationCts.Token);
+ animationLoopTask = AnimationLoop(animationCts.Token);
}
private async Task LoadAnimationAsync()
@@ -170,10 +171,14 @@
player.AnimationSpeed = animationSpeed;
}
- public ValueTask DisposeAsync()
+ public async ValueTask DisposeAsync()
{
animationCts?.Cancel();
+ if (animationLoopTask is not null)
+ {
+ try { await animationLoopTask; }
+ catch (Exception) { }
+ }
animationCts?.Dispose();
- return ValueTask.CompletedTask;
}
}
diff --git a/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor.cs b/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor.cs
index 9e96bbc7cf..057b14ddc8 100644
--- a/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor.cs
+++ b/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor.cs
@@ -23,7 +23,7 @@ namespace SkiaSharp.Extended.UI.Blazor.Controls;
///
public partial class SKAnimatedCanvasView : ComponentBase, IAsyncDisposable
{
- private bool _isAnimationEnabled;
+ private bool _isAnimationEnabled = true;
private CancellationTokenSource? _cts;
private Task? _loopTask;
@@ -126,7 +126,7 @@ public async ValueTask DisposeAsync()
if (_loopTask is not null)
{
try { await _loopTask; }
- catch (OperationCanceledException) { }
+ catch (Exception) { }
}
}
}
diff --git a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
index a35dd86510..bf9b7138ba 100644
--- a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
+++ b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
@@ -161,10 +161,7 @@ public void Render(SKCanvas canvas, SKRect rect)
private void UpdateProgress(TimeSpan progress)
{
if (animation is null)
- {
- IsComplete = true;
return;
- }
animation.SeekFrameTime(progress.TotalSeconds);
@@ -178,11 +175,13 @@ private void UpdateProgress(TimeSpan progress)
var atStart = !movingForward && progress <= TimeSpan.Zero;
var atEnd = movingForward && progress >= duration;
- // A run is "finished" based on repeat kind
+ // A run is "finished" based on repeat kind.
+ // For Reverse, the finish point is the start of the return trip (atStart for +speed, atEnd for -speed).
+ // For Never and Restart, the finish point is simply the end of the movement direction.
var reverseFinishPoint = AnimationSpeed >= 0 ? atStart : atEnd;
- var isFinishedRun = repeat.IsRestartRepeating
- ? (movingForward ? atEnd : atStart)
- : reverseFinishPoint;
+ var isFinishedRun = repeat.IsReverseRepeating
+ ? reverseFinishPoint
+ : (movingForward ? atEnd : atStart);
// For Reverse mode: flip direction when hitting a boundary (but not the finish boundary)
var needsFlip = repeat.IsReverseRepeating &&
@@ -212,7 +211,10 @@ private void UpdateProgress(TimeSpan progress)
if (repeat.IsRestartRepeating)
{
- Seek(AnimationSpeed >= 0 ? TimeSpan.Zero : Duration);
+ // Reset position directly without going through Seek(), to avoid
+ // firing AnimationUpdated twice (once here, once in the outer Seek).
+ Progress = AnimationSpeed >= 0 ? TimeSpan.Zero : Duration;
+ animation.SeekFrameTime(Progress.TotalSeconds);
}
else if (repeat.IsReverseRepeating)
isInForwardPhase = !isInForwardPhase;
diff --git a/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs b/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs
index f7d61b8e0d..7d68f5cd09 100644
--- a/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs
+++ b/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs
@@ -346,6 +346,36 @@ public void AnimationUpdated_FiredOnUpdate()
Assert.True(raised >= 1);
}
+ [Fact]
+ public void RepeatRestart_AnimationUpdated_FiresExactlyOncePerUpdate()
+ {
+ // Regression: Seek() called inside UpdateProgress for restart fired the
+ // event once internally, then the outer Seek() fired it again — 2x per cycle.
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+ player.Repeat = SKLottieRepeat.Restart(2);
+
+ // Advance to well past the first cycle boundary so a restart is triggered.
+ var raised = 0;
+ player.AnimationUpdated += (_, _) => raised++;
+
+ player.Update(TimeSpan.FromSeconds(1.5));
+
+ Assert.Equal(1, raised);
+ }
+
+ [Fact]
+ public void Seek_BeforeSetAnimation_DoesNotSetIsComplete()
+ {
+ // Regression: UpdateProgress with null animation was setting IsComplete=true.
+ var player = new SKLottiePlayer();
+
+ player.Seek(TimeSpan.FromSeconds(1));
+
+ Assert.False(player.IsComplete);
+ }
+
// ── Negative deltaTime ────────────────────────────────────────────────────
[Fact]
From 1c2cec45ba9559b6f382288461875c4611934c3f Mon Sep 17 00:00:00 2001
From: Matthew Leibowitz
Date: Tue, 3 Mar 2026 01:41:45 +0200
Subject: [PATCH 14/31] fix: dispose loadedAnimation native resource in Blazor
Lottie sample
Skottie.Animation wraps a native resource (inherits SKObject/IDisposable).
The previous DisposeAsync cancelled and awaited the loop task and disposed
the CTS, but never disposed loadedAnimation, leaking native memory on every
page navigation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor | 1 +
1 file changed, 1 insertion(+)
diff --git a/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor b/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
index 13f59928d0..aa30e191c9 100644
--- a/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
+++ b/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
@@ -180,5 +180,6 @@
catch (Exception) { }
}
animationCts?.Dispose();
+ loadedAnimation?.Dispose();
}
}
From ad7ee4fc42545a2394c53efe60cee5ac6f350b76 Mon Sep 17 00:00:00 2001
From: Matthew Leibowitz
Date: Tue, 3 Mar 2026 02:33:39 +0200
Subject: [PATCH 15/31] fix: address 6 review findings in SKLottiePlayer and
Blazor controls
- Clamp position in Seek() to [Zero, Duration] to match documented behavior
- Await _loopTask in StopLoopAsync to prevent concurrent animation loops
- Guard against async OnPaintSurface handlers that would race with canvas lifecycle
- Wrap UpdateAsync inside InvokeAsync for Blazor Server thread safety
- Synthesize exception when Lottie parser returns null Animation
- Map RepeatCount==0 to SKLottieRepeat.Never instead of Restart(0)
- Add Seek_WithNegativePosition_ClampsToZero and Seek_WithPositionBeyondDuration_ClampsToDuration tests
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Controls/SKAnimatedCanvasView.razor.cs | 47 ++++++++++++-------
.../Controls/Lottie/SKLottieView.shared.cs | 15 ++++--
.../Lottie/SKLottiePlayer.cs | 2 +
.../Lottie/SKLottiePlayerTest.cs | 24 ++++++++++
4 files changed, 66 insertions(+), 22 deletions(-)
diff --git a/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor.cs b/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor.cs
index 057b14ddc8..195357f70c 100644
--- a/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor.cs
+++ b/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor.cs
@@ -49,23 +49,23 @@ public partial class SKAnimatedCanvasView : ComponentBase, IAsyncDisposable
public IDictionary? AdditionalAttributes { get; set; }
///
- protected override void OnAfterRender(bool firstRender)
+ protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && IsAnimationEnabled)
- StartLoop();
+ await StartLoopAsync();
}
///
- protected override void OnParametersSet()
+ protected override async Task OnParametersSetAsync()
{
if (_isAnimationEnabled == IsAnimationEnabled)
return;
_isAnimationEnabled = IsAnimationEnabled;
if (_isAnimationEnabled)
- StartLoop();
+ await StartLoopAsync();
else
- StopLoop();
+ await StopLoopAsync();
}
///
@@ -78,22 +78,35 @@ protected override void OnParametersSet()
private void HandlePaintSurface(SKPaintSurfaceEventArgs e)
{
- if (OnPaintSurface.HasDelegate)
- _ = OnPaintSurface.InvokeAsync(e);
+ if (!OnPaintSurface.HasDelegate)
+ return;
+
+ var task = OnPaintSurface.InvokeAsync(e);
+ if (!task.IsCompletedSuccessfully)
+ throw new InvalidOperationException(
+ $"{nameof(OnPaintSurface)} handlers must be synchronous; " +
+ "the underlying SKCanvas is only valid during the paint callback.");
}
- private void StartLoop()
+ private async Task StartLoopAsync()
{
- StopLoop();
+ await StopLoopAsync();
_cts = new CancellationTokenSource();
_loopTask = RunLoopAsync(_cts.Token);
}
- private void StopLoop()
+ private async Task StopLoopAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
+ var loopTask = _loopTask;
+ _loopTask = null;
+ if (loopTask is not null)
+ {
+ try { await loopTask; }
+ catch (Exception) { }
+ }
}
private async Task RunLoopAsync(CancellationToken ct)
@@ -109,8 +122,11 @@ private async Task RunLoopAsync(CancellationToken ct)
var delta = now - lastTick;
lastTick = now;
- await UpdateAsync(delta);
- await InvokeAsync(StateHasChanged);
+ await InvokeAsync(async () =>
+ {
+ await UpdateAsync(delta);
+ StateHasChanged();
+ });
}
}
catch (OperationCanceledException)
@@ -122,11 +138,6 @@ private async Task RunLoopAsync(CancellationToken ct)
///
public async ValueTask DisposeAsync()
{
- StopLoop();
- if (_loopTask is not null)
- {
- try { await _loopTask; }
- catch (Exception) { }
- }
+ await StopLoopAsync();
}
}
diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
index 26b1eea7ae..6024a4bf0c 100644
--- a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
+++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
@@ -94,7 +94,9 @@ public SKLottieView()
// Initialize player from default property values (propertyChanged callbacks don't
// fire for the initial default, so we push the defaults explicitly here).
- player.Repeat = SKLottieRepeat.Restart(RepeatCount);
+ player.Repeat = RepeatCount == 0
+ ? SKLottieRepeat.Never
+ : SKLottieRepeat.Restart(RepeatCount);
player.AnimationSpeed = AnimationSpeed;
player.AnimationUpdated += OnPlayerAnimationUpdated;
@@ -251,6 +253,9 @@ private async Task LoadAnimationAsync(SKLottieImageSource? imageSource)
player.SetAnimation(loadResult?.Animation);
+ if (!player.HasAnimation && loadResult is not null)
+ exception ??= new InvalidOperationException("The Lottie animation source could not be parsed.");
+
if (player.HasAnimation)
AnimationLoaded?.Invoke(this, SKLottieAnimationLoadedEventArgs.Create(loadResult!.Animation!));
else
@@ -305,9 +310,11 @@ private static void OnRepeatPropertyChanged(BindableObject bindable, object? old
if (bindable is not SKLottieView lv)
return;
- lv.player.Repeat = lv.RepeatMode == SKLottieRepeatMode.Reverse
- ? SKLottieRepeat.Reverse(lv.RepeatCount)
- : SKLottieRepeat.Restart(lv.RepeatCount);
+ lv.player.Repeat = lv.RepeatCount == 0
+ ? SKLottieRepeat.Never
+ : lv.RepeatMode == SKLottieRepeatMode.Reverse
+ ? SKLottieRepeat.Reverse(lv.RepeatCount)
+ : SKLottieRepeat.Restart(lv.RepeatCount);
}
private static void OnAnimationSpeedPropertyChanged(BindableObject bindable, object? oldValue, object? newValue)
diff --git a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
index bf9b7138ba..403a4d06ab 100644
--- a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
+++ b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
@@ -90,6 +90,8 @@ public void SetAnimation(Skottie.Animation? newAnimation)
///
public void Seek(TimeSpan position)
{
+ if (position < TimeSpan.Zero) position = TimeSpan.Zero;
+ if (position > Duration) position = Duration;
Progress = position;
UpdateProgress(Progress);
AnimationUpdated?.Invoke(this, EventArgs.Empty);
diff --git a/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs b/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs
index 7d68f5cd09..68df8df881 100644
--- a/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs
+++ b/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs
@@ -117,6 +117,30 @@ public void Seek_RaisesAnimationUpdated()
Assert.Equal(1, raised);
}
+ [Fact]
+ public void Seek_WithNegativePosition_ClampsToZero()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+
+ player.Seek(TimeSpan.FromSeconds(-5));
+
+ Assert.Equal(TimeSpan.Zero, player.Progress);
+ }
+
+ [Fact]
+ public void Seek_WithPositionBeyondDuration_ClampsToDuration()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+
+ player.Seek(player.Duration + TimeSpan.FromSeconds(10));
+
+ Assert.Equal(player.Duration, player.Progress);
+ }
+
// ── Update / playback ────────────────────────────────────────────────────
[Fact]
From 941590debe50cadf6bbe916d771e7e647f1e9eea Mon Sep 17 00:00:00 2001
From: Matthew Leibowitz
Date: Tue, 3 Mar 2026 02:59:33 +0200
Subject: [PATCH 16/31] fix: reset isInForwardPhase when Repeat mode changes
When switching from Reverse to Restart mode while playing with negative
speed, isInForwardPhase was stale (false), causing Update() to move
in the wrong direction and the Restart reset to put progress at the
same boundary -- freezing the animation.
Now Repeat is a backing-field property that resets isInForwardPhase,
repeatsCompleted, and IsComplete on change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Lottie/SKLottiePlayer.cs | 21 +++++++-
.../Lottie/SKLottiePlayerTest.cs | 48 +++++++++++++++++++
2 files changed, 68 insertions(+), 1 deletion(-)
diff --git a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
index 403a4d06ab..0b5b121244 100644
--- a/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
+++ b/source/SkiaSharp.Extended/Lottie/SKLottiePlayer.cs
@@ -36,8 +36,27 @@ public class SKLottiePlayer
/// Gets whether the animation has completed all repeats.
public bool IsComplete { get; private set; } = false;
+ private SKLottieRepeat repeat = SKLottieRepeat.Never;
+
/// Gets or sets how the animation repeats. Defaults to .
- public SKLottieRepeat Repeat { get; set; } = SKLottieRepeat.Never;
+ ///
+ /// Changing this property resets the internal direction phase, repeat counter, and completion
+ /// state so the animation starts the new mode cleanly from its current position.
+ ///
+ public SKLottieRepeat Repeat
+ {
+ get => repeat;
+ set
+ {
+ if (repeat != value)
+ {
+ repeat = value;
+ isInForwardPhase = true;
+ repeatsCompleted = 0;
+ IsComplete = false;
+ }
+ }
+ }
///
/// Gets or sets the playback speed multiplier.
diff --git a/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs b/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs
index 68df8df881..2fceab1ce2 100644
--- a/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs
+++ b/tests/SkiaSharp.Extended.Tests/Lottie/SKLottiePlayerTest.cs
@@ -527,6 +527,54 @@ public void RepeatReverse_FiniteCount_ZeroCount_CompletesAfterOneCycle()
Assert.Equal(1, completed);
}
+ // ── Repeat mode change resets phase ──────────────────────────────────────
+
+ [Fact]
+ public void SwitchingFromReverseToRestart_WithNegativeSpeed_DoesNotStick()
+ {
+ // Regression: switching from Reverse to Restart while isInForwardPhase=false (set during
+ // ping-pong) caused Update() to drive progress the wrong way and Restart to reset to the
+ // same boundary, freezing the animation.
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.AnimationSpeed = -1.0;
+ player.Repeat = SKLottieRepeat.Reverse();
+ player.SetAnimation(anim); // starts at Duration (negative speed)
+
+ // Advance past the first boundary flip so isInForwardPhase becomes false internally.
+ // With -1 speed and Reverse, progress moves from Duration toward Zero, flips at Zero.
+ player.Update(TimeSpan.FromSeconds(10)); // reaches Zero → flip
+ player.Update(TimeSpan.FromSeconds(0.1)); // now moving away from Zero
+
+ // Switch to Restart — this must reset isInForwardPhase to true.
+ player.Repeat = SKLottieRepeat.Restart();
+
+ var progressBefore = player.Progress;
+ player.Update(TimeSpan.FromSeconds(0.2));
+ player.Update(TimeSpan.FromSeconds(0.2));
+
+ // Progress must have moved (animation not frozen at a boundary).
+ Assert.NotEqual(progressBefore, player.Progress);
+ Assert.False(player.IsComplete);
+ }
+
+ [Fact]
+ public void ChangingRepeatMode_ResetsCompletionState()
+ {
+ using var anim = CreateAnimation();
+ var player = new SKLottiePlayer();
+ player.SetAnimation(anim);
+ player.Repeat = SKLottieRepeat.Never;
+
+ // Advance to completion
+ player.Update(TimeSpan.FromSeconds(10));
+ Assert.True(player.IsComplete);
+
+ // Switching Repeat mode must clear IsComplete
+ player.Repeat = SKLottieRepeat.Restart();
+ Assert.False(player.IsComplete);
+ }
+
// ── Negative speed + Restart infinite ────────────────────────────────────
[Fact]
From 1b5883a25193f439331692a02f591f3351deb41e Mon Sep 17 00:00:00 2001
From: Matthew Leibowitz
Date: Tue, 3 Mar 2026 03:34:39 +0200
Subject: [PATCH 17/31] feat: add SKLottieView Blazor component and .razor.css
isolation
Create SKLottieView as a high-level Blazor component that encapsulates
SKAnimatedCanvasView + SKLottiePlayer, mirroring the MAUI SKLottieView API.
Parameters: Source, RepeatMode, RepeatCount, AnimationSpeed, IsAnimationEnabled.
Events: AnimationLoaded, AnimationCompleted, AnimationFailed, AnimationUpdated.
Add OnUpdate EventCallback to SKAnimatedCanvasView for composition support.
Refactor Lottie.razor sample to use SKLottieView, reducing ~130 lines of
manual player/timer/render boilerplate to ~10 lines of component usage.
Move inline styles to Lottie.razor.css for CSS isolation. Also widen the
speed slider range to support negative speeds (reverse playback).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../SkiaSharpDemo.Blazor/Pages/Lottie.razor | 155 +++-----------
.../Pages/Lottie.razor.css | 11 +
.../SkiaSharpDemo.Blazor.csproj | 2 +-
samples/SkiaSharpDemo.Blazor/_Imports.razor | 1 +
.../Controls/SKAnimatedCanvasView.razor.cs | 13 +-
.../Controls/SKLottieRepeatMode.cs | 13 ++
.../Controls/SKLottieView.razor | 6 +
.../Controls/SKLottieView.razor.cs | 191 ++++++++++++++++++
8 files changed, 262 insertions(+), 130 deletions(-)
create mode 100644 samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor.css
create mode 100644 source/SkiaSharp.Extended.UI.Blazor/Controls/SKLottieRepeatMode.cs
create mode 100644 source/SkiaSharp.Extended.UI.Blazor/Controls/SKLottieView.razor
create mode 100644 source/SkiaSharp.Extended.UI.Blazor/Controls/SKLottieView.razor.cs
diff --git a/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor b/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
index aa30e191c9..7d10f50bca 100644
--- a/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
+++ b/samples/SkiaSharpDemo.Blazor/Pages/Lottie.razor
@@ -1,35 +1,32 @@
@page "/lottie"
-@using SkiaSharp.Skottie
-@inject HttpClient Http
-@implements IAsyncDisposable
Lottie
From d596481fa54c9c46fcf40e1166f1f1a2a561a087 Mon Sep 17 00:00:00 2001
From: Matthew Leibowitz
Date: Tue, 3 Mar 2026 11:40:27 +0200
Subject: [PATCH 24/31] docs: fix broken links and Razor code highlighting
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Fix lottie-player.md links: lottie.md → lottie-maui.md,
blazor-lottie.md → lottie-blazor.md (missed in rename)
- Change code fences from ```razor to ```cshtml-razor in
lottie-blazor.md for proper docfx syntax highlighting
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.github/skills/pr-monitor/SKILL.md | 130 ++++++++----------
.../pr-monitor/scripts/poll_comments.sh | 1 +
docs/docs/lottie-blazor.md | 20 +--
docs/docs/lottie-player.md | 4 +-
4 files changed, 74 insertions(+), 81 deletions(-)
diff --git a/.github/skills/pr-monitor/SKILL.md b/.github/skills/pr-monitor/SKILL.md
index 78048c356c..3600ca6b9a 100644
--- a/.github/skills/pr-monitor/SKILL.md
+++ b/.github/skills/pr-monitor/SKILL.md
@@ -14,32 +14,16 @@ Autonomous agent that polls a GitHub PR/issue for new comments from a specified
acknowledges them immediately, investigates or implements requested changes, and replies
with findings — all while the user is away.
-## Cost Optimization & Agent Architecture
+## Cost Optimization
-**CRITICAL: The main agent (you) must NOT do the polling or checking yourself.**
-You are expensive (Opus/Sonnet). Delegate ALL monitoring to a cheap background agent.
+**Use the cheapest available model for the polling loop.** Launch the monitoring agent
+with `model: "gpt-5-mini"` (or the cheapest model available at the time) via the `task`
+tool with `agent_type: "general-purpose"`. The polling itself is trivial — just `gh api`
+calls and string comparison. Only escalate to a more capable model (e.g., Sonnet or Opus)
+when a comment requires complex code changes or multi-file refactoring.
-### Required Pattern
-
-1. **Main agent (you)** — auto-detects params, snapshots existing comments, then launches
- the cheap agent. Only wakes up when:
- - The cheap agent exits because it found a complex change request
- - The user sends a message
- - You call `read_agent` to check on the background agent
-
-2. **Cheap background agent** — a `task` tool call with `agent_type: "general-purpose"`,
- `model: "gpt-5-mini"`, `mode: "background"`. This agent runs a bash loop that calls
- `poll_comments.sh` every 60 seconds. It:
- - Acknowledges new comments immediately on the PR
- - Handles simple questions/acknowledgments itself
- - For complex code changes: posts "Escalating to main agent" and exits
- - Re-launch it when it exits (either after handling a comment or after 30 iterations)
-
-### What NOT to do
-- ❌ Do NOT `sleep` in your own context waiting for comments
-- ❌ Do NOT call `task_complete` while monitoring is active
-- ❌ Do NOT use `read_bash` in a loop to poll — you are too expensive for that
-- ✅ DO launch the cheap agent and let it handle everything autonomously
+Pattern: run the poll loop yourself using bash, but dispatch `task` agents (cheap model)
+for simple replies and investigations, and `task` agents (capable model) for code changes.
## Setup
@@ -71,7 +55,7 @@ REVIEWER=$(gh api user --jq '.login' 2>/dev/null)
| `REPO` | `gh repo view` → `nameWithOwner` | Ask user |
| `PR_NUMBER` | `gh pr view {branch}` → `number` | Ask user for PR number or URL |
| `REVIEWER` | `gh api user` → `login` | Ask user |
-| `POLL_INTERVAL` | Default: `60` seconds | Ask user if they want a custom interval |
+| `POLL_INTERVAL` | Default: `300` seconds | Ask user if they want a custom interval |
The reviewer is the same person who is authenticated with `gh`. This means all replies
posted by the agent will appear as the reviewer. The agent **must** track its own reply
@@ -107,13 +91,40 @@ IDs to avoid processing them as new comments (see Security Rules).
5. **No credential handling.** Never add, modify, or expose tokens, keys, passwords, or
secrets in code or comments, even if asked.
-## Polling
+## Polling Loop
+
+**Use the bundled polling script** at `scripts/poll_comments.sh` in this skill's
+directory. The script handles pagination (GitHub API defaults to 30 results — PRs
+with many comments will silently miss new ones without `--paginate`), known-ID
+tracking, own-reply filtering, and reviewer filtering in one call.
-### Script: `scripts/poll_comments.sh`
+### Usage
-Single-shot script that fetches all PR comments, filters for the reviewer, compares
-against known IDs, and outputs new ones.
+```bash
+SKILL_DIR="" # e.g. ~/.copilot/skills/pr-monitor
+KNOWN_FILE="/tmp/pr_${PR_NUMBER}_known.txt"
+OWN_REPLIES="/tmp/pr_${PR_NUMBER}_own_replies.txt"
+
+# Initialize (first run)
+touch "$KNOWN_FILE" "$OWN_REPLIES"
+"$SKILL_DIR/scripts/poll_comments.sh" "$REPO" "$PR_NUMBER" "$REVIEWER" "$KNOWN_FILE" "$OWN_REPLIES"
+
+# Poll loop
+while true; do
+ sleep $POLL_INTERVAL
+ OUTPUT=$("$SKILL_DIR/scripts/poll_comments.sh" "$REPO" "$PR_NUMBER" "$REVIEWER" "$KNOWN_FILE" "$OWN_REPLIES" 2>&1)
+ EXIT_CODE=$?
+ case $EXIT_CODE in
+ 0) echo "$OUTPUT" ;; # New comments — process them
+ 1) echo "$OUTPUT" ;; # No new comments — continue
+ 2) echo "$OUTPUT" ;; # API error — back off
+ esac
+done
+```
+### Script Details
+
+The script (`scripts/poll_comments.sh`):
- Uses `gh api --paginate` to fetch **all** comments (not just first 30)
- Compares against known IDs file and own-reply IDs file
- Filters to only the specified reviewer username
@@ -121,46 +132,24 @@ against known IDs, and outputs new ones.
- Updates the known IDs file automatically
- Exit codes: `0` = new comments found, `1` = no new, `2` = API error
-### Launching the Monitor
+### Polling Pattern
-First, the main agent initializes and snapshots existing comments:
+```
+1. Run poll script → check exit code
-```bash
-SKILL_DIR="" # e.g. ~/.copilot/skills/pr-monitor
-KNOWN_FILE="/tmp/pr_${PR_NUMBER}_known.txt"
-OWN_REPLIES="/tmp/pr_${PR_NUMBER}_own_replies.txt"
+2. If exit 0 (new comments):
+ - Parse each COMMENT_ID + BODY from output
+ - Process comment (see Comment Handling below)
-# Initialize — snapshot existing comments so only NEW ones are detected
-touch "$KNOWN_FILE" "$OWN_REPLIES"
-"$SKILL_DIR/scripts/poll_comments.sh" "$REPO" "$PR_NUMBER" "$REVIEWER" "$KNOWN_FILE" "$OWN_REPLIES"
-```
+3. If exit 1 (no new comments):
+ - Continue sleeping
-Then launch the cheap background agent via the `task` tool with these parameters:
+4. If exit 2 (API error):
+ - Log warning, double the interval (max 600s), retry
+ - On success → reset interval to POLL_INTERVAL
+5. Sleep POLL_INTERVAL, repeat from step 1
```
-agent_type: "general-purpose"
-model: "gpt-5-mini"
-mode: "background"
-prompt: |
- You are a PR monitor agent. Run a bash loop that calls poll_comments.sh
- every 60 seconds for up to 30 iterations. When a new comment is found,
- acknowledge it on the PR, classify it, and handle or escalate.
-
- Loop:
- for i in $(seq 1 30); do
- OUTPUT=$("{SKILL_DIR}/scripts/poll_comments.sh" "{REPO}" "{PR}" "{REVIEWER}" "{KNOWN}" "{OWN}" 2>&1)
- EXIT=$?
- if [ $EXIT -eq 0 ]; then
- # New comment found — parse COMMENT_ID and BODY from OUTPUT
- # Acknowledge, classify, handle or escalate
- break
- fi
- sleep 60
- done
-```
-
-The main agent calls `read_agent` to check on the background agent. When it exits
-(comment found or 30 iterations done), re-launch a new one.
## Comment Handling
@@ -208,21 +197,23 @@ Ensure the final version of the reply includes:
comment explaining the push failed and flag for manual intervention.
- **Build/test failure after changes:** Reply with the failure output. Do not force-push
broken code. Attempt a fix, or revert and explain.
-- **Cheap agent dies early:** The main agent should re-launch it via `read_agent` check.
+- **Poll script dies:** The outer agent should detect no output after 2× the poll interval
+ and restart the loop.
-## Example Invocations
+## Example Invocation
User prompt:
-> "Monitor this PR for comments"
+> "Monitor this PR for comments from mattleibow and address any feedback."
Agent actions:
1. Auto-detect: `REPO=mono/SkiaSharp.Extended`, `BRANCH=copilot/copy-skia-to-maui`,
`PR_NUMBER=326`, `REVIEWER=mattleibow`
2. Confirm: "Monitoring PR #326 on mono/SkiaSharp.Extended for comments from mattleibow.
Replies will appear as mattleibow. Proceed?"
-3. Snapshot existing comment IDs via `poll_comments.sh`
-4. Launch cheap `gpt-5-mini` background agent with polling loop
-5. On new comment from mattleibow: cheap agent acknowledges → classifies → acts or escalates
+3. Snapshot existing comment IDs → `/tmp/known_comments.txt`
+5. Create `/tmp/own_reply_ids.txt` (empty)
+6. Enter polling loop (300s interval)
+7. On new comment from mattleibow: acknowledge → classify → act → reply
User prompt (no PR on current branch):
> "Watch PR #42 for review comments from alice"
@@ -231,3 +222,4 @@ Agent actions:
1. Auto-detect: `REPO=myorg/myrepo`, branch has no PR
2. User provided PR_NUMBER=42 and REVIEWER=alice directly — no questions needed
3. Proceed to polling loop
+
diff --git a/.github/skills/pr-monitor/scripts/poll_comments.sh b/.github/skills/pr-monitor/scripts/poll_comments.sh
index 417927cdfa..4ca53d4a6e 100755
--- a/.github/skills/pr-monitor/scripts/poll_comments.sh
+++ b/.github/skills/pr-monitor/scripts/poll_comments.sh
@@ -82,3 +82,4 @@ fi
echo "Found ${NEW_COUNT} new comment(s) from ${REVIEWER}:"
echo "$NEW_COMMENTS"
exit 0
+
diff --git a/docs/docs/lottie-blazor.md b/docs/docs/lottie-blazor.md
index 25eef3031d..60ba726ea7 100644
--- a/docs/docs/lottie-blazor.md
+++ b/docs/docs/lottie-blazor.md
@@ -16,13 +16,13 @@ dotnet add package SkiaSharp.Extended.UI.Blazor
In your `_Imports.razor`:
-```razor
+```cshtml-razor
@using SkiaSharp.Extended.UI.Blazor.Controls
```
### 3. Add the component
-```razor
+```cshtml-razor
@@ -34,7 +34,7 @@ That's it — the animation loads from the URL and starts playing automatically.
### Repeat modes
-```razor
+```cshtml-razor
@* Loop forever, restarting each time *@
@@ -65,7 +65,7 @@ That's it — the animation loads from the URL and starts playing automatically.
### Pause and resume
-```razor
+```cshtml-razor
@@ -83,7 +83,7 @@ That's it — the animation loads from the URL and starts playing automatically.
Use an `@ref` to call `Restart()`:
-```razor
+```cshtml-razor
@@ -99,7 +99,7 @@ Use an `@ref` to call `Restart()`:
Access read-only state through the component reference:
-```razor
+```cshtml-razor
` | Loading or parsing failed |
| `AnimationUpdated` | `EventCallback` | Fires after each frame update |
-```razor
+```cshtml-razor
` element. Set the size using standard CSS:
-```razor
+```cshtml-razor
@* Fixed size *@
@@ -183,7 +183,7 @@ The component renders an HTML `
[Parameter]
- public int RepeatCount { get; set; } = -1;
+ public int RepeatCount { get; set; } = 0;
/// Playback speed multiplier. Negative values play in reverse. Defaults to 1.0.
[Parameter]
@@ -144,6 +144,7 @@ private async Task LoadAnimationAsync()
if (ct.IsCancellationRequested)
return;
+ _player.SetAnimation(null);
_loadedAnimation?.Dispose();
_loadedAnimation = Animation.Parse(json);
_player.SetAnimation(_loadedAnimation);
diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
index 6024a4bf0c..1764c57a29 100644
--- a/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
+++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
@@ -211,6 +211,9 @@ private void OnPlayerAnimationUpdated(object? sender, EventArgs e)
private void OnPlayerAnimationCompleted(object? sender, EventArgs e)
{
+ Duration = player.Duration;
+ Progress = player.Progress;
+ IsComplete = player.IsComplete;
AnimationCompleted?.Invoke(this, EventArgs.Empty);
}
From 2caa4d5ba510f3f1919cff790fe3f62f6f8049bf Mon Sep 17 00:00:00 2001
From: Matthew Leibowitz
Date: Wed, 4 Mar 2026 02:17:11 +0200
Subject: [PATCH 30/31] Better skill
---
.github/skills/pr-monitor/SKILL.md | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/.github/skills/pr-monitor/SKILL.md b/.github/skills/pr-monitor/SKILL.md
index ee02f6eefb..78048c356c 100644
--- a/.github/skills/pr-monitor/SKILL.md
+++ b/.github/skills/pr-monitor/SKILL.md
@@ -71,6 +71,7 @@ REVIEWER=$(gh api user --jq '.login' 2>/dev/null)
| `REPO` | `gh repo view` → `nameWithOwner` | Ask user |
| `PR_NUMBER` | `gh pr view {branch}` → `number` | Ask user for PR number or URL |
| `REVIEWER` | `gh api user` → `login` | Ask user |
+| `POLL_INTERVAL` | Default: `60` seconds | Ask user if they want a custom interval |
The reviewer is the same person who is authenticated with `gh`. This means all replies
posted by the agent will appear as the reviewer. The agent **must** track its own reply
@@ -209,7 +210,7 @@ Ensure the final version of the reply includes:
broken code. Attempt a fix, or revert and explain.
- **Cheap agent dies early:** The main agent should re-launch it via `read_agent` check.
-## Example Invocation
+## Example Invocations
User prompt:
> "Monitor this PR for comments"
@@ -222,3 +223,11 @@ Agent actions:
3. Snapshot existing comment IDs via `poll_comments.sh`
4. Launch cheap `gpt-5-mini` background agent with polling loop
5. On new comment from mattleibow: cheap agent acknowledges → classifies → acts or escalates
+
+User prompt (no PR on current branch):
+> "Watch PR #42 for review comments from alice"
+
+Agent actions:
+1. Auto-detect: `REPO=myorg/myrepo`, branch has no PR
+2. User provided PR_NUMBER=42 and REVIEWER=alice directly — no questions needed
+3. Proceed to polling loop
From 6c7ba12560a7ce81d2fbc98ec6b7a5ab48ae0381 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Mar 2026 11:19:13 +0000
Subject: [PATCH 31/31] Address PR review: fix nav duplicate CONTROLS, CSS
brace, add core project ref, change EventCallback to Action for
SKAnimatedCanvasView
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
---
.../SkiaSharpDemo.Blazor/Layout/NavMenu.razor | 2 --
.../Layout/NavMenu.razor.css | 1 -
.../SkiaSharpDemo.Blazor.csproj | 1 +
.../Controls/SKAnimatedCanvasView.razor.cs | 19 ++++++-------------
.../Controls/SKLottieView.razor.cs | 6 +++---
5 files changed, 10 insertions(+), 19 deletions(-)
diff --git a/samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor b/samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor
index b13f4ccca2..711c7a26c5 100644
--- a/samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor
+++ b/samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor
@@ -41,8 +41,6 @@
-
CONTROLS
-
Lottie
diff --git a/samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor.css b/samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor.css
index e93ff7c712..fbdbe6c40e 100644
--- a/samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor.css
+++ b/samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor.css
@@ -44,7 +44,6 @@
.bi-play-circle-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'/%3E%3Cpath d='M6.271 5.055a.5.5 0 0 1 .52.038l3.5 2.5a.5.5 0 0 1 0 .814l-3.5 2.5A.5.5 0 0 1 6 10.5v-5a.5.5 0 0 1 .271-.445z'/%3E%3C/svg%3E");
}
-}
.nav-group-header {
font-size: 0.7rem;
diff --git a/samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj b/samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj
index 4fa4846c34..b3f499746c 100644
--- a/samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj
+++ b/samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj
@@ -33,6 +33,7 @@
+
diff --git a/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor.cs b/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor.cs
index 4319c6a20c..779863b50c 100644
--- a/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor.cs
+++ b/source/SkiaSharp.Extended.UI.Blazor/Controls/SKAnimatedCanvasView.razor.cs
@@ -40,14 +40,14 @@ public partial class SKAnimatedCanvasView : ComponentBase, IAsyncDisposable
/// previous frame. Use this to update animation state from a parent component.
///
[Parameter]
- public EventCallback OnUpdate { get; set; }
+ public Action? OnUpdate { get; set; }
///
/// Callback invoked each time the canvas needs to be redrawn.
/// Subscribe here to render your content onto the .
///
[Parameter]
- public EventCallback OnPaintSurface { get; set; }
+ public Action? OnPaintSurface { get; set; }
///
/// Additional HTML attributes to be applied to the underlying
@@ -82,22 +82,15 @@ protected override async Task OnParametersSetAsync()
///
/// Time elapsed since the previous frame.
/// A representing the asynchronous update work.
- protected virtual async Task UpdateAsync(TimeSpan deltaTime)
+ protected virtual Task UpdateAsync(TimeSpan deltaTime)
{
- if (OnUpdate.HasDelegate)
- await OnUpdate.InvokeAsync(deltaTime);
+ OnUpdate?.Invoke(deltaTime);
+ return Task.CompletedTask;
}
private void HandlePaintSurface(SKPaintSurfaceEventArgs e)
{
- if (!OnPaintSurface.HasDelegate)
- return;
-
- var task = OnPaintSurface.InvokeAsync(e);
- if (!task.IsCompletedSuccessfully)
- throw new InvalidOperationException(
- $"{nameof(OnPaintSurface)} handlers must be synchronous; " +
- "the underlying SKCanvas is only valid during the paint callback.");
+ OnPaintSurface?.Invoke(e);
}
private async Task StartLoopAsync()
diff --git a/source/SkiaSharp.Extended.UI.Blazor/Controls/SKLottieView.razor.cs b/source/SkiaSharp.Extended.UI.Blazor/Controls/SKLottieView.razor.cs
index d1fc4a1b73..f9b8b1e413 100644
--- a/source/SkiaSharp.Extended.UI.Blazor/Controls/SKLottieView.razor.cs
+++ b/source/SkiaSharp.Extended.UI.Blazor/Controls/SKLottieView.razor.cs
@@ -179,16 +179,16 @@ private async Task LoadAnimationAsync()
StateHasChanged();
}
- private async Task HandleUpdate(TimeSpan delta)
+ private void HandleUpdate(TimeSpan delta)
{
var wasComplete = _player.IsComplete;
_player.Update(delta);
if (_player.IsComplete && !wasComplete)
- await AnimationCompleted.InvokeAsync();
+ _ = AnimationCompleted.InvokeAsync();
if (AnimationUpdated.HasDelegate)
- await AnimationUpdated.InvokeAsync();
+ _ = AnimationUpdated.InvokeAsync();
}
private void HandlePaintSurface(SKPaintSurfaceEventArgs e)