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.
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.
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.
' --- 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
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.
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.
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.
' --- 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
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.
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.
' --- 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
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.
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.
' --- 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
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.
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.
' --- 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
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.
28.6 GitHub Copilot — Mini Breakout Game
' 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
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. UseMath.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, ClrandUpdate(W, H)/Draw(g)methods. Store all sprites in aList(Of Sprite). - AABB collision —
Rectangle.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_vyeach 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
DrawLinesand aPoint()array computed fromMath.Sin(x * frequency + phase). - Animate it by incrementing
phaseeach 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"
Related Resources
← Lesson 27
Timer — the animation heartbeat.
← Lesson 26
GDI+ drawing — Paint, Pen, Brush.
Lesson 29 →
Database connections with ADO.NET.
easings.net
Visual reference for all easing curves with formulas.
Featured Books
Visual Basic 2022 Made Easy
Animation and game projects with GDI+ and Timer control.
View on Amazon →
VB Programming With Code Examples
Worked animation programs from bouncing balls to simple games.
View on Amazon →