DrawFiddle▶ RunEdit in Fiddle
// Breakout — steel walls carve chambers out of the brick field, broken bricks drop powerups
const int cols = 37, rows = 33;          // odd cols so a 1-cell corridor sits dead centre          // 8 more brick rows, the extra bricks stack above the cross
const double fieldH = 488;               // fills the canvas now that the labels are gone
const double paddleY = 450;              // paddle sits lower
const double brickW = 9, brickH = 9;     // a corridor must stay wider than the 8 dip ball
const double wallTop = 6;                // brick block starts at the top edge
const double fieldW = cols * brickW;
const double basePaddleW = 64, paddleH = 8;
const double ballR = 4;
const double baseSpeed = 260;
const double brickBite = 0.45;      // a brick bounce always sends the ball off at 27 degrees or more
const double paddleSpread = 0.55;   // middle of the paddle answers straight
const double paddleCore = 0.6;      // 20-60-20: the middle 60% is the soft zone
const double paddleEdge = 1.1;      // the outer fifths kick like a brick does
const double dropSpeed = 130;
const double dropChance = 0.6;      // powerup drop rate per destroyed brick, 0..1
const int maxBalls = 64;
const int maxDrops = 8;             // no new powerup while this many are already falling

// 5x7 pixel font for the powerup captions
var glyphs = new Dictionary<char, string[]>();
glyphs['W'] = new[] { "1...1", "1...1", "1...1", "1.1.1", "1.1.1", "11111", "1...1" };
glyphs['3'] = new[] { "1111.", "....1", "....1", ".111.", "....1", "....1", "1111." };
glyphs['x'] = new[] { ".....", ".....", "1...1", ".1.1.", "..1..", ".1.1.", "1...1" };
glyphs['+'] = new[] { ".....", "..1..", "..1..", "11111", "..1..", "..1..", "....." };

var strength = new int[cols, rows];   // 0 = gone, 1 = brick, -1 = steel
var balls = new List<double[]>();     // x, y, vx, vy
var drops = new List<double[]>();     // x, y, kind: 0 = x3, 1 = +3, 2 = W
var alive = 0;
var score = 0;
var lives = 3;
var over = false;
var won = false;
var wideLeft = 0.0;                   // seconds of wide paddle left
double paddleW = basePaddleW;
double paddleX = fieldW / 2, paddleTo = fieldW / 2;
var held = true;                    // the ball rides the paddle until it is served
var keyDir = 0;                       // -1 / +1 while an arrow key is held
var rnd = new Random();


// angle 0 = straight up, positive = to the right
void AddBall(double x, double y, double angle)
{
    if (balls.Count >= maxBalls) return;
    balls.Add(new[] { x, y, Math.Sin(angle) * baseSpeed, -Math.Cos(angle) * baseSpeed });
}

void BuildWall()
{
    for (int c = 0; c < cols; c++)
        for (int r = 0; r < rows; r++)
            strength[c, r] = 1;

    var mid = cols / 2;       // cols is odd, so this is the true centre column
    var arm = 12;             // centre to a side corridor
    var left = mid - arm;
    var right = mid + arm;    // symmetric by construction
    var sideHalf = 7;         // half length of a side corridor
    var spineArm = 9;         // spine above the bar

    // centre the H in the brick block, the leg then runs on down to the entrance
    var bar = 1 + ((rows - 2) - (spineArm + sideHalf)) / 2 + spineArm;
    var spineTop = bar - spineArm;
    var sideTop = bar - sideHalf;   // sides are centred on the bar
    var sideBot = bar + sideHalf;

    for (int c = 0; c < cols; c++)
    {
        strength[c, 0] = -1;
        strength[c, rows - 1] = (c == mid) ? 0 : -1;   // entrance, one brick wide
    }
    for (int r = 0; r < rows; r++)
    {
        strength[0, r] = -1;
        strength[cols - 1, r] = -1;
    }

    // empty H corridors — spine runs down to the row above the bottom steel, it is the way in
    for (int r = spineTop; r <= rows - 2; r++)
        strength[mid, r] = 0;

    for (int c = left; c <= right; c++)
        strength[c, bar] = 0;

    for (int r = sideTop; r <= sideBot; r++)
    {
        strength[left, r] = 0;
        strength[right, r] = 0;
    }

    // steel lining around the H, including the last brick row
    for (int c = 1; c < cols - 1; c++)
    {
        for (int r = 1; r <= rows - 2; r++)
        {
            if (strength[c, r] != 1) continue;
            if (strength[c - 1, r] == 0 || strength[c + 1, r] == 0 ||
                strength[c, r - 1] == 0 || strength[c, r + 1] == 0)
                strength[c, r] = -1;
        }
    }

    // open the top ends of the side corridors — strip the steel caps back to bricks
    strength[left, sideTop - 1] = 1;
    strength[right, sideTop - 1] = 1;

    // steel cap over the top of the spine, 3 bricks wide
    for (int c = mid - 1; c <= mid + 1; c++)
        strength[c, spineTop - 1] = -1;

    // bottoms of the side corridors: steel shoulders, open in the middle
    for (int c = -1; c <= 1; c++)
    {
        strength[left + c, sideBot + 1] = c == 0 ? 0 : -1;
        strength[right + c, sideBot + 1] = c == 0 ? 0 : -1;
    }

    alive = 0;
    for (int c = 0; c < cols; c++)
        for (int r = 0; r < rows; r++)
            if (strength[c, r] == 1) alive++;
}

void Restart()
{
    BuildWall();
    balls.Clear();
    drops.Clear();
    AddBall(fieldW / 2, paddleY - ballR - 1, 0);
    held = true;
    score = 0;
    lives = 3;
    over = false;
    won = false;
    wideLeft = 0;
    paddleW = basePaddleW;
    paddleX = fieldW / 2;
    paddleTo = paddleX;
    keyDir = 0;
}

void Apply(int kind)
{
    if (kind == 0)                        // x3 — every ball splits into three
    {
        foreach (var b in balls.ToList())
        {
            var a = Math.Atan2(b[2], -b[3]);
            AddBall(b[0], b[1], a - 0.4);
            AddBall(b[0], b[1], a + 0.4);
        }
    }
    else if (kind == 1)                   // +3 — three fresh balls off the paddle
    {
        for (int i = -1; i <= 1; i++)
            AddBall(paddleX, paddleY - ballR - 1, i * 0.4);
    }
    else                                  // W — wider paddle for 5s
    {
        wideLeft = 5;
    }
}

bool HitCell(double x, double y)
{
    var c = (int)Math.Floor(x / brickW);
    var r = (int)Math.Floor((y - wallTop) / brickH);
    if (c < 0 || c >= cols || r < 0 || r >= rows) return false;

    var st = strength[c, r];
    if (st == 0) return false;
    if (st < 0) return true;        // steel bounces the ball and stays

    strength[c, r] = 0;
    score += 10;
    alive--;
    if (drops.Count < maxDrops && rnd.NextDouble() < dropChance)
        drops.Add(new double[] { c * brickW + brickW / 2, wallTop + r * brickH, rnd.Next(3) });
    return true;
}

void Serve()
{
    if (!held || balls.Count == 0) return;
    held = false;
    var a = (rnd.NextDouble() - 0.5) * 0.5;
    balls[0][2] = Math.Sin(a) * baseSpeed;
    balls[0][3] = -Math.Cos(a) * baseSpeed;
}

void Step(double dt)
{
    if (over || won) return;
    if (dt > 0.05) dt = 0.05;

    // held arrow key drives the target at a constant speed, key repeat is not involved
    if (keyDir != 0)
        paddleTo += keyDir * 380 * dt;

    wideLeft = Math.Max(0, wideLeft - dt);
    paddleW = wideLeft > 0 ? basePaddleW * 1.7 : basePaddleW;
    paddleTo = Math.Clamp(paddleTo, paddleW / 2, fieldW - paddleW / 2);

    // pointer events land ~16/s, the canvas draws 60/s: chase the target so the paddle glides
    paddleX += (paddleTo - paddleX) * Math.Min(1.0, dt * 22);

    if (held && balls.Count > 0)
    {
        balls[0][0] = paddleX;
        balls[0][1] = paddleY - ballR - 1;
        balls[0][2] = 0;
        balls[0][3] = 0;
    }

    for (int i = drops.Count - 1; i >= 0; i--)
    {
        var d = drops[i];
        d[1] += dropSpeed * dt;

        if (d[1] > paddleY - 7 && d[1] < paddleY + paddleH + 7 && Math.Abs(d[0] - paddleX) < paddleW / 2 + 12)
        {
            Apply((int)d[2]);
            drops.RemoveAt(i);
        }
        else if (d[1] > fieldH)
        {
            drops.RemoveAt(i);
        }
    }

    for (int i = balls.Count - 1; i >= 0; i--)
    {
        var b = balls[i];
        var dist = Math.Sqrt(b[2] * b[2] + b[3] * b[3]) * dt;
        var steps = Math.Max(1, (int)Math.Ceiling(dist / 2.0));   // small steps, no tunneling
        var sdt = dt / steps;
        var lost = false;

        for (int s = 0; s < steps; s++)
        {
            b[0] += b[2] * sdt;
            b[1] += b[3] * sdt;

            if (b[0] < ballR) { b[0] = ballR; b[2] = Math.Abs(b[2]); }
            if (b[0] > fieldW - ballR) { b[0] = fieldW - ballR; b[2] = -Math.Abs(b[2]); }
            if (b[1] < ballR) { b[1] = ballR; b[3] = Math.Abs(b[3]); }

            if (b[3] > 0 && b[1] + ballR >= paddleY && b[1] - ballR <= paddleY + paddleH
                && b[0] >= paddleX - paddleW / 2 - ballR && b[0] <= paddleX + paddleW / 2 + ballR)
            {
                b[1] = paddleY - ballR;
                var off = Math.Clamp((b[0] - paddleX) / (paddleW / 2), -1.0, 1.0);
                var ang = off * paddleSpread;
                if (Math.Abs(off) > paddleCore)
                {
                    // caught on a shoulder: sharp answer, same floor a brick bounce uses
                    ang = off * paddleEdge;
                    var least = Math.Asin(brickBite);
                    if (Math.Abs(ang) < least) ang = (ang < 0 ? -1 : 1) * least;
                }
                var sp = Math.Sqrt(b[2] * b[2] + b[3] * b[3]);
                b[2] = Math.Sin(ang) * sp;
                b[3] = -Math.Cos(ang) * sp;
            }

            // bricks do not bounce the ball back flat: the component that flips gets a minimum
            // share of the speed, so a grazing hit in a corridor comes off at a real angle
            if (HitCell(b[0] + Math.Sign(b[2]) * ballR, b[1]))
            {
                b[2] = -b[2];
                var sp = Math.Sqrt(b[2] * b[2] + b[3] * b[3]);
                var least = sp * brickBite;
                if (Math.Abs(b[2]) < least)
                {
                    var sgn = b[2] < 0 ? -1 : 1;
                    b[2] = sgn * least;
                    b[3] = (b[3] < 0 ? -1 : 1) * Math.Sqrt(Math.Max(0, sp * sp - least * least));
                }
            }
            else if (HitCell(b[0], b[1] + Math.Sign(b[3]) * ballR))
            {
                b[3] = -b[3];
                var sp = Math.Sqrt(b[2] * b[2] + b[3] * b[3]);
                var least = sp * brickBite;
                if (Math.Abs(b[3]) < least)
                {
                    var sgn = b[3] < 0 ? -1 : 1;
                    b[3] = sgn * least;
                    b[2] = (b[2] < 0 ? -1 : 1) * Math.Sqrt(Math.Max(0, sp * sp - least * least));
                }
            }

            if (alive <= 0) { won = true; return; }

            if (b[1] - ballR > fieldH) { lost = true; break; }
        }

        if (lost) balls.RemoveAt(i);
    }

    if (balls.Count == 0)
    {
        lives--;
        if (lives <= 0) over = true;
        else
        {
            AddBall(paddleX, paddleY - ballR - 1, 0);   // next ball waits on the paddle
            held = true;
        }
    }
}

Restart();

return new SkiaStack()
{
    Spacing = 10,
    BackgroundColor = Color.Parse("#12161F"),
    Padding = new Thickness(16),
    Children = new List<SkiaControl>()
    {
        // the frame paints the board background, the child layer paints the game on top
        new SkiaShape()
        {
            CornerRadius = 10,
            BackgroundColor = Color.Parse("#0B0E14"),
            WidthRequest = fieldW,
            HeightRequest = fieldH,
            Children = new List<SkiaControl>()
            {
                new SkiaLayer()
                .Fill()
                .WhenPaint((me, ctx) =>
                {
                    var canvas = ctx.Context.Canvas;
                    var scale = ctx.Scale;
                    var dest = ctx.Destination;

                    float X(double v) => (float)(dest.Left + v * scale);
                    float Y(double v) => (float)(dest.Top + v * scale);
                    float S(double v) => (float)(v * scale);

                    using var paint = new SKPaint { IsAntialias = true, Style = SKPaintStyle.Fill };

                    void Glyph(char ch, float gx, float gy, float ps)
                    {
                        var g = glyphs[ch];
                        for (int r = 0; r < 7; r++)
                            for (int c = 0; c < 5; c++)
                                if (g[r][c] == '1') canvas.DrawRect(gx + c * ps, gy + r * ps, ps, ps, paint);
                    }

                    var yellow = SKColor.Parse("#FFD43B");
                    var steel = SKColor.Parse("#4A5568");

                    for (int c = 0; c < cols; c++)
                    {
                        for (int r = 0; r < rows; r++)
                        {
                            var st = strength[c, r];
                            if (st == 0) continue;
                            paint.Color = st < 0 ? steel : yellow;
                            canvas.DrawRect(new SKRect(
                                X(c * brickW + 1.5), Y(wallTop + r * brickH + 1.5),
                                X(c * brickW + brickW - 1.5), Y(wallTop + r * brickH + brickH - 1.5)), paint);
                        }
                    }

                    foreach (var d in drops)
                    {
                        var kind = (int)d[2];
                        paint.Color = kind == 0 ? SKColor.Parse("#5EE7C4")
                            : kind == 1 ? SKColor.Parse("#2E7BF6") : yellow;
                        canvas.DrawRoundRect(new SKRect(
                            X(d[0] - 13), Y(d[1] - 8), X(d[0] + 13), Y(d[1] + 8)), S(4), S(4), paint);

                        var label = kind == 0 ? "x3" : kind == 1 ? "+3" : "W";
                        var ps = S(1.5);
                        paint.Color = SKColor.Parse("#0B0E14");
                        var gx = X(d[0]) - (label.Length * 6 * ps - ps) / 2;
                        for (int i = 0; i < label.Length; i++) Glyph(label[i], gx + i * 6 * ps, Y(d[1]) - 3.5f * ps, ps);
                    }

                    paint.Color = SKColor.Parse("#FF6B4A");
                    canvas.DrawRoundRect(new SKRect(
                        X(paddleX - paddleW / 2), Y(paddleY),
                        X(paddleX + paddleW / 2), Y(paddleY + paddleH)), S(4), S(4), paint);

                    paint.Color = SKColors.White;
                    foreach (var b in balls) canvas.DrawCircle(X(b[0]), Y(b[1]), S(ballR), paint);
                })
                .WithGestures((me, args, apply) =>
                {
                    if (args.Type == TouchActionResult.Down || args.Type == TouchActionResult.Panning)
                    {
                        if (over || won)
                        {
                            Restart();
                            return me;
                        }
                        if (held) Serve();
                        var local = (args.Event.Location.X - me.DrawingRect.Left) / me.RenderingScale;
                        paddleTo = Math.Clamp(local, paddleW / 2, fieldW - paddleW / 2);
                        return me;
                    }
                    return null;
                })
                .Animate(1.0, (me, animator, value, dt) =>
                {
                    Step(dt);
                    me.Update();
                }, repeat: -1),
            }
        }
        .CenterX(),
    }
}
.OnKeyDown((me, key) =>
{
    if (over || won)
    {
        if (key == InputKey.Space || key == InputKey.Enter) Restart();
        return;
    }
    if (key == InputKey.Space || key == InputKey.Enter || key == InputKey.ArrowUp) Serve();
    if (key == InputKey.ArrowLeft) keyDir = -1;
    else if (key == InputKey.ArrowRight) keyDir = 1;
})
.OnKeyUp((me, key) =>
{
    if (key == InputKey.ArrowLeft && keyDir < 0) keyDir = 0;
    else if (key == InputKey.ArrowRight && keyDir > 0) keyDir = 0;
});