public async Task MoveAsync(
        Point target,
        double duration,
        Action onUpdate,
        CancellationToken cancellationToken)
    {
        // MoveAsync uses a Phaser tween to change the x/y position of the
        // sprite. Use a TaskCompletionSource to wait until the tween is
        // done.
        var tcs = new TaskCompletionSource();

        // When the tween has updated the sprite, check if cancellation
        // is requested, and inform the caller of the new position.
        var onUpdateHandler = new PhaserCallback <Point, bool>(
            pointerPosition =>
        {
            Position = pointerPosition;

            onUpdate();

            return(!cancellationToken.IsCancellationRequested);
        });

        // When the tween has completed, set the result on the
        // TaskCompletionSource to allow this method to run to completion.
        var onCompleteHandler = new PhaserCallback <Point>(
            pointerPosition =>
        {
            Position = pointerPosition;

            tcs.SetResult();
        });

        // Create DotNetObjectReference instances for the callbacks. These
        // need to be disposed once we're done with them.
        using var onUpdateRef   = DotNetObjectReference.Create(onUpdateHandler);
        using var onCompleteRef = DotNetObjectReference.Create(onCompleteHandler);

        // Start the tween.
        _jsRuntime.InvokeVoid(
            PhaserConstants.Functions.MoveSprite,
            Key,
            target.X,
            target.Y,
            duration,
            onUpdateRef,
            onCompleteRef);

        // Wait for tween to complete.
        await tcs.Task;
    }
    public async Task StartCameraFollowAsync(ISprite sprite)
    {
        var tcs = new TaskCompletionSource();

        var onCompleteHandler = new PhaserCallback(tcs.SetResult);

        // Create DotNetObjectReference instances for the callbacks. These
        // need to be disposed once we're done with them.
        using var onCompleteRef = DotNetObjectReference.Create(onCompleteHandler);

        // Start the tween.
        _jsInProcessRuntime.InvokeVoid(
            "startCameraFollow",
            sprite.Key,
            onCompleteRef);

        await tcs.Task;
    }
    public async Task <IGraphics> CreateAsync(
        GameManifest manifest,
        int width,
        int height,
        Action <Point> onDraw,
        string containerElementId)
    {
        TaskCompletionSource <IGraphics> tcs = new();
        List <IDisposable> disposables       = new();

        // When the Phaser scene preloads, load the sprite atlasses from the
        // game manifest.
        var onPreloadHandler = new PhaserCallback(
            () =>
        {
            foreach (var atlas in manifest.Spec.Atlasses)
            {
                _jsInProcessRuntime.InvokeVoid(
                    PhaserConstants.Functions.LoadAtlas,
                    atlas.Key,
                    manifest.BasePath + atlas.Value.TextureUrl,
                    manifest.BasePath + atlas.Value.AtlasUrl);
            }
        });

        // When the Phaser scene is created, complete the task by setting
        // a new PhaserGraphics as the task result.
        var onCreateHandler = new PhaserCallback(
            () =>
        {
            var graphics = new PhaserGraphics(
                width,
                height,
                disposables,
                _jsInProcessRuntime);

            tcs.SetResult(graphics);
        });

        var onUpdateHandler = new PhaserCallback <Point>(onDraw);

        // Create DotNetObjectReference instances for the callbacks. These
        // need to be disposed once we're done with them.
        using var onPreloadRef = DotNetObjectReference.Create(onPreloadHandler);
        using var onCreateRef  = DotNetObjectReference.Create(onCreateHandler);

        // We cannot dispose the onUpdate callback yet, it will be used until
        // the PhaserGraphics object is disposed.
        var onUpdateRef = DotNetObjectReference.Create(onUpdateHandler);

        disposables.Add(onUpdateRef);

        _jsInProcessRuntime.InvokeVoid(
            PhaserConstants.Functions.StartPhaser,
            containerElementId,
            width,
            height,
            onPreloadRef,
            onCreateRef,
            onUpdateRef);

        return(await tcs.Task);
    }