Skip to content

Extract SKLottiePlayer and add Blazor Lottie support#365

Open
Copilot wants to merge 31 commits intomainfrom
copilot/extract-skia-kottie-player
Open

Extract SKLottiePlayer and add Blazor Lottie support#365
Copilot wants to merge 31 commits intomainfrom
copilot/extract-skia-kottie-player

Conversation

Copy link
Contributor

Copilot AI commented Feb 26, 2026

  • Remove duplicate CONTROLS group header in NavMenu.razor (both Pixel Comparer and Lottie now appear under one header)
  • Remove extra closing brace in NavMenu.razor.css .bi-play-circle-nav-menu rule
  • Add explicit ProjectReference to SkiaSharp.Extended core in SkiaSharpDemo.Blazor.csproj (alongside SkiaSharp.Extended.UI.Blazor)
  • Change SKAnimatedCanvasView.OnUpdate from EventCallback<TimeSpan> to Action<TimeSpan>?
  • Change SKAnimatedCanvasView.OnPaintSurface from EventCallback<SKPaintSurfaceEventArgs> to Action<SKPaintSurfaceEventArgs>?
  • Update SKLottieView.HandleUpdate to void to match new Action<TimeSpan> signature; use fire-and-forget for EventCallback invocations

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI changed the title [WIP] Investigate lottie view and extract SKSkottiePlayer Extract SKSkottiePlayer for cross-platform Lottie playback (MAUI + Blazor) Feb 26, 2026
github-actions bot pushed a commit that referenced this pull request Feb 26, 2026
Copilot AI changed the title Extract SKSkottiePlayer for cross-platform Lottie playback (MAUI + Blazor) Extract SKLottiePlayer: share Lottie playback logic across MAUI and Blazor Feb 26, 2026
Copy link
Collaborator

@mattleibow mattleibow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot due to a somewhat unfortunate naming, the maui project has a assembly name of SkiaSharp.Extended.UI. And we also can't move the enum the way it is as the namespace is a breaking change.

Please move the enum back to the maui project.

We could make a new unified SKLottieRepeat readonly struct that has a private ctor and static methods - SKLottieRepeat.Never, SKLottieRepeat.Reverse(int count = -1), SKLottieRepeat.Restart(int count = -1).

internally we can have a private enum maybe. And then we can expose bool properties IsRepeating and IsReverseRepeating/IsRestartRepeating.

Copy link
Contributor Author

Copilot AI commented Feb 26, 2026

@copilot due to a somewhat unfortunate naming, the maui project has a assembly name of SkiaSharp.Extended.UI. And we also can't move the enum the way it is as the namespace is a breaking change.

P...

Done in 3d1b22a.

  • SKLottieRepeatMode enum is back in MAUI (SkiaSharp.Extended.UI.Controls, SkiaSharp.Extended.UI assembly) — no namespace/assembly breaking change.
  • New SKLottieRepeat readonly struct in core (SkiaSharp.Extended) replaces the enum for SKLottiePlayer. It has a private constructor, static factories (Never, Restart(count = -1), Reverse(count = -1)), and bool properties (IsRepeating, IsRestartRepeating, IsReverseRepeating) plus Count.
  • SKLottiePlayer.Repeat (single SKLottieRepeat property) replaces the old separate RepeatMode + RepeatCount properties.
  • SKLottieView.Update() bridges the MAUI RepeatMode+RepeatCount bindable properties into a SKLottieRepeat for the player.
  • MAUI sample XAML uses controls:SKLottieRepeatMode again (no extended: xmlns needed).
  • Blazor sample uses SKLottieRepeat.Restart()/SKLottieRepeat.Reverse() directly.

player.Repeat = RepeatMode == SKLottieRepeatMode.Reverse
? SKLottieRepeat.Reverse(RepeatCount)
: SKLottieRepeat.Restart(RepeatCount);
player.AnimationSpeed = AnimationSpeed;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Why are we passing in things here and all that with the syc things. Can we not use the bindable property updates to the player? And maybe we can have an AnimationUpdate event in player to pass things back p to the view? Lets try design this better so we don't need the "locks
or "guard fields"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 963a708. The design is now fully event-driven with no guard fields:

Settings → Player (via propertyChanged callbacks):

  • RepeatModeProperty / RepeatCountPropertyOnRepeatPropertyChangedplayer.Repeat = ...
  • AnimationSpeedPropertyOnAnimationSpeedPropertyChangedplayer.AnimationSpeed = ...

State → View (via AnimationUpdated event):

  • SKLottiePlayer.Progress setter raises AnimationUpdated after every change
  • View's OnPlayerAnimationUpdated sets Duration, Progress, IsComplete from the player

Re-entry prevention (no guard field):

  • OnProgressPropertyChanged checks player.Progress == newProgress and returns early if already in sync (covers the case where AnimationUpdated triggers a bindable set that would otherwise loop back into the player)
  • MAUI's own SetValue equality check also prevents re-firing for unchanged values

Update() is now a single line:

protected override void Update(TimeSpan deltaTime) => player.Update(deltaTime);

/// and may trigger completion logic (repeats, AnimationCompleted event).
/// Raises <see cref="AnimationUpdated"/> after each change.
/// </summary>
public TimeSpan Progress
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot maybe this can be a getter only style? The callers can just use UpdateProgress to indicate things are happening.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in abd8c6a. Progress is now getter-only (public TimeSpan Progress => _progress) and a public Seek(TimeSpan position) method is the single entry point for changing the playback position. All internal callers (Update, Reset, restart-repeat loop) use Seek(), and SKLottieView.OnProgressPropertyChanged calls player.Seek(newProgress) instead of assigning the property.


private void Reset()
{
isResetting = true;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot do we need this isResetting field?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 6c74782. Reset() now sets _progress, Duration, IsComplete, isInForwardPhase, and repeatsCompleted directly without going through Seek(), so the guard flag isn't needed. Also fixed a related bug where AnimationCompleted re-fired on every Update() call after completion — it now only fires on the false → true transition.

Copy link
Collaborator

@mattleibow mattleibow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please make sure you are doing good things here. The lottie player needs tests. the player should not be designed around the view, instead the API should be clean, neat and be self-contained.

Make sure you can write tests for this, maybe even start with the tests for the maui view.

Lets make properties simple and getter ownly. The methods can be the ones to trigger complex logic.

mattleibow and others added 18 commits March 4, 2026 02:36
- 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>
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>
- 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>
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>
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>
Only reset isInForwardPhase when switching away from Reverse mode (to
Restart/Never), not when changing Reverse count. This prevents an
unwanted direction flip mid-playback while still guarding against the
Reverse→Restart freeze bug.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. SKAnimatedCanvasView now calls Invalidate() on the inner SKCanvasView
   to force canvas repaint. StateHasChanged() alone doesn't trigger
   SkiaSharp canvas repainting in Blazor WASM.

2. Move isInForwardPhase reset from Repeat setter to the Restart branch
   of UpdateProgress. Direction now only changes when the animation
   actually hits a boundary, not when the Repeat property changes.
   This prevents abrupt mid-animation direction flips while still
   fixing the Reverse->Restart freeze bug.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add lottie-player.md: platform-agnostic SKLottiePlayer docs under
  SkiaSharp.Extended with frame loop, speed, repeat, seeking, events
- Add blazor-lottie.md: SKLottieView and SKAnimatedCanvasView for
  Blazor WebAssembly with quick start, controls, events, sizing
- Rework lottie.md: focus on MAUI, add speed/direction section, add
  cross-links to Player and Blazor docs, remove inline player docs
- Update toc.yml: add Lottie Player, new Blazor section, fix hrefs
- All three docs cross-link to each other

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename lottie.md → lottie-maui.md and blazor-lottie.md → lottie-blazor.md
for consistent naming. Update all cross-links and toc.yml.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Nest Lottie and Confetti under 'Controls' subgroup for both
MAUI and Blazor sections in toc.yml for cleaner navigation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move Lottie under a 'CONTROLS' section header in the sidebar nav
and add a Controls card group on the homepage, separating it from
the core Extended utility demos. Revert docs toc.yml back to flat.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- 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>
The polling loop was dying because async bash sessions without
detach:true are killed on session idle. Added critical note about
using detach:true and documented PID-based termination.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add CancellationTokenSource to LoadAnimationAsync to prevent race
  conditions when Source changes rapidly (stale out-of-order results)
- Cancel in-flight HTTP requests on dispose to prevent resource leaks
  and post-dispose callbacks
- Clear player animation reference in DisposeAsync before disposing
  the native handle to prevent use-after-free during teardown

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add poll_loop.sh for continuous detached background polling
- Restructure SKILL.md with cost optimization & agent architecture
  (cheap monitoring agent + main agent escalation pattern)
- Update poll_comments.sh (minor trailing newline fix)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Simplified architecture: main agent launches a cheap gpt-5-mini background
agent that runs poll_comments.sh in an inline bash loop (30 iterations,
60s interval). No more detached poll_loop.sh process.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Align Blazor RepeatCount default to 0 (match MAUI play-once behavior)
- Fix use-after-free in LoadAnimationAsync: clear player before disposing
- Fix MAUI AnimationCompleted event: sync IsComplete/Progress/Duration
  before firing, so subscribers see consistent state
- Fix broken lottie.md link in docs/index.md (renamed to lottie-maui.md)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow
Copy link
Collaborator

Looking into this — testing testing

@mattleibow mattleibow force-pushed the copilot/extract-skia-kottie-player branch from b397e71 to 2caa4d5 Compare March 4, 2026 00:37
@mattleibow
Copy link
Collaborator

Done ✅ — Rebased on latest origin/main (picked up commit c30e453d1 — pr-monitor skill merge from #375). Resolved skill file conflicts (kept latest gist version). All 160 tests pass. Pushed 2caa4d5ba.

@mattleibow mattleibow marked this pull request as ready for review March 4, 2026 00:37
github-actions bot pushed a commit that referenced this pull request Mar 4, 2026
Copy link
Collaborator

@mattleibow mattleibow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please adrees each of my comments in this review. Also, please ensure the projects build and all tests pass.

Comment on lines +35 to +37

<div class="nav-group-header px-3">CONTROLS</div>

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be removed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6c7ba12. Removed the duplicate CONTROLS group header — both Pixel Comparer and Lottie now appear under a single CONTROLS heading.

.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");
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate closing brace?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ba12. Removed the extra } after .bi-play-circle-nav-menu.

Comment on lines -35 to +36
<ProjectReference Include="..\..\source\SkiaSharp.Extended\SkiaSharp.Extended.csproj" />
<ProjectReference Include="..\..\source\SkiaSharp.Extended.UI.Blazor\SkiaSharp.Extended.UI.Blazor.csproj" />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need both

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6c7ba12. Added an explicit ProjectReference to SkiaSharp.Extended core alongside SkiaSharp.Extended.UI.Blazor.

/// previous frame. Use this to update animation state from a parent component.
/// </summary>
[Parameter]
public EventCallback<TimeSpan> OnUpdate { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be Action of T

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6c7ba12. Changed OnUpdate to Action<TimeSpan>?. Updated SKLottieView.HandleUpdate to match the new void signature.

/// Subscribe here to render your content onto the <see cref="SKCanvas"/>.
/// </summary>
[Parameter]
public EventCallback<SKPaintSurfaceEventArgs> OnPaintSurface { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be Action of T

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6c7ba12. Changed OnPaintSurface to Action<SKPaintSurfaceEventArgs>?. The synchronous-only constraint is now enforced by the type system rather than a runtime check.

…oject ref, change EventCallback to Action<T> for SKAnimatedCanvasView

Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot please adrees each of my comments in this review. Also, please ensure the projects build and all tests pass.

All review comments addressed in 6c7ba12. Build succeeds and the 2 Blazor unit tests pass.

Security summary: CodeQL found 0 alerts.

github-actions bot pushed a commit that referenced this pull request Mar 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants