This application displays a single ball that moves smoothly across the screen using delta‑time‑based animation.
When the program starts, the ball is placed at the exact center of the window. A random direction is chosen, and the ball begins traveling across the client area at a constant speed.
The movement is updated inside a high‑frequency timer, which calculates how much real time has passed between frames. This ensures the animation stays smooth and consistent even if the frame rate changes.
As the ball travels, it checks for collisions with the edges of the window. When the ball reaches any boundary - left, right, top, or bottom - it “bounces” by reversing the appropriate velocity component. This creates natural, physics‑style motion where the ball ricochets around the screen indefinitely.
The app also includes a simple FPS counter drawn directly onto the form. It updates once per second and shows how many frames were rendered, giving a clear visual indicator of performance.
This project is designed to teach:
- How animation works
- How physics loops work
- How rendering loops work
- How to avoid flicker
- How to optimize without complexity
- How to structure real-time code cleanly
It’s a introduction to game programming concepts using Windows Forms and VB.NET.
A complete, high‑level overview of how the animation engine works, why each subsystem exists, and how they fit together.
This project is a miniature real‑time graphics engine built inside Windows Forms.
It demonstrates smooth animation, delta‑time physics, motion trails, FPS tracking, and high‑quality GDI+ rendering all using simple, readable code.
Below is the full architecture broken into its major subsystems.
The engine stores everything it needs to simulate and draw the ball:
- ballPos - the ball’s current position
- velX / velY - the ball’s velocity components
- ballDiameter - the size of the ball
- speed - the magnitude of the velocity
- trail - a list of past positions for the motion trail
- trailSizes / trailOffsets - precomputed geometry for trail circles
- frameCount / fps - FPS tracking
- physicsTimer / fpsTimer / stopwatch - timing systems
These variables form the backbone of the animation.
The engine initializes itself in two phases:
- Enables double‑buffering
- Sets high‑quality painting flags
- Centers the ball
- Generates a random direction
- Starts the physics and FPS timers
- Creates all GDI+ resources ( brushes, fonts )
- Preallocates trail brushes
- Precomputes trail sizes and offsets
- Starts the physics loop
This ensures the engine is fully prepared before the first frame is drawn.
The physics loop runs every 15 ms ( ~ 66 FPS ):
- Measures delta‑time (
dt) - Clamps dt to avoid teleporting during lag
- Updates ball position
- Handles collisions
- Updates the trail
- Calls
Invalidate()to trigger a redraw
This loop is the heartbeat of the animation.
The engine checks for collisions with all four window edges:
- If the ball hits a wall, snap it back inside
- Reverse the velocity component
- Use
Math.Absto guarantee correct bounce direction
This creates clean, predictable bouncing behavior.
The trail system records the ball’s movement history:
- Adds the current ball position
- Removes the oldest entry when the list exceeds its max length
- Uses precomputed sizes and offsets
- Applies exponential alpha fading
- Draws each trail circle behind the ball
The result is a smooth, tapered comet‑like trail.
Rendering happens inside OnPaint:
- Anti‑aliasing
- High‑quality interpolation
- High‑quality pixel offset
- SourceOver compositing
These ensure the animation looks crisp and professional.
- DrawTrail
- DrawBall
- DrawFPS
This layering keeps the trail behind the ball and the FPS counter on top.
- Suppressed to eliminate flicker
- Lets double‑buffering handle the entire frame
This is essential for smooth animation.
The FPS system measures how many frames occur per second:
- Increment frame count
- If one second passed:
- Set
fps - Reset counter
- Restart stopwatch
- Set
- Draws the FPS text in the top‑left corner
This gives real‑time performance feedback.
When the window is resized:
- Ignore resize until resources exist
- Clamp the ball inside the new bounds
- Recompute trail offsets
- Invalidate the form
This keeps the engine stable and visually correct during window resizing.
When the form closes:
- Dispose all brushes
- Dispose the font
- Dispose the timer
- Dispose each trail brush
This prevents memory leaks and cleans up unmanaged GDI resources.
This animation engine works beautifully because it follows real graphics‑engine principles:
- Fixed timestep physics
- Delta‑time movement
- High‑quality rendering modes
- Double‑buffering everywhere
- Suppressed background painting
- Preallocated GDI resources
- Precomputed geometry
- Clean separation of responsibilities
It’s small, readable, and perfect for learning how real‑time animation works.
A line‑by‑line explanation of how this animation works and why we built it this way.
This project shows how to animate a bouncing ball in Windows Forms using smooth motion, trails, and an FPS counter.
The goal is to teach you how real-time graphics work, using simple, readable code.
Imports System.Drawing.Drawing2D- This line brings in the Drawing2D namespace from .NET’s GDI+ drawing library.
- It gives you access to advanced rendering features such as:
SmoothingModeInterpolationModePixelOffsetModeCompositingMode
- Without this import, you’d have to fully qualify those types (e.g.,
System.Drawing.Drawing2D.SmoothingMode.AntiAlias).
In short: This import enables high‑quality graphics rendering options for your form.
Public Class Form1- This begins the definition of your Windows Forms class.
Form1inherits fromSystem.Windows.Forms.Form( implicitly ).- Everything that follows: fields, methods, event handlers, belongs to this form.
- When the program runs, this class becomes the window the user sees.
In short: This is the main window of your application, and all your animation logic lives inside it.
' -------------------------------
' Engine State
' -------------------------------
Private ballPos As PointF
Private ballDiameter As Integer = 80
Private velX As Double
Private velY As Double
Private speed As Double = 450
Private physicsTimer As New Timer()
Private sw As New Stopwatch()- Comment block (Engine State): Just a visual separator in the code, labeling the next section as “Engine State”.
Private ballPos As PointF: Declares a private field that stores the ball’s current position as a 2D point (X,Y).Private ballDiameter As Integer = 80: Declares the ball’s diameter in pixels and initializes it to80.Private velX As Double: Declares the horizontal velocity component of the ball ( pixels per second ).Private velY As Double: Declares the vertical velocity component of the ball ( pixels per second ).Private speed As Double = 450: Declares a base speed value ( magnitude ) for the ball’s movement and initializes it to450.Private physicsTimer As New Timer(): Creates aTimerthat will drive the physics updates ( the fixed‑timestep loop ).Private sw As New Stopwatch(): Creates aStopwatchused to measure elapsed time between physics ticks ( fordt).
' -------------------------------
' FPS Tracking
' -------------------------------
Private frameCount As Integer = 0
Private fps As Integer = 0
Private fpsTimer As New Stopwatch()- Comment block ( FPS Tracking ): Labels this section as related to frames‑per‑second tracking.
Private frameCount As Integer = 0: Counts how many frames have been rendered in the current one‑second window.Private fps As Integer = 0: Stores the calculated FPS value that will be displayed on screen.Private fpsTimer As New Stopwatch(): Measures time to know when one second has passed so FPS can be updated.
' -------------------------------
' GDI Resources
' -------------------------------
Private ballBrush As SolidBrush
Private fpsBrush As SolidBrush
Private fpsFont As Font
Private trailBrushes As SolidBrush()- Comment block ( GDI Resources ): Marks the section for drawing resources.
Private ballBrush As SolidBrush: Brush used to fill the ball when drawing.Private fpsBrush As SolidBrush: Brush used to draw the FPS text.Private fpsFont As Font: Font used for the FPS text rendering.Private trailBrushes As SolidBrush(): An array of brushes used to draw each segment of the trail with varying alpha.
' -------------------------------
' Trail System
' -------------------------------
Private trail As New List(Of PointF)
Private trailLength As Integer = 25
Private trailSizes As Integer()
Private trailOffsets As Single()- Comment block ( Trail System ): Labels the section for the motion trail behind the ball.
Private trail As New List(Of PointF): A list storing past positions of the ball to render the trail.Private trailLength As Integer = 25: Maximum number of trail points ( segments ) to keep and draw.Private trailSizes As Integer(): An array holding precomputed sizes ( diameters ) for each trail segment.Private trailOffsets As Single(): An array holding precomputed offsets so each trail ellipse is centered relative to the main ball.
Public Sub New()This is the form’s constructor. It runs once, the moment the form is created.
InitializeComponent()Loads everything designed in the Windows Forms Designer: controls, properties, layout, etc.
Every WinForms form calls this first.
Me.SetStyle(ControlStyles.AllPaintingInWmPaint Or
ControlStyles.UserPaint Or
ControlStyles.OptimizedDoubleBuffer,
True)This line enables three important rendering flags:
-
AllPaintingInWmPaint
Prevents background erasing, reducing flicker. -
UserPaint
Tells WinForms that you will handle all painting manually (viaOnPaint). -
OptimizedDoubleBuffer
Enables double‑buffering at the control level, eliminating flicker during animation.
Setting them to True applies the combined flags.
Me.DoubleBuffered = TrueA second layer of double‑buffering.
Even though you already set OptimizedDoubleBuffer, this property ensures the form itself uses a back‑buffer.
Me.BackColor = Color.BlackSets the form’s background color to black — the canvas for your animation.
ballPos = New PointF((ClientSize.Width - ballDiameter) / 2, (ClientSize.Height - ballDiameter) / 2)This computes the centered position:
ClientSize.Width - ballDiameter→ remaining horizontal space- Divide by 2 → center horizontally
- Same logic vertically
Creates a PointF representing the ball’s starting position.
Dim rnd As New Random()Creates a random number generator.
Dim angle As Double = rnd.NextDouble() * Math.PI * 2Generates a random angle between 0 and 2π (full circle).
velX = Math.Cos(angle) * speedComputes the horizontal velocity based on the angle.
velY = Math.Sin(angle) * speedComputes the vertical velocity based on the angle.
Together, these give the ball a random direction with the same speed magnitude.
physicsTimer.Interval = 15Sets the timer to tick every 15 ms, which is ~66 updates per second.
AddHandler physicsTimer.Tick, AddressOf PhysicsTickConnects the timer’s Tick event to your physics update method.
Every tick → PhysicsTick() runs.
sw.Start()Starts the stopwatch used to measure delta‑time (dt) between physics updates.
fpsTimer.Start()Starts the stopwatch used for FPS measurement.
End SubMarks the end of the constructor.
OnLoad runs once, right after the form is created and just before it becomes visible.
This is the perfect place to initialize brushes, fonts, trail arrays, and anything that depends on the form’s size.
Protected Overrides Sub OnLoad(e As EventArgs)This overrides the form’s built‑in OnLoad method.
It fires when the form is fully constructed and ready to initialize runtime resources.
MyBase.OnLoad(e)Calls the base class version of OnLoad.
This ensures WinForms performs its normal setup before your custom logic runs.
ballBrush = New SolidBrush(Color.DeepSkyBlue)
fpsBrush = New SolidBrush(Color.White)
fpsFont = New Font("Segoe UI", 14, FontStyle.Bold)These are the core drawing resources:
- ballBrush — fills the ball with a bright blue color
- fpsBrush — draws the FPS text in white
- fpsFont — sets the font used for the FPS counter
In short: These are the main tools used to draw your scene.
trailBrushes = New SolidBrush(trailLength - 1) {}Creates an array of SolidBrush objects with trailLength entries.
This avoids allocating brushes during animation, which keeps performance smooth.
For i As Integer = 0 To trailLength - 1
trailBrushes(i) = New SolidBrush(Color.FromArgb(0, 0, 191, 255))
NextInitializes each brush with a fully transparent blue color:
- Alpha = 0
- RGB = (0, 191, 255)
The alpha will be changed dynamically during drawing to create the fading trail effect.
In short: Every trail segment gets its own brush, ready to have its alpha adjusted.
trailSizes = New Integer(trailLength - 1) {}
trailOffsets = New Single(trailLength - 1) {}Creates two arrays:
- trailSizes — the diameter of each trail circle
- trailOffsets — how much each circle must be shifted to stay centered
Precomputing these values avoids doing math inside the render loop.
For i As Integer = 0 To trailLength - 1
Dim size As Integer = ballDiameter - (trailLength - i) * 2
If size < 10 Then size = 10This loop calculates the size of each trail segment:
- Older trail segments (lower
i) get smaller circles - Newer trail segments (higher
i) get larger circles - Minimum size is clamped to 10 pixels
This creates a tapered, comet‑like trail behind the ball.
trailSizes(i) = size
trailOffsets(i) = CSng((ballDiameter - size) / 2)
NextStores:
- the computed size
- the offset needed to center the smaller circle inside the ball’s position
The offset is (ballDiameter - size) / 2, which centers the ellipse perfectly.
In short: Each trail circle is smaller and centered relative to the ball.
physicsTimer.Start()Begins the fixed‑timestep physics loop.
From this moment on:
- the ball moves
- collisions are checked
- the trail updates
- the form invalidates and redraws
This is the moment the animation officially starts.
End SubMarks the end of the OnLoad method.
This method runs every 15 ms (≈66 times per second).
It updates the ball’s position, handles collisions, updates the trail, and triggers a redraw.
Private Sub PhysicsTick(sender As Object, e As EventArgs)This is the event handler for the timer’s Tick event.
Every time the timer fires, this method executes one physics update.
Dim dt As Double = sw.Elapsed.TotalSeconds
sw.Restart()sw.Elapsed.TotalSeconds
Reads how much time has passed since the last physics tick.sw.Restart()
Resets and starts the stopwatch again for the next tick.
This gives you a precise delta‑time, which makes movement smooth even if the timer drifts slightly.
In short: dt = time since last update.
dt = Math.Min(dt, 0.05)If the app freezes or lags for a moment, dt could become very large.
Large dt → ball teleports across the screen → breaks physics.
Clamping dt to 0.05 seconds (50 ms) prevents runaway movement.
In short: Never allow dt to exceed a safe maximum.
ballPos.X += CSng(velX * dt)
ballPos.Y += CSng(velY * dt)This applies velocity to position:
velX * dt→ how far the ball moves horizontally this framevelY * dt→ how far the ball moves vertically this frame
CSng() converts the result to Single because PointF uses Single values.
In short: Move the ball based on velocity and elapsed time.
HandleCollisions()Checks whether the ball hit any of the window edges:
- left
- right
- top
- bottom
If so, it adjusts position and reverses velocity.
In short: Bounce the ball off the walls.
UpdateTrail()Adds the ball’s current position to the trail list and removes old entries.
This creates the fading motion trail behind the ball.
In short: Record the ball’s movement history.
Invalidate()Tells WinForms:
“The screen is out of date — please repaint the form.”
This triggers OnPaint, which draws:
- the trail
- the ball
- the FPS counter
In short: PhysicsTick updates the world; OnPaint draws it.
End SubMarks the end of the physics update loop.
This method checks whether the ball has hit any of the four edges of the window.
If it has, the ball is repositioned and its velocity is flipped so it bounces back.
Private Sub HandleCollisions()Begins the collision‑handling routine.
This is called once per physics update.
If ballPos.X <= 0 Then
ballPos.X = 0
velX = Math.Abs(velX)ballPos.X <= 0
The ball’s left edge has reached or passed the left boundary.ballPos.X = 0
Snap the ball back inside the window so it doesn’t drift off‑screen.velX = Math.Abs(velX)
Ensures the horizontal velocity becomes positive, sending the ball to the right.
In short: If the ball hits the left wall, push it right.
ElseIf ballPos.X >= ClientSize.Width - ballDiameter Then
ballPos.X = ClientSize.Width - ballDiameter
velX = -Math.Abs(velX)
End IfballPos.X >= ClientSize.Width - ballDiameter
The ball’s right edge has reached the right boundary.ballPos.X = ClientSize.Width - ballDiameter
Snap it back inside the window.velX = -Math.Abs(velX)
Ensures the horizontal velocity becomes negative, sending the ball left.
In short: If the ball hits the right wall, push it left.
If ballPos.Y <= 0 Then
ballPos.Y = 0
velY = Math.Abs(velY)ballPos.Y <= 0
The ball’s top edge has reached the top boundary.ballPos.Y = 0
Snap it back inside.velY = Math.Abs(velY)
Ensures vertical velocity becomes positive, sending the ball downward.
In short: If the ball hits the top wall, push it down.
ElseIf ballPos.Y >= ClientSize.Height - ballDiameter Then
ballPos.Y = ClientSize.Height - ballDiameter
velY = -Math.Abs(velY)
End IfballPos.Y >= ClientSize.Height - ballDiameter
The ball’s bottom edge has reached the bottom boundary.ballPos.Y = ClientSize.Height - ballDiameter
Snap it back inside.velY = -Math.Abs(velY)
Ensures vertical velocity becomes negative, sending the ball upward.
In short: If the ball hits the bottom wall, push it up.
End SubEnds the collision‑handling routine.
This collision system is:
- simple — no complex physics
- predictable — always snaps the ball inside the bounds
- stable — avoids tunneling thanks to dt clamping
- clean — uses
Math.Absto guarantee correct bounce direction
It’s exactly the kind of logic that is easy to understand while still feeling “real” in motion.
The trail system works by storing the ball’s previous positions in a list.
Each frame, you add the newest position and remove the oldest one once the list reaches its maximum length.
This creates a clean, tapered motion trail behind the ball.
Private Sub UpdateTrail()Begins the trail‑update routine.
This is called once per physics tick, right after the ball moves.
trail.Add(New PointF(ballPos.X, ballPos.Y))This line records the ball’s current position:
- Creates a new
PointFusing the ball’s currentXandY. - Appends it to the end of the
traillist.
In short: Each physics update adds one new trail point.
If trail.Count > trailLength Then
trail.RemoveAt(0)
End IfThis ensures the trail never grows beyond the configured length:
- If the list has more entries than
trailLength(25 by default) - Remove the oldest entry at index
0
This keeps the trail:
- lightweight
- predictable
- visually consistent
In short: Oldest positions are removed so the trail stays exactly the right size.
End SubEnds the trail update routine.
This trail system is intentionally simple:
- No complex math
- No interpolation
- No heavy data structures
- Just a clean FIFO (first‑in, first‑out) list
Combined with our precomputed sizes and alpha fading, it produces a smooth, professional‑looking motion trail with minimal CPU overhead.
OnPaint is the core rendering function.
Every time the form needs to redraw (because Invalidate() was called), WinForms calls this method and gives you a Graphics object to draw with.
Protected Overrides Sub OnPaint(e As PaintEventArgs)Overrides the form’s built‑in painting method.
This is where all custom drawing for your animation happens.
MyBase.OnPaint(e)Calls the base class version of OnPaint.
This ensures WinForms performs its normal painting behavior before your custom graphics run.
Even though you suppress background painting elsewhere, calling the base method is still good practice.
Dim g = e.GraphicsRetrieves the Graphics object from the PaintEventArgs.
This object is your drawing canvas — everything you draw goes through g.
g.CompositingMode = CompositingMode.SourceOverSets how overlapping graphics are blended.
- SourceOver means new pixels are drawn over existing ones.
- This is ideal for trails, transparency, and layered effects.
In short: Allows smooth alpha blending.
g.SmoothingMode = SmoothingMode.AntiAliasEnables anti‑aliasing:
- Smooth edges
- No jagged circles
- Professional‑looking graphics
Perfect for drawing ellipses and curved shapes.
g.PixelOffsetMode = PixelOffsetMode.HighQualityImproves pixel alignment:
- Reduces tiny rendering artifacts
- Makes circles look cleaner
- Helps sub‑pixel drawing look smoother
This is especially useful because your ball moves in floating‑point space.
g.InterpolationMode = InterpolationMode.HighQualityBicubicControls how scaled images are rendered.
Even though you’re not scaling images here, setting this mode ensures:
- smooth gradients
- smooth alpha transitions
- high‑quality rendering overall
It’s part of a “best practice” set of rendering flags for GDI+ animation.
DrawTrail(g)
DrawBall(g)
DrawFPS(g)These three calls render the entire frame:
Draws the fading motion trail behind the ball.
Draws the main ball at its current position.
Draws the FPS counter in the corner.
The order matters:
- Trail first → behind the ball
- Ball second → on top
- FPS last → UI overlay
This layering creates a clean, readable scene.
End SubEnds the rendering routine.
Our rendering setup is:
- clean - separated into small draw functions
- efficient - uses preallocated brushes and precomputed sizes
- high‑quality - uses all the right GDI+ smoothing modes
- flicker‑free - thanks to double‑buffering and suppressed background painting
This is exactly how you build a smooth real‑time animation in WinForms.
The trail is drawn before the ball so it appears underneath it.
Each trail segment is a smaller, more transparent circle, creating a smooth fading effect.
Private Sub DrawTrail(g As Graphics)Begins the trail‑rendering routine.
This method draws all trail segments using the precomputed sizes, offsets, and brushes.
Dim count As Integer = Math.Min(trail.Count, trailLength)trail.Count= how many positions are currently storedtrailLength= maximum allowed trail segments
Math.Min ensures you never draw more than the configured trail length.
In short: Draw only the valid portion of the trail.
For i As Integer = 0 To count - 1Iterates through each trail entry:
i = 0→ oldest trail pointi = count - 1→ newest trail point
This ordering is important because the fade effect depends on i.
Dim t As Double = i / trailLength
Dim alpha As Integer = CInt(32 * t * t)
If alpha > 255 Then alpha = 255This computes the transparency ( alpha ) for each trail segment.
-
t = i / trailLength
Gives a value between 0.0 ( oldest ) and 1.0 ( newest ). -
t * t
Squares the value, creating an exponential curve.
This makes the fade smoother and more natural. -
32 * t * t
Scales the alpha into a usable range. -
Clamp to 255
Ensures alpha never exceeds the valid maximum.
In short: Older trail segments are more transparent; newer ones are more visible.
trailBrushes(i).Color = Color.FromArgb(alpha, 0, 191, 255)Updates the brush color for this trail segment:
alpha= computed transparency- RGB =
(0, 191, 255)→ same blue as the ball
Each trail segment gets its own brush with its own alpha.
In short: Every trail circle fades smoothly based on its age.
Dim p As PointF = trail(i)
Dim size As Integer = trailSizes(i)
Dim offset As Single = trailOffsets(i)These values were precomputed in OnLoad:
p→ the recorded position of the ballsize→ the diameter of this trail circleoffset→ how much to shift the circle so it stays centered
In short: Each trail circle is smaller and centered relative to the ball.
g.FillEllipse(trailBrushes(i),
p.X + offset,
p.Y + offset,
size,
size)Draws the actual circle:
- Uses the brush with the correct alpha
- Uses the precomputed size
- Adds the offset so the circle is centered
This creates the layered, fading trail effect.
NextEnds the loop - all trail segments have now been drawn.
End SubEnds the trail‑rendering routine.
Your trail system combines:
- exponential alpha fading → smooth, natural fade
- shrinking circle sizes → tapered comet effect
- centered offsets → visually aligned trail
- preallocated brushes → no runtime allocations
- precomputed geometry → fast rendering
This is exactly how you build a professional‑looking motion trail in WinForms.
This method draws the ball itself.
It’s intentionally simple: one brush, one ellipse, one position.
All the complexity ( physics, trail, smoothing ) happens elsewhere - this is the final visual step.
Private Sub DrawBall(g As Graphics)Begins the ball‑rendering routine.
The Graphics object g is the drawing surface for this frame.
g.FillEllipse(ballBrush,
ballPos.X,
ballPos.Y,
ballDiameter,
ballDiameter)This single line draws the ball:
- The solid brush created in
OnLoad - Uses the color
DeepSkyBlue - No transparency - the ball is fully opaque
- The current position of the ball
- Updated every physics tick
- Stored as floating‑point values for smooth motion
- Width and height of the ellipse
- Because width = height, the ellipse is a perfect circle
- Draws a filled circle
- At the ball’s current position
- Using the ball’s color
- With the ball’s size
In short: This is the ball you see bouncing around the screen.
End SubEnds the ball‑rendering routine.
All the heavy lifting happens elsewhere:
- PhysicsTick moves the ball
- HandleCollisions keeps it inside the window
- UpdateTrail records its history
- DrawTrail renders the fading trail
- OnPaint sets up high‑quality rendering modes
By the time DrawBall runs, everything is ready to draw the final circle.
This separation of responsibilities is exactly what makes our animation engine clean and easy to extend.
This method draws the FPS counter in the top‑left corner of the screen.
It updates the FPS value once per second, then renders it using our preloaded font and brush.
Private Sub DrawFPS(g As Graphics)Begins the FPS‑rendering routine.
The Graphics object g is the drawing surface for this frame.
UpdateFPS()Calls our FPS‑tracking method, which:
- increments the frame counter
- checks whether one second has passed
- updates the
fpsfield - resets the counter for the next second
This ensures the FPS number is always accurate and refreshed once per second.
In short: Before drawing the FPS text, make sure the value is up‑to‑date.
g.DrawString($"FPS: {fps}", fpsFont, fpsBrush, 10, 10)This line draws the FPS counter:
- A formatted string showing the current FPS value
- Example:
"FPS: 64"
- The font created in
OnLoad "Segoe UI", 14pt, Bold- Clean and readable for UI overlays
- A solid white brush
- Ensures the text stands out against the black background
- Draws the text at coordinates
(10, 10) - Top‑left corner of the window
- Slight padding so it doesn’t touch the edges
In short: This draws a clean, readable FPS counter in the corner of the screen.
End SubEnds the FPS‑rendering routine.
Our FPS counter is:
- lightweight — only updates once per second
- accurate — uses a stopwatch instead of relying on frame timing
- non‑intrusive — drawn last so it overlays the scene
- beginner‑friendly — easy to understand and extend
It’s a perfect example of how real‑time diagnostics work in animation engines.
Windows Forms normally repaints the background before drawing the foreground.
For static apps, that’s fine.
For real‑time animation, it causes flicker — especially when drawing fast‑moving objects.
This override disables that behavior.
Protected Overrides Sub OnPaintBackground(pevent As PaintEventArgs)This overrides the form’s built‑in background‑painting method.
WinForms calls this before OnPaint unless you suppress it.
' Suppress background flickerA comment explaining the purpose of this override:
- Prevent background clearing
- Prevent unnecessary redraws
- Prevent flicker during animation
- Allow our own double‑buffering to handle everything
This is a classic technique used in game loops and custom renderers.
End SubEnds the override.
By default, WinForms does this every frame:
- Clear background
- Paint background
- Paint controls
- Paint your custom graphics
When animating:
- Step 1 creates a flash
- Step 2 creates a second flash
- Step 4 draws your ball
- The result is visible flicker, especially at high speeds
Our animation engine already uses:
OptimizedDoubleBufferDoubleBuffered = True- custom
OnPaint - no child controls
So background painting is not needed.
Suppressing it gives you:
- perfectly smooth animation
- no flashing
- no tearing
- no redundant work
This is exactly how professional WinForms animation engines handle rendering.
This method keeps track of how many frames were drawn in the last second.
It’s called once per frame (inside DrawFPS), and updates the FPS value once every 1000 ms.
Private Sub UpdateFPS()Begins the FPS‑update routine.
This method is called every time the screen is redrawn.
frameCount += 1Every time UpdateFPS runs, one frame has just been rendered.
So we increment the frame counter.
- If the app is running at 60 FPS → this line runs 60 times per second
- If the app is running at 120 FPS → it runs 120 times per second
In short: This counts how many frames occurred in the current one‑second window.
If fpsTimer.ElapsedMilliseconds >= 1000 ThenfpsTimer is a stopwatch that started in the constructor.
This line checks:
Has at least 1000 ms (1 second) passed since the last FPS update?
If yes, it’s time to compute a new FPS value.
fps = frameCountThe number of frames counted in the last second becomes the FPS value.
Examples:
- If
frameCount = 64→ FPS is 64 - If
frameCount = 120→ FPS is 120
This gives a real‑time measurement of how fast the animation is running.
frameCount = 0
fpsTimer.Restart()Two things happen:
Start counting frames for the next one‑second window.
Begin timing the next second.
In short: Every second, the FPS counter resets and starts fresh.
End If
End SubEnds the FPS‑update routine.
Our FPS counter is:
- simple — easy to understand
- accurate — uses a stopwatch instead of relying on frame timing
- efficient — updates only once per second
- clean — no heavy math or averaging needed
It’s exactly the kind of diagnostic tool you want in a real‑time animation.
This method runs every time the form is resized — whether the user drags the window edges, maximizes it, or restores it.
Its job is to keep the ball inside the new bounds and ensure the trail geometry stays correct.
Protected Overrides Sub OnResize(e As EventArgs)Overrides the form’s built‑in resize handler.
This fires whenever the form’s client area changes size.
MyBase.OnResize(e)Calls the base class version of OnResize.
This ensures WinForms performs its normal layout and resize behavior before your custom logic runs.
If trailSizes Is Nothing OrElse trailOffsets Is Nothing Then
Return
End IfThis prevents errors during startup.
OnResizecan fire beforeOnLoad- At that moment,
trailSizesandtrailOffsetsare stillNothing - Accessing them would cause a crash
So this guard simply exits early until the arrays are initialized.
In short: Don’t run resize logic until the trail system is ready.
When the window shrinks, the ball might suddenly be outside the visible area.
These checks snap it back inside.
If ballPos.X > ClientSize.Width - ballDiameter Then
ballPos.X = ClientSize.Width - ballDiameter
End If- If the ball’s right edge is outside the new width
- Move it back inside so it remains visible
If ballPos.Y > ClientSize.Height - ballDiameter Then
ballPos.Y = ClientSize.Height - ballDiameter
End If- Same logic, but for the bottom edge
In short: Resizing the window never hides the ball.
For i As Integer = 0 To trailLength - 1
Dim size As Integer = trailSizes(i)
trailOffsets(i) = CSng((ballDiameter - size) / 2)
NextEach trail circle is smaller than the main ball.
To keep them centered, you compute an offset:
(ballDiameter - size) / 2
This ensures:
- every trail circle stays perfectly centered
- resizing the window doesn’t distort the trail alignment
- the visual taper remains correct
In short: Recalculate the centering math for each trail segment.
Invalidate()Tells WinForms:
“The window changed — redraw everything.”
This ensures the ball and trail update immediately after resizing.
End SubEnds the resize‑handling routine.
Our animation engine stays stable because:
- it avoids crashes during early resize events
- it clamps the ball inside the new bounds
- it recalculates trail geometry
- it triggers a redraw so the scene updates instantly
This is exactly how you build a robust real‑time animation system in WinForms.
Windows Forms uses GDI+ objects (Brushes, Fonts, Pens, etc.) that must be manually disposed.
If you don’t dispose them, they remain in memory until the process exits — which is fine for tiny apps, but bad practice for real‑time graphics.
This cleanup method ensures our animation engine shuts down cleanly.
Private Sub Form1_FormClosing(sender As Object, e As FormClosingEventArgs) Handles Me.FormClosingThis method runs right before the form closes.
The Handles Me.FormClosing part wires it to the form’s closing event.
This is our chance to release resources and stop timers.
ballBrush?.Dispose()
fpsBrush?.Dispose()
fpsFont?.Dispose()
physicsTimer?.Dispose()Each of these objects holds unmanaged resources inside Windows:
Used to draw the main ball.
Used to draw the FPS text.
Used to render the FPS counter.
Stops the physics loop and releases the timer’s internal handles.
The ?. operator ensures disposal only happens if the object is not Nothing.
In short: Dispose everything that was created in OnLoad or the constructor.
If trailBrushes IsNot Nothing Then
For Each b In trailBrushes
b?.Dispose()
Next
End IfOur trail uses an array of brushes — one per trail segment.
This loop:
- Checks if the array exists
- Iterates through each brush
- Disposes it safely
This prevents dozens of small unmanaged objects from lingering after the form closes.
In short: Every trail brush is cleaned up properly.
End SubEnds the cleanup routine.
Even though the app closes immediately afterward, disposing GDI objects is:
- good practice
- professional
- important in long‑running apps
- important in apps that recreate brushes/fonts often
- a great example
It shows that real‑time graphics require careful resource management.