DrawFiddle▶ RunEdit in Fiddle
// Blackjack vs the house. Tap Deal, then Hit or Stand. Dealer stands on 17.
// Card faces are real SVGs from github.com/MattCain/svg-playing-cards (MIT), pulled off the
// CDN the first time a card appears and cached, so a round downloads only what it deals.
const string cdn = "https://cdn.jsdelivr.net/gh/MattCain/svg-playing-cards@master/imgs/";
const double W = 380, H = 470;
// the deck's svgs draw their own white body + keyline; we strip that and draw the card here,
// so cardPad is ours to set. artH keeps the remaining art at its native 167.09 x 242.67.
const double cardW = 70, cardPad = 3;
const double artW = cardW - cardPad * 2;
const double artH = artW * 242.6669922 / 167.0869141;
const double cardH = artH + cardPad * 2, spread = 38;
const double discardX = -cardW - 20, discardY = -cardH * 0.3;   // spent cards leave past the top-left
const double dealerY = 60, playerY = 210, deckX = W - 62, deckY = 18, sideX = 32;
const double dealDur = 0.26, flipDur = 0.18, outDur = 0.34;
const double sweepStagger = 0.01;    // gap between one card leaving and the next in the same row
const double sweepRowGap = 0.0;     // extra pause before the dealer's row follows the player's
const int pool = 12;

// the three button rectangles, used both to draw them and to hit-test taps
const double btnY = 358, btnH = 42;
const double btnAX = W / 2 - 60, btnAW = 120;      // Deal   / Hit
const double btnBX = W / 2 + 70, btnBW = 100;      // bet +25 / Stand
const double btnCX = W / 2 - 170, btnCW = 100;     // bet -25

var rankName = new string[] { "ace", "2", "3", "4", "5", "6", "7", "8", "9", "10", "jack", "queen", "king" };
var suitName = new string[] { "hearts", "diamonds", "clubs", "spades" };
// file name for a card id, e.g. 10 of hearts -> "10_of_hearts"
string Name(int card) => rankName[card % 13] + "_of_" + suitName[card / 13];

var art = new Dictionary<int, string>();     // card id (-1 = back) -> svg markup
var pending = new HashSet<int>();
var http = new System.Net.Http.HttpClient();

// SkiaSvg parses with Encoding.ASCII, so non-ascii bytes (this deck's metadata has plenty)
// would turn into '?' inside the markup — strip them before they ever get there
string Ascii(string svg)
{
    var sb = new System.Text.StringBuilder(svg.Length);
    foreach (var ch in svg) if (ch < 128) sb.Append(ch);
    return sb.ToString();
}

// drops the card outline the deck bakes in, leaving pips and figures on transparency
string StripFrame(string svg)
{
    const string key = "fill:#FFFFFF;stroke-width:0.5;";
    var k = svg.IndexOf(key);
    if (k < 0) return svg;
    var start = svg.LastIndexOf("<path", k);
    var end = svg.IndexOf("/>", k);
    if (start < 0 || end < 0) return svg;
    return svg.Remove(start, end + 2 - start);
}

// downloads one card's svg once and caches it; -1 is the shared back
async void Fetch(int card)
{
    if (art.ContainsKey(card) || !pending.Add(card)) return;
    try
    {
        var svg = await http.GetStringAsync(cdn + (card < 0 ? "back" : Name(card)) + ".svg");
        art[card] = card < 0 ? svg : Ascii(StripFrame(svg));
    }
    catch (Exception e)
    {
        Console.WriteLine("card art failed: " + e.Message);
    }
    finally
    {
        pending.Remove(card);
    }
}

var rnd = new Random();
var deck = new List<int>();
var dealer = new List<int>();
var player = new List<int>();

int bank = 500, bet = 50, phase = 0;      // 0 bet, 1 dealing, 2 player, 3 dealer, 4 payout
double wait = 0;
string msg = "place your bet";

var qOwner = new List<int>();             // pending deals: 0 dealer, 1 player
var qUp = new List<bool>();

var slotCard = new int[pool];
var slotOwner = new int[pool];
var slotSeat = new int[pool];
var slotAnim = new double[pool];
var slotFlip = new double[pool];
var slotUp = new bool[pool];
var slotUsed = new bool[pool];
var shown = new int[pool];
var backSet = new bool[pool];
var slotOut = new double[pool];
var slotLeaving = new bool[pool];        // on its way off the table
var slotDelay = new double[pool];        // its turn in the right-to-left sweep
var slotFromX = new double[pool];        // where it sat when the sweep began
var slotFromY = new double[pool];
int dealSeq = 0;                         // every dealt card gets a higher z than the one before it          // 0 = on the table, otherwise how far it has been swept away
var slotJitterX = new double[pool];      // a dip of scatter so a hand never looks machine-stacked
var slotJitterY = new double[pool];

var vBody = new SkiaShape[pool];
var vFace = new SkiaSvg[pool];
var vBack = new SkiaSvg[pool];

SkiaLabel hudMsg = null, hudBank = null, hudBet = null, hudDealer = null, hudPlayer = null;
SkiaShape btnA = null, btnB = null, btnC = null;
SkiaLabel labA = null, labB = null, labC = null;

// best total for a hand: aces start at 11 and drop to 1 while it would bust
int Val(List<int> h)
{
    int sum = 0, aces = 0;
    foreach (var c in h)
    {
        var r = c % 13;
        sum += r == 0 ? 11 : r >= 9 ? 10 : r + 1;
        if (r == 0) aces++;
    }
    while (sum > 21 && aces > 0) { sum -= 10; aces--; }
    return sum;
}

// a two-card 21, which pays 3:2 and ends the hand at once
bool Natural(List<int> h) => h.Count == 2 && Val(h) == 21;

// where the seat-th card of a hand sits, fanned around the table centre
double SeatX(int seat, int count) => W / 2 + (seat - (count - 1) / 2.0) * spread - cardW / 2;
// dealer row or player row
double SeatY(int owner) => owner == 0 ? dealerY : playerY;

// deals the top card into the first free slot and starts its slide from the deck
void Give(int owner, bool up)
{
    for (int i = 0; i < pool; i++)
    {
        if (slotUsed[i]) continue;
        var card = deck[deck.Count - 1];
        deck.RemoveAt(deck.Count - 1);
        (owner == 0 ? dealer : player).Add(card);
        slotUsed[i] = true;
        slotCard[i] = card;
        slotOwner[i] = owner;
        slotSeat[i] = (owner == 0 ? dealer.Count : player.Count) - 1;
        slotAnim[i] = 0;
        slotFlip[i] = 0;
        slotUp[i] = up;
        slotOut[i] = 0;
        slotLeaving[i] = false;
        slotDelay[i] = 0;
        vBody[i].Opacity = 1;
        vBody[i].ZIndex = ++dealSeq;     // pooled slots are reused out of order, so z follows the deal

        slotJitterX[i] = rnd.NextDouble() * 2 - 1;
        slotJitterY[i] = rnd.NextDouble() * 4 - 2;
        Fetch(card);
        return;
    }
}

// sends every card on the table off past the top-left; slots free themselves when they arrive.
// returns true if anything was actually on the table, so the deal can wait for it to clear
double Sweep()
{
    var going = new List<int>();
    for (int i = 0; i < pool; i++)
        if (slotUsed[i] && !slotLeaving[i]) going.Add(i);
    if (going.Count == 0) return 0;

    // the player's row goes first (they exit upward), and within a row rightmost leads
    going.Sort((a, b) =>
    {
        var row = slotOwner[b].CompareTo(slotOwner[a]);      // owner 1 = player, so it sorts ahead of the dealer
        return row != 0 ? row : vBody[b].TranslationX.CompareTo(vBody[a].TranslationX);
    });
    var at = 0.0;
    for (int k = 0; k < going.Count; k++)
    {
        var i = going[k];
        if (k > 0 && slotOwner[i] != slotOwner[going[k - 1]]) at += sweepRowGap;   // row change
        slotLeaving[i] = true;
        slotDelay[i] = at;
        slotUp[i] = false;                 // turns face down before it goes
        slotOut[i] = 0;
        slotFromX[i] = vBody[i].TranslationX;
        slotFromY[i] = vBody[i].TranslationY;
        at += sweepStagger;
    }
    return at - sweepStagger + flipDur + outDur;
}

// turns the dealer's hole card face up
void Reveal()
{
    for (int i = 0; i < pool; i++)
        if (slotUsed[i] && slotOwner[i] == 0) slotUp[i] = true;
}

// true while anything is still queued, sliding or flipping — the phase machine waits on it
bool Busy()
{
    if (qOwner.Count > 0) return true;
    for (int i = 0; i < pool; i++)
    {
        if (!slotUsed[i]) continue;
        if (slotAnim[i] < 1) return true;
        if (slotUp[i] && slotFlip[i] < 1) return true;
    }
    return false;
}

// settles the hand; the stake already left the stack, so a win pays it back doubled
void Payout()
{
    var p = Val(player);
    var d = Val(dealer);
    if (p > 21) msg = $"bust — you lose {bet}";
    else if (Natural(player) && !Natural(dealer)) { bank += bet + bet * 3 / 2; msg = $"blackjack! +{bet * 3 / 2}"; }
    else if (d > 21) { bank += bet * 2; msg = $"dealer busts — you win {bet}"; }
    else if (p > d) { bank += bet * 2; msg = $"you win {bet}"; }
    else if (p < d) msg = $"dealer wins {bet}";
    else { bank += bet; msg = "push"; }
    phase = 4;
    wait = 2.0;
}

// takes the stake, shuffles, clears the table and queues the opening four cards
void NewRound()
{
    if (bank < 25) { msg = "out of chips — tap Deal to rebuy"; bank = 500; bet = 50; return; }
    if (bet > bank) bet = bank;
    bank -= bet;                       // the wager sits on the table now

    deck.Clear();
    for (int i = 0; i < 52; i++) deck.Add(i);
    for (int i = deck.Count - 1; i > 0; i--)
    {
        var j = rnd.Next(i + 1);
        (deck[i], deck[j]) = (deck[j], deck[i]);
    }
    dealer.Clear();
    player.Clear();
    var clearing = Sweep();            // last hand leaves first, the deal waits for the table

    qOwner.Clear(); qUp.Clear();
    qOwner.Add(1); qUp.Add(true);
    qOwner.Add(0); qUp.Add(true);
    qOwner.Add(1); qUp.Add(true);
    qOwner.Add(0); qUp.Add(false);      // hole card

    phase = 1;
    wait = clearing;                   // Sweep tells us how long the table takes to clear
    msg = "";
}

// pushes the model onto the controls: art, position, flip state and the hud text
void Sync()
{
    var dCount = dealer.Count;
    var pCount = player.Count;

    for (int i = 0; i < pool; i++)
    {
        var body = vBody[i];
        if (body == null) continue;
        if (!slotUsed[i]) { body.IsVisible = false; continue; }

        var card = slotCard[i];
        if (shown[i] != card)
        {
            if (art.TryGetValue(card, out var svg))
            {
                shown[i] = card;
                vFace[i].SvgString = svg;
                if (vFace[i].Svg?.Picture == null)     // parsed to nothing: blank card, refetch once
                {
                    Console.WriteLine($"art did not parse: {Name(card)}");
                    art.Remove(card);
                    shown[i] = -2;
                }
            }
            else Fetch(card);
        }
        if (!backSet[i] && art.TryGetValue(-1, out var backSvg)) { backSet[i] = true; vBack[i].SvgString = backSvg; }

        body.IsVisible = true;
        if (slotLeaving[i])                // holds its old spot, then slides off past the top-left
        {
            var o = Math.Min(1, slotOut[i]);
            o *= o;
            body.TranslationX = slotFromX[i] + (discardX - slotFromX[i]) * o;
            body.TranslationY = slotFromY[i] + (discardY - slotFromY[i]) * o;
            body.Opacity = 1 - o;
        }
        else
        {
            var e = slotAnim[i] >= 1 ? 1 : slotAnim[i] * slotAnim[i] * (3 - 2 * slotAnim[i]);
            var tx = SeatX(slotSeat[i], slotOwner[i] == 0 ? dCount : pCount) + slotJitterX[i];
            var ty = SeatY(slotOwner[i]) + slotJitterY[i];
            body.TranslationX = deckX + (tx - deckX) * e;
            body.TranslationY = deckY + (ty - deckY) * e;
            body.Opacity = 1;
        }

        var f = slotFlip[i];
        var face = f > 0.5;
        body.ScaleX = Math.Max(0.04, Math.Abs(1 - 2 * f));
        vBack[i].IsVisible = !face;
        vFace[i].IsVisible = face && shown[i] == card;   // stays blank until its art has landed
    }

    if (hudMsg != null) hudMsg.Text = msg;
    // while betting the pending wager is already taken off the displayed stack
    if (hudBank != null) hudBank.Text = $"chips {(phase == 0 ? bank - bet : bank)}";
    if (hudBet != null) hudBet.Text = $"bet {bet}";
    if (hudDealer != null)
    {
        var reveal = true;
        for (int i = 0; i < pool; i++) if (slotUsed[i] && slotOwner[i] == 0 && slotFlip[i] <= 0.5) reveal = false;
        hudDealer.Text = dealer.Count == 0 ? "" : reveal ? $"{Val(dealer)}" : "?";
    }
    if (hudPlayer != null) hudPlayer.Text = player.Count == 0 ? "" : $"{Val(player)}";

    var betting = phase == 0;
    var playing = phase == 2;      // stay on screen while a card lands, so the ripple is visible
    btnA.IsVisible = betting || playing;
    btnB.IsVisible = betting || playing;
    btnC.IsVisible = betting;
    labA.Text = betting ? "Deal" : "Hit";
    labB.Text = betting ? "bet +25" : "Stand";
    labC.Text = "bet -25";
}

// one frame: advance animations, tick the pause timer, then run the phase machine
void Step(double dt)
{
    for (int i = 0; i < pool; i++)
    {
        if (!slotUsed[i]) continue;
        if (slotLeaving[i])
        {
            // leaving is the deal in reverse: wait your turn, turn face down, then slide off
            if (slotDelay[i] > 0) { slotDelay[i] -= dt; continue; }
            if (slotFlip[i] > 0) { slotFlip[i] = Math.Max(0, slotFlip[i] - dt / flipDur); continue; }
            slotOut[i] += dt / outDur;
            if (slotOut[i] >= 1) { slotUsed[i] = false; slotLeaving[i] = false; slotOut[i] = 0; shown[i] = -2; }
            continue;
        }
        if (slotAnim[i] < 1) slotAnim[i] = Math.Min(1, slotAnim[i] + dt / dealDur);
        else if (slotUp[i] && slotFlip[i] < 1 && art.ContainsKey(slotCard[i])) slotFlip[i] = Math.Min(1, slotFlip[i] + dt / flipDur);
        else if (!slotUp[i] && slotFlip[i] > 0) slotFlip[i] = Math.Max(0, slotFlip[i] - dt / flipDur);
    }

    if (wait > 0)
    {
        wait -= dt;
        if (wait > 0) { Sync(); return; }
        wait = 0;
        if (phase == 4) { phase = 0; msg = "place your bet"; }
    }

    if (phase == 1)
    {
        if (qOwner.Count > 0)
        {
            if (wait <= 0)
            {
                Give(qOwner[0], qUp[0]);
                qOwner.RemoveAt(0);
                qUp.RemoveAt(0);
                wait = 0.24;
            }
        }
        else if (!Busy())
        {
            if (Natural(player) || Natural(dealer)) { Reveal(); phase = 3; wait = 0.5; }
            else { phase = 2; msg = "hit or stand"; }
        }
    }
    else if (phase == 2)
    {
        if (!Busy() && Val(player) > 21) { Reveal(); Payout(); }
    }
    else if (phase == 3)
    {
        if (!Busy())
        {
            Reveal();
            if (Val(player) <= 21 && Val(dealer) < 17 && !Natural(player)) { Give(0, true); wait = 0.35; }
            else Payout();
        }
    }

    Sync();
}

// ripple from where the finger landed, in the button's own coordinates
void Ripple(SkiaShape btn, double left, double x, double y)
{
    if (btn != null && btn.IsVisible) btn.PlayRippleAnimation(Color.Parse("#FFFFFF66"), x - left, y - btnY);
}

// hit-tests the button row in table coordinates
void Tap(double x, double y)
{
    if (y < btnY || y > btnY + btnH) return;
    var onA = x > btnAX && x < btnAX + btnAW;
    var onB = x > btnBX && x < btnBX + btnBW;
    var onC = x > btnCX && x < btnCX + btnCW;

    if (phase == 0)
    {
        if (onA) { Ripple(btnA, btnAX, x, y); NewRound(); return; }
        if (onB && bet + 25 <= bank) { Ripple(btnB, btnBX, x, y); bet += 25; return; }
        if (onC && bet > 25) { Ripple(btnC, btnCX, x, y); bet -= 25; return; }
    }
    else if (phase == 2 && !Busy())
    {
        if (onA) { Ripple(btnA, btnAX, x, y); Give(1, true); return; }
        if (onB) { Ripple(btnB, btnBX, x, y); phase = 3; wait = 0.4; msg = ""; }
    }
}

// one pooled card view: white body carrying a face svg and a back svg
SkiaControl Card(int i) => new SkiaShape
{
    CornerRadius = 4,
    StrokeColor = Color.Parse("#8F9791"),
    StrokeWidth = 1,
    BackgroundColor = Colors.White,
    WidthRequest = cardW,
    HeightRequest = cardH,
    HorizontalOptions = LayoutOptions.Start,
    VerticalOptions = LayoutOptions.Start,
    IsVisible = false,    
    UseCache = SkiaCacheType.Image,
    Children = new List<SkiaControl>
    {
        new SkiaSvg
        {
            Margin = cardPad,
            WidthRequest = artW,
            HeightRequest = artH,
        }.Assign(out vFace[i]),

        new SkiaSvg
        {
            WidthRequest = cardW,
            HeightRequest = cardH,
        }.Assign(out vBack[i]),
    }
}.Assign(out vBody[i]);

// a button visual only — the overlay below owns the input
SkiaControl Button(double left, double width, string caption, out SkiaShape shape, out SkiaLabel label) => new SkiaShape
{
    CornerRadius = 24,
    BackgroundColor = Color.Parse("#19563E"),
    WidthRequest = width,
    HeightRequest = btnH,
    HorizontalOptions = LayoutOptions.Start,
    VerticalOptions = LayoutOptions.Start,
    Margin = new Thickness(left, btnY, 0, 0),
    Children = new List<SkiaControl>
    {
        new SkiaLabel(caption)
        {
            FontSize = 14,
            FontWeight = 600,
            TextColor = Colors.White,
        }.Center().Assign(out label),
    }
}.Assign(out shape);

// our own back instead of the deck's plain blue hatch
string Back()
{
    const string ink = "#14493A", trim = "#FDFDFB", emblem = "#E9C46A";
    var s = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 140'>";
    s += "<rect width='100' height='140' rx='7' fill='" + trim + "'/>";
    s += "<clipPath id='c'><rect x='3' y='3' width='94' height='134' rx='5'/></clipPath>";
    s += "<g clip-path='url(#c)'><rect x='3' y='3' width='94' height='134' fill='" + ink + "'/>";
    s += "<g stroke='#ffffff2e' stroke-width='2'>";
    for (int i = -140; i <= 100; i += 9) s += "<line x1='" + i + "' y1='0' x2='" + (i + 140) + "' y2='140'/>";
    for (int i = 0; i <= 240; i += 9) s += "<line x1='" + i + "' y1='0' x2='" + (i - 140) + "' y2='140'/>";
    s += "</g></g>";
    s += "<rect x='3' y='3' width='94' height='134' rx='5' fill='none' stroke='" + trim + "' stroke-width='2'/>";
    s += "<ellipse cx='50' cy='70' rx='21' ry='29' fill='" + ink + "' stroke='" + emblem + "' stroke-width='2'/>";
    s += "<path d='M50 50 L63 70 L50 90 L37 70 Z' fill='" + emblem + "'/>";
    s += "<circle cx='50' cy='70' r='5' fill='" + ink + "'/>";
    return s + "</svg>";
}

art[-1] = Back();

// prefetch the whole deck so a card never reaches its flip before its art does
for (int i = 0; i < 52; i++) Fetch(i);

// main layout
return new SkiaShape
{
    CornerRadius = 14,
    HorizontalOptions = LayoutOptions.Center,
    VerticalOptions = LayoutOptions.Center,
    BackgroundColor = Color.Parse("#123A2C"),
    WidthRequest = W,
    HeightRequest = H,
    Children = new List<SkiaControl>
    {
        // table
        new SkiaShape
        {
            CornerRadius = 24,
            BackgroundColor = Color.Parse("#174A38"),
            StrokeColor = Color.Parse("#0B2A20"),
            StrokeWidth = 3,
            WidthRequest = W - 24,
            HeightRequest = 300,
            Margin = new Thickness(12, 26, 0, 0),
            HorizontalOptions = LayoutOptions.Start,
            VerticalOptions = LayoutOptions.Start,
        },

        new SkiaLabel("")
        {
            FontSize = 15,
            FontWeight = 600,
            TextColor = Color.Parse("#CFE8DC"),
            Margin = new Thickness(0, 176, 0, 0),
        }.CenterX().Assign(out hudMsg),

        new SkiaLabel("")
        {
            FontSize = 15,
            FontWeight = 700,
            TextColor = Color.Parse("#FFD98A"),
            Margin = new Thickness(sideX, dealerY, 0, 0),
            HorizontalOptions = LayoutOptions.Start,
        }.Assign(out hudDealer),

        new SkiaLabel("")
        {
            FontSize = 15,
            FontWeight = 700,
            TextColor = Color.Parse("#FFD98A"),
            Margin = new Thickness(sideX, playerY, 0, 0),
            HorizontalOptions = LayoutOptions.Start,
        }.Assign(out hudPlayer),

        new SkiaLayer
        {
            Children = Enumerable.Range(0, pool).Select(i => Card(i)).ToList()
        }.Fill(),

        Button(btnAX, btnAW, "Deal", out btnA, out labA),
        Button(btnBX, btnBW, "bet +25", out btnB, out labB),
        Button(btnCX, btnCW, "bet -25", out btnC, out labC),

        new SkiaLabel("chips 500")
        {
            FontSize = 13,
            TextColor = Color.Parse("#CFE8DC"),
            Margin = new Thickness(sideX, 420, 0, 0),
            HorizontalOptions = LayoutOptions.Start,
        }.Assign(out hudBank),

        new SkiaLabel("bet 50")
        {
            FontSize = 13,
            TextColor = Color.Parse("#CFE8DC"),
            Margin = new Thickness(0, 420, sideX, 0),
            HorizontalOptions = LayoutOptions.End,
        }.Assign(out hudBet),

        new SkiaLayer()
            .Fill()
            .WithGestures((me, args, apply) =>
            {
                if (args.Type == TouchActionResult.Tapped)
                {
                    Tap((args.Event.Location.X - me.DrawingRect.Left) / me.RenderingScale,
                        (args.Event.Location.Y - me.DrawingRect.Top) / me.RenderingScale);
                    return me;
                }
                if (args.Type == TouchActionResult.Down) return me;
                return null;
            })
            .Animate(1, (me, animator, value, dt) =>
            {
                Step(Math.Min(dt, 0.05));
                me.Update();
            }, repeat: -1),
    }
};