Lesson 28 · Animation

Animation with Timer & GDI+

Build smooth, interactive animations in VB 2026 — the bouncing ball, multiple moving objects, a scrolling starfield, collision detection, and easing curves — all using the Timer + Paint event loop you mastered in Lessons 26 and 27.

Key Takeaway: Every VB animation follows the same three-step loop: (1) update object positions/state variables, (2) call picBox.Invalidate(), (3) redraw everything inside the Paint event. Never draw outside Paint. Module-level variables hold the current position and velocity of each object. The Timer fires the loop at your chosen frame rate — 50 ms (≈20 fps) is smooth enough for most school projects; 16 ms (≈60 fps) is fluid game-quality motion.
Animation loop
Pattern
Tick → update state → Invalidate() → Paint → draw state. Repeat.
Velocity (dx, dy)
Integer / Single
Pixels to move per frame. Positive = right/down. Negative = left/up.
Boundary check
Pattern
If x < 0 Or x+w > width Then dx = -dx — reverse on wall hit.
g.Clear()
Method
Erase previous frame before drawing new positions. First line of Paint.
Collision detection
Pattern
Rectangle.IntersectsWith() tests overlap between two bounding boxes.
Easing
Math pattern
Gradually change velocity toward a target for smooth acceleration/deceleration.
Parallax scroll
Pattern
Background layers scroll at different speeds — far objects move slower.
Sprite (class)
Class / Structure
Bundle x, y, dx, dy, color, size into a class for each moving object.

28.1 The Animation Loop

All GDI+ animation uses the same pattern: a Timer fires at your target frame rate, updates the positions of all objects, then calls Invalidate(). The Paint event redraws everything from scratch — first clearing the canvas with g.Clear(), then drawing each object at its new position.

AnimationLoop.vb — Visual Basic 2026
' --- Module-level state ---
Private _x  As Integer = 10   ' current X position
Private _y  As Integer = 10   ' current Y position
Private _dx As Integer = 4    ' velocity: pixels per frame (right)
Private _dy As Integer = 3    ' velocity: pixels per frame (down)
Private Const RADIUS As Integer = 20

' --- Step 1: Form_Load — start the loop ---
Private Sub Form_Load(...) Handles MyBase.Load
    Timer1.Interval = 16   ' ~60 fps
    Timer1.Start()
End Sub

' --- Step 2: Tick — update positions ---
Private Sub Timer1_Tick(...) Handles Timer1.Tick
    ' Move the ball
    _x += _dx
    _y += _dy

    ' Bounce off left / right walls
    If _x - RADIUS < 0                   Then _dx = Math.Abs(_dx)
    If _x + RADIUS > picBox.Width        Then _dx = -Math.Abs(_dx)

    ' Bounce off top / bottom walls
    If _y - RADIUS < 0                   Then _dy = Math.Abs(_dy)
    If _y + RADIUS > picBox.Height       Then _dy = -Math.Abs(_dy)

    picBox.Invalidate()   ' request a repaint
End Sub

' --- Step 3: Paint — draw current state ---
Private Sub picBox_Paint(...) Handles picBox.Paint
    Dim g = e.Graphics
    g.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias

    g.Clear(Color.MidnightBlue)   ' erase previous frame

    Using brush As New SolidBrush(Color.OrangeRed)
        g.FillEllipse(brush, _x - RADIUS, _y - RADIUS, RADIUS * 2, RADIUS * 2)
    End Using
End Sub
Never Draw Outside the Paint Event

Calling picBox.CreateGraphics().FillEllipse(…) outside Paint works once but is erased the moment Windows repaints the control (on resize, minimize/restore, or any overlay). Always store state in variables and redraw everything in Paint — this is the only way to get stable, flicker-free animation.

Try It — Simulation 28.1: Bouncing Ball

A single ball bounces around the canvas. Adjust speed and size, then watch how the Tick handler updates position and the Paint event redraws. The code panel updates live with each frame.

Bouncing Ball — Timer1.Interval = 16
Speed (dx,dy):
Radius:
Color:

28.2 Multiple Objects — the Sprite Class

When animating many objects, bundle position, velocity, size, and colour into a class or structure. Store all instances in a List(Of Sprite), iterate in Tick to update, and iterate again in Paint to draw.

Sprites.vb — Visual Basic 2026
' --- Sprite class ---
Public Class Sprite
    Public X, Y, Dx, Dy, Radius As Single
    Public Clr As Color

    Public Sub New(x%, y%, dx%, dy%, r%, c As Color)
        Me.X = x : Me.Y = y : Me.Dx = dx : Me.Dy = dy
        Me.Radius = r : Me.Clr = c
    End Sub

    Public Sub Update(W%, H%)
        X += Dx : Y += Dy
        If X - Radius < 0  Then Dx = Math.Abs(Dx)
        If X + Radius > W  Then Dx = -Math.Abs(Dx)
        If Y - Radius < 0  Then Dy = Math.Abs(Dy)
        If Y + Radius > H  Then Dy = -Math.Abs(Dy)
    End Sub

    Public Sub Draw(g As Graphics)
        Using b As New SolidBrush(Clr)
            g.FillEllipse(b, X - Radius, Y - Radius, Radius * 2, Radius * 2)
        End Using
    End Sub

    Public ReadOnly Property Bounds As RectangleF
        Get
            Return New RectangleF(X - Radius, Y - Radius, Radius * 2, Radius * 2)
        End Get
    End Property
End Class

' --- Form code ---
Private _sprites As New List(Of Sprite)
Private _rng     As New Random()

Private Sub Form_Load(...) Handles MyBase.Load
    For i = 1 To 8
        _sprites.Add(New Sprite(
            _rng.Next(20, picBox.Width  - 20),
            _rng.Next(20, picBox.Height - 20),
            _rng.Next(-5, 5),
            _rng.Next(-5, 5),
            _rng.Next(8, 24),
            Color.FromArgb(_rng.Next(128, 255), _rng.Next(50,255),
                           _rng.Next(50,255), _rng.Next(50,255))))
    Next
    Timer1.Interval = 16
    Timer1.Start()
End Sub

Private Sub Timer1_Tick(...) Handles Timer1.Tick
    For Each s In _sprites
        s.Update(picBox.Width, picBox.Height)
    Next
    picBox.Invalidate()
End Sub

Private Sub picBox_Paint(...) Handles picBox.Paint
    Dim g = e.Graphics
    g.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
    g.Clear(Color.FromArgb(20, 20, 40))
    For Each s In _sprites
        s.Draw(g)
    Next
End Sub
Try It — Simulation 28.2: Multiple Sprites

Add up to 20 randomised sprites. Each has its own position, velocity, radius, and colour — all managed by the same For Each s In _sprites loop in Tick and Paint.

Multiple Sprites — List(Of Sprite)
Sprite count:
Trail effect:

28.3 Collision Detection

Check whether two bounding rectangles overlap using RectangleF.IntersectsWith(). For circle-circle collisions, compare the distance between centres to the sum of their radii.

Collision.vb — Visual Basic 2026
' --- Rectangle (AABB) collision ---
Dim ballRect   As New RectangleF(ballX - r, ballY - r, r * 2, r * 2)
Dim paddleRect As New RectangleF(paddleX, paddleY, paddleW, paddleH)

If ballRect.IntersectsWith(paddleRect) Then
    ballDy = -Math.Abs(ballDy)   ' bounce upward
End If

' --- Circle-circle collision ---
Function CirclesCollide(ax%, ay%, ar%, bx%, by%, br%) As Boolean
    Dim dx = ax - bx
    Dim dy = ay - by
    Dim dist = Math.Sqrt(dx * dx + dy * dy)
    Return dist < ar + br
End Function

' --- Check all sprite pairs ---
For i = 0 To _sprites.Count - 2
    For j = i + 1 To _sprites.Count - 1
        Dim a = _sprites(i) : Dim b = _sprites(j)
        If CirclesCollide(a.X, a.Y, a.Radius, b.X, b.Y, b.Radius) Then
            ' simple elastic-ish response: swap velocities
            Dim tmpDx = a.Dx : Dim tmpDy = a.Dy
            a.Dx = b.Dx : a.Dy = b.Dy
            b.Dx = tmpDx : b.Dy = tmpDy
            a.Clr = Color.White   ' flash on hit
            b.Clr = Color.White
        End If
    Next
Next

' --- Simple breakout: ball hits a brick ---
For i = _bricks.Count - 1 To 0 Step -1
    If ballRect.IntersectsWith(_bricks(i)) Then
        _bricks.RemoveAt(i)     ' destroy brick
        _ballDy = -_ballDy      ' reverse vertical direction
        _score += 10
    End If
Next
Try It — Simulation 28.3: Collision Detection

Balls flash white when they collide. Toggle between rectangle (AABB) and circle-circle detection. Watch the distance formula fire in real time and see each collision logged.

Collision Detection Demo
Mode:
Balls:
Collisions: 0

28.4 Scrolling Backgrounds and Parallax

A scrolling background loops an offset variable: increment it each Tick, reset when it exceeds the image/tile width. For a parallax effect, use two or more background layers scrolling at different speeds — distant layers move slower than near ones.

Scroll.vb — Visual Basic 2026
' --- Infinite horizontal scroll ---
Private _scrollX As Integer = 0
Private Const SCROLL_SPEED = 3

Private Sub Timer1_Tick(...) Handles Timer1.Tick
    _scrollX -= SCROLL_SPEED              ' move left
    If _scrollX < -picBox.Width Then _scrollX = 0   ' seamless loop
    picBox.Invalidate()
End Sub

Private Sub picBox_Paint(...) Handles picBox.Paint
    Dim g = e.Graphics
    g.Clear(Color.Black)
    ' Draw background twice side-by-side for seamless wrap
    g.DrawImage(bgImage, _scrollX, 0)
    g.DrawImage(bgImage, _scrollX + picBox.Width, 0)
End Sub

' --- Parallax: 3 layers at different speeds ---
Private _fg As Integer = 0   ' foreground  — fast
Private _mg As Integer = 0   ' midground   — medium
Private _bg As Integer = 0   ' sky/backdrop — slow

Private Sub Timer1_Tick(...) Handles Timer1.Tick
    _fg = (_fg - 6) Mod picBox.Width   ' fastest — foreground hills
    _mg = (_mg - 3) Mod picBox.Width   ' mid — distant trees
    _bg = (_bg - 1) Mod picBox.Width   ' slowest — sky/clouds
    picBox.Invalidate()
End Sub

' --- Starfield: random star positions scroll downward ---
Private _stars(99) As PointF

Private Sub InitStars()
    Dim rng As New Random()
    For i = 0 To 99
        _stars(i) = New PointF(rng.Next(0, picBox.Width),
                                 rng.Next(0, picBox.Height))
    Next
End Sub

Private Sub Timer1_Tick(...) Handles Timer1.Tick
    For i = 0 To 99
        _stars(i).Y += 1 + i Mod 3   ' different speeds = depth illusion
        If _stars(i).Y > picBox.Height Then _stars(i).Y = 0
    Next
    picBox.Invalidate()
End Sub
Try It — Simulation 28.4: Scrolling Starfield & Parallax

A procedural starfield with three depth layers scrolling at different speeds — creating a parallax depth illusion. Toggle warp speed to increase velocity across all layers simultaneously.

Scrolling Starfield — Parallax Layers
Star count:
Direction:

28.5 Easing and Smooth Transitions

Easing makes animations feel natural. Instead of jumping to a target position instantly, the object moves faster at the start and slows as it approaches the target — or accelerates from rest. The simplest technique is lerp (linear interpolation): move a fraction of the remaining distance each frame.

Easing.vb — Visual Basic 2026
' --- Lerp (ease-out): move 12% of remaining distance per frame ---
Private _currentX As Single = 0
Private _targetX  As Single = 400

Private Sub Timer1_Tick(...) Handles Timer1.Tick
    _currentX += (_targetX - _currentX) * 0.12F   ' ease-out
    picBox.Invalidate()
End Sub

' --- Ease-in (accelerate from rest) ---
Private _vel As Single = 0
Private Const GRAVITY As Single = 0.5F

Private Sub Timer1_Tick(...) Handles Timer1.Tick
    _vel += GRAVITY        ' accelerate (ease-in)
    _y   += _vel
    If _y > GROUND Then
        _y   = GROUND
        _vel = -_vel * 0.7F  ' bounce with 30% energy loss
    End If
    picBox.Invalidate()
End Sub

' --- Common easing functions (normalised t = 0 → 1) ---
Function EaseInQuad  (t As Single) As Single : Return t * t         : End Function
Function EaseOutQuad (t As Single) As Single : Return t * (2 - t)    : End Function
Function EaseInOutQuad(t As Single) As Single
    Return If(t < 0.5F, 2 * t * t, -1 + (4 - 2 * t) * t)
End Function

' Usage: position = start + (target - start) * EaseOutQuad(t)
' where t increments from 0 to 1 over the animation duration
Try It — Simulation 28.5: Easing & Gravity

Click to set a new target and watch the ball ease toward it. Switch between ease-out (lerp), ease-in-out (smooth), and gravity+bounce. The easing curve is drawn alongside the ball's path.

Easing & Physics Demo
Mode:
Speed factor:
Click on the canvas to set the target position.

28.6 GitHub Copilot — Mini Breakout Game

GitHub Copilot — Mini Breakout / Brick-breaker
You typed: ' Build a mini Breakout game: paddle controlled by keyboard left/right arrows, ball bounces off walls and paddle, bricks in a 10×5 grid, score increments on each brick hit, Game Over when ball falls below paddle.'
' Mini Breakout — complete game in one form file.
' State
Private _bx, _by, _bdx, _bdy As Integer          ' ball pos + velocity
Private _px As Integer                             ' paddle X
Private Const PW = 80, PH = 12, BR = 8
Private _bricks As New List(Of Rectangle)
Private _score  As Integer
Private _alive  As Boolean = True
Private _keys(255) As Boolean

Private Sub Form_Load(...) Handles MyBase.Load
    InitGame()
    Timer1.Interval = 16
    Timer1.Start()
End Sub

Private Sub InitGame()
    _bx = picBox.Width \ 2 : _by = picBox.Height - 60
    _bdx = 4 : _bdy = -4
    _px = picBox.Width \ 2 - PW \ 2
    _score = 0 : _alive = True
    _bricks.Clear()
    For row = 0 To 4
        For col = 0 To 9
            _bricks.Add(New Rectangle(10 + col * 60, 20 + row * 20, 54, 14))
        Next
    Next
End Sub

Private Sub Timer1_Tick(...) Handles Timer1.Tick
    If Not _alive Then Return
    ' Move paddle
    If _keys(Keys.Left)  Then _px = Math.Max(0, _px - 6)
    If _keys(Keys.Right) Then _px = Math.Min(picBox.Width - PW, _px + 6)
    ' Move ball
    _bx += _bdx : _by += _bdy
    ' Wall bounces
    If _bx - BR < 0 Or _bx + BR > picBox.Width  Then _bdx = -_bdx
    If _by - BR < 0                                Then _bdy = -_bdy
    ' Paddle bounce
    Dim ballR As New Rectangle(_bx - BR, _by - BR, BR * 2, BR * 2)
    Dim padR  As New Rectangle(_px, picBox.Height - PH - 6, PW, PH)
    If ballR.IntersectsWith(padR) And _bdy > 0 Then _bdy = -_bdy
    ' Brick collisions
    For i = _bricks.Count - 1 To 0 Step -1
        If ballR.IntersectsWith(_bricks(i)) Then
            _bricks.RemoveAt(i) : _bdy = -_bdy : _score += 10
        End If
    Next
    ' Game over
    If _by > picBox.Height Then _alive = False
    picBox.Invalidate()
End Sub

Private Sub picBox_Paint(...) Handles picBox.Paint
    Dim g = e.Graphics
    g.Clear(Color.MidnightBlue)
    If Not _alive Then
        Using f As New Font("Segoe UI", 20, FontStyle.Bold)
            g.DrawString($"GAME OVER — Score: {_score}", f,
                         Brushes.Red, 80, picBox.Height \ 2 - 20)
        End Using
        Return
    End If
    ' Draw bricks (colour by row)
    Dim rowColors = {Color.Red, Color.Orange, Color.Yellow, Color.Lime, Color.Cyan}
    For Each b In _bricks
        Dim row = (b.Y - 20) \ 20
        g.FillRectangle(New SolidBrush(rowColors(row)), b)
        g.DrawRectangle(Pens.Black, b)
    Next
    ' Draw paddle and ball
    g.FillRectangle(Brushes.White, _px, picBox.Height - PH - 6, PW, PH)
    g.FillEllipse(Brushes.OrangeRed, _bx - BR, _by - BR, BR * 2, BR * 2)
    g.DrawString($"Score: {_score}", New Font("Segoe UI", 9), Brushes.White, 4, 4)
End Sub

Private Sub Form_KeyDown(...) Handles MyBase.KeyDown
    _keys(e.KeyCode) = True
End Sub
Private Sub Form_KeyUp(...) Handles MyBase.KeyUp
    _keys(e.KeyCode) = False
End Sub
Copilot Chat Prompts for This Lesson

Try these in the Copilot Chat panel:

  • "Add a particle explosion effect: when a brick is destroyed, spawn 8–12 small particles with random velocities that fade out over 20 frames"
  • "Implement double buffering using a Bitmap as a back-buffer: draw all objects to the Bitmap in Tick, then draw the Bitmap to e.Graphics in Paint — eliminates flicker on slow machines"
  • "Add keyboard-controlled player sprite with WASD movement, clamped to the canvas bounds, with a trail of ghost images fading behind it"
  • "Draw a sine wave that animates by incrementing a phase offset each Tick: g.DrawLines with a Point array computed from Math.Sin(x * freq + phase)"

Lesson Summary

  • The animation loop is always: Tick → update state variables (x, y, dx, dy) → Invalidate() → Paint → g.Clear() → draw everything at new positions. Never draw outside the Paint handler.
  • Use module-level variables (not local variables) to hold the position and velocity of every animated object — they must survive between Tick calls.
  • Reverse a velocity component (dx = -dx) when an object hits a boundary. Use Math.Abs(dx) to ensure the direction is correct and prevent tunnelling through walls.
  • Bundle each object's state into a Sprite class with X, Y, Dx, Dy, Radius, Clr and Update(W, H) / Draw(g) methods. Store all sprites in a List(Of Sprite).
  • AABB collisionRectangle.IntersectsWith() — is fast and sufficient for most games. For circles, use the distance formula: Math.Sqrt(dx² + dy²) < r1 + r2.
  • Scrolling: increment an offset each Tick, reset when it exceeds the tile width. For parallax, apply different speeds to each layer — distant layers move slower.
  • Easing (lerp): current += (target − current) × factor — gives natural-feeling motion without complex physics. Gravity simulation: add a constant to _vy each Tick, reverse with energy loss on ground hit.
  • Target 16 ms interval (≈60 fps) for fluid games; 50 ms (≈20 fps) is smooth enough for demos. Keep Tick handlers short — heavy work should go on a background thread.

Exercises

Exercise 28.1 — Gravity & Bouncing Balls

  • Create 5 balls, each starting at a different horizontal position and a random height.
  • Apply gravity (_vy += 0.5) each Tick. When the ball hits the bottom, reverse vertical velocity with 30% energy loss (_vy *= -0.7).
  • Balls should eventually come to rest on the floor (velocity too small to bounce again).
  • Add a "Drop" button that resets all balls to random heights and restores full bounce.
  • Copilot challenge: "Use a LinearGradientBrush to draw each ball with a specular highlight — white at top-left fading to the ball colour at bottom-right"

Exercise 28.2 — Pong Clone

  • Two paddles (left: W/S keys, right: Up/Down keys) and a ball.
  • Ball bounces off top/bottom walls and paddles. When it exits left or right, increment the opponent's score and reset.
  • Draw the score at the top centre with DrawString. Show "PLAYER 1 WINS!" when either side reaches 5.
  • Add a centre dashed dividing line with DashStyle.Dash.
  • Copilot challenge: "Increase the ball speed by 5% every time it hits a paddle — reset speed on score"

Exercise 28.3 — Animated Sine Wave

  • Draw a sine wave using DrawLines and a Point() array computed from Math.Sin(x * frequency + phase).
  • Animate it by incrementing phase each Tick — the wave appears to travel horizontally.
  • Add sliders/NumericUpDown controls for amplitude, frequency, and speed.
  • Show a second wave offset by π/2 (cosine) in a different colour.
  • Copilot challenge: "Add a Lissajous figure mode: draw a parametric curve where x = A·sin(aT + δ), y = B·sin(bT), animated by incrementing T each frame"

Next: Lesson 29 — Database Introduction

Connect VB 2026 to a real database — ADO.NET, SQL Server / SQLite, DataGridView, and CRUD operations.

Continue »

Related Resources


Featured Books

Visual Basic 2022 Made Easy

Visual Basic 2022 Made Easy

by Dr. Liew Voon Kiong

Animation and game projects with GDI+ and Timer control.

View on Amazon →
VB Programming With Code Examples

VB Programming With Code Examples

by Dr. Liew Voon Kiong

Worked animation programs from bouncing balls to simple games.

View on Amazon →