Visual effects like water droplets are mesmerizing, and they showcase how simple algorithms can produce complex, beautiful
animations. In this article, I’ll walk you through creating a water droplet effect using VGA mode 13h.
We’ll rely on some of the code that we developed in the VGA routines from Watcom C
article for VGA setup and utility functions, focusing on how to implement the effect itself.
The effect that we’re looking to produce should look something like this:
The Idea
The water droplet effect simulates circular ripples spreading out from random points on the screen. Here’s the high-level approach:
Drops: Represent each drop with a structure containing its position, energy, and ripple generation.
Drawing Ripples: Use trigonometry to create circular patterns for each ripple generation.
Blur Effect: Smooth the buffer to simulate water’s fluid motion.
Palette: Set a blue-themed palette to enhance the watery feel.
Setting the Water Palette
First, we set a blue-tinted gradient palette. Each color gradually transitions from dark blue to bright blue.
voidset_water_palette(){uint16_ti;uint8_tr,g,b;for(i=0;i<256;i++){r=i>>2;// Dim redg=i>>2;// Dim greenb=63;// Maximum blueset_palette(i,r,g,b);}}
Representing Drops
Each drop is represented by a structure that tracks:
(x, y): The origin of the drop.
e: Energy, which fades with time.
g: Current ripple generation.
structdrop{intx;/* original x-coordinate */inty;/* original y-coordinate */inte;/* energy left in the drop */intg;/* current generation */};structdropdrops[N_DROPS];
Creating and Advancing Drops
Drops are reset with random positions, maximum energy, and zero ripple generation:
Ripples are drawn using polar coordinates. We calculate x and y offsets using cosine and sine functions for each
angle and scale by the current generation.
voiddraw_drop(structdrop*d,uint8_t*buffer){// if this droplet still has some energyif(d->e>0){// 0 to 2πfor(floatrad=0.0f;rad<6.28f;rad+=0.05f){// x, y co-ordinates to go around the circleintxx=(int)(cos(rad)*(float)d->g);intyy=(int)(sin(rad)*(float)d->g);// translate them into the fieldxx+=d->x;yy+=d->y;// clip them to the visible fieldif((xx>=0)&&(xx<320)&&(yy>=0)&&(yy<200)){uint16_toffset=xx+(yy<<6)+(yy<<8);// VGA offsetuint16_tc=buffer[offset];// clamp the pixel colour to 255if((c+d->e)>255){c=255;}else{c+=d->e;}// set the pixelbuffer[offset]=c;}}}}
The colour that is rendered to the buffer is additive. We take the current colour at the pixel position, and add to it
giving the droplets a sense of collision when they overlap.
Simulating Fluid Motion
A blur effect smooths the ripples, blending them into neighboring pixels for a more fluid appearance. This is done by
averaging surrounding pixels.
voidblur_buffer(uint8_t*buffer){memset(buffer,0,320);// Clear top bordermemset(buffer+63680,0,320);// Clear bottom borderfor(uint16_ti=320;i<63680;i++){buffer[i]=(buffer[i-321]+buffer[i-320]+buffer[i-319]+buffer[i-1]+buffer[i+1]+buffer[i+319]+buffer[i+320]+buffer[i+321])>>3;// Average of 8 neighbors}}
Main Loop
The main loop handles:
Adding new drops randomly.
Advancing and drawing existing drops.
Applying the blur effect.
Rendering the buffer to the VGA screen.
intmain(){uint8_t*back_buffer=(uint8_t*)malloc(64000);uint8_tdrop_index=0;set_mcga();// Switch to VGA modeset_water_palette();// Set blue gradientclear_buffer(0x00,back_buffer);// Clear the back bufferwhile(!kbhit()){// Continue until a key is pressed// Randomly reset a dropif((rand()%10)==0){reset_drop(&drops[drop_index]);drop_index++;drop_index%=N_DROPS;}// Process and draw each dropfor(inti=0;i<N_DROPS;i++){advance_drop(&drops[i]);draw_drop(&drops[i],back_buffer);}blur_buffer(back_buffer);// Apply the blur effectwait_vsync();// Synchronize with vertical refreshcopy_buffer(vga,back_buffer);// Copy back buffer to screenclear_buffer(0x00,back_buffer);// Clear back buffer for next frame}free(back_buffer);set_text();// Return to text modereturn0;}
Conclusion
This water droplet effect combines simple algorithms with creative use of VGA mode 13h to create a visually stunning effect. By leveraging circular ripples, energy fading, and a blur filter, we replicate the mesmerizing motion of water.
You can find the complete code on GitHub as a gist.
Try it out, tweak the parameters, and share your own effects! There’s a lot of joy in creating beautiful visuals with minimal resources.
The VGA era was all about getting the most out of limited hardware. It required clever tricks to push pixels and make
things move. To make it easier to work with VGA and related concepts, I put together a library called freak.
This library includes tools for VGA handling, keyboard input, vector and matrix math, and
fixed-point math. In this post, I’ll go through each part of the library and explain how it works, with examples.
Be warned - this stuff is old! You’ll need a Watcom Compiler as well as a dos-like environment to be able to run any
of your code. I’ve previously written about getting Watcom up and running with DosBox. If you want to get this running
you can read the following:
The code that this article outlines is available here.
Video routines
First of all, we’re going to take care of shifting in and out of old mode 13.
Setting a video mode
To shift in and out of video modes we use the int 10h bios interrupt.
#define BIOS_VIDEO_80x25 0x03
#define BIOS_VIDEO_320x200x256 0x13
voidfreak_set_video(uint8_tmode);#pragma aux freak_set_video = \
"mov ah, 0" \
"int 0x10" \
parm [al];
/** Sets the video to 320x240x256 */inlinevoidfreak_set_mcga(){freak_set_video(BIOS_VIDEO_320x200x256);}/** Sets the video to 80x25 text */inlinevoidfreak_set_text(){freak_set_video(BIOS_VIDEO_80x25);}
Passing the video mode into al and setting ah to 0 allows us to change modes.
We also need to define where we want to draw to. VGA maps to A000:0000 in real mode. Because we’re in protected mode
(thanks to DOS/4G) we set our pointer to 0xA0000.
We defined the pointer freak_vga as a location in memory. From that point in memory for the next 64,000 bytes (we’re
using 320x200x8 which is 64k) are all of the pixels on the screen.
That means we can treat the screen like any old memory buffer. That also means that we can define virtual buffers as
long as we have 64k to spare; which we do.
You could imagine doing something like this pretty easily:
uint8_t*back_buffer=(uint8_t*)malloc(64000);
We could use memset and memcpy to work with these buffers; or we would write our own optimised implementations to
use instructions to move a double at a time (like movsd and stosd):
/** Clears a buffer with a value */voidfreak_clear_buffer(uint8_tc,uint8_t*buf);#pragma aux freak_clear_buffer = \
"mov ah, al" \
"mov bx, ax" \
"shl eax, 16" \
"mov ax, bx" \
"mov ecx, 16000" \
"rep stosd" \
modify [eax ebx ecx] \
parm [al] [edi];
/** Copies a buffer onto another */voidfreak_copy_buffer(uint8_t*dest,uint8_t*src);#pragma aux freak_copy_buffer = \
"mov ecx, 16000" \
"rep movsd" \
modify [ecx] \
parm [edi] [esi];
Before flipping a back buffer onto the vga surface, we wait for the vsync to complete. This removes any flicker.
/** Waits for a vertical sync to occur */voidfreak_wait_vsync();#pragma aux freak_wait_vsync = \
"mov dx, 03dah" \
"@@vsync1:" \
"in al, dx" \
"test al, 08h" \
"jz @@vsync1" \
"@@vsync2:" \
"in al, dx" \
"test al, 08h" \
"jnz @@vsync2" \
modify [ax dx];
Colours
In mode13, we are given 256 colour slots to where we can control the red, green, and blue component. Whilst the default
palette does provide a vast array of different colours; it kinda sucks.
In order to set the r, g, b components of a colour we first need to write the colour index out to port 0x3c8. We then
write the r, g, and b components sequentially out to 0x3c9.
The fixed point article that I had previously written walks you through
the basic mechanics of the topic. The bit lengths of the whole and fractional parts are pretty small; and unusable. So
we’re going to use this technique, but scale it up.
Conversions
First of all, we need to be able to go from the “C type world” (the world of int and double, for instance) into the
“fixed point world”. We also need to make our way back:
Our trig tables are based around a nerd number of 1,024 making this a little easier to reason about and giving us an
acceptable level of precision between fractions of radians for what we need.
These are then nicely wrapped up in macros.
Operations
The fixed multiply is a very simple integer-based operation (by design):
To rotate around an arbitrary axis \(\mathbf{a} = \begin{bmatrix} a_x \\ a_y \\ a_z \end{bmatrix}\) by an angle \(\theta\),
the rotation matrix is defined as:
The freak library is my attempt to distill the essence of classic VGA programming into a modern, accessible toolkit. By
combining essential building blocks like graphics handling, input, and math operations, it provides everything you need
to recreate the magic of the demoscene or explore retro-style programming.
I hope this article inspires you to dive into the world of low-level programming and experiment with the techniques that
defined a generation of creativity. Whether you’re building your first polygon renderer or optimizing an effect with
fixed-point math, freak is here to make the journey both rewarding and fun.
Let me know what you think or share what you build—there’s nothing quite like seeing new creations come to life with
tools like these!
Sometimes, you need to squeeze more performance out of your Python code, and one great way to do that is to offload some of your CPU-intensive tasks to an extension. Traditionally, you might use a language like C for this. I’ve covered this topic in a previous post.
In today’s post, we’ll use the Rust language to create an extension that can be called from Python. We’ll also explore the reverse: allowing your Rust code to call Python.
Setup
Start by creating a new project. You’ll need to switch to the nightly Rust compiler:
# Create a new project
cargo new hello_world_ext
cd hello_world_ext
# Set the preference to use the nightly compiler
rustup override set nightly
Next, ensure pyo3 is installed with the extension-module feature enabled. Update your Cargo.toml file:
This function simply returns the string "Hello, world!".
The #[pyfunction] attribute macro exposes Rust functions to Python. The return type PyResult<T> is an alias for Result<T, PyErr>, which handles Python function call results.
The #[pymodule] attribute macro defines the module. The add_wrapped method adds the wrapped function to the module.
Building
With the code in place, build the module:
cargo build
Once built, install it as a Python package using maturin. First, set up a virtual environment and install maturin:
# Create a new virtual environment
python -m venv venv
# Activate the environmentsource ./venv/bin/activate
# Install maturin
pip install maturin
Now, build and install the module:
maturin develop
The develop command that we use here builds our extension, and automatically installs the result into our virtual
environment. This makes life easy for us during the development and testing stages.
usepyo3::prelude::*;usepyo3::types::IntoPyDict;fnmain()->PyResult<()>{Python::with_gil(|py|{letsys=py.import("sys")?;letversion:String=sys.getattr("version")?.extract()?;letlocals=[("os",py.import("os")?)].into_py_dict(py);letuser:String=py.eval("os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'",None,Some(&locals))?.extract()?;println!("Hello {}, I'm Python {}",user,version);Ok(())})}
Rewriting critical pieces of your Python code in a lower-level language like Rust can significantly improve performance. With pyo3, the integration between Python and Rust becomes seamless, allowing you to harness the best of both worlds.
In a previous post we covered the basic setup on
drawing to a <canvas> object via WebAssembly (WASM). In today’s article, we’ll create animated graphics directly on a
HTML5 canvas.
We’ll break down the provided code into digestible segments and walk through each part to understand how it works. By
the end of this article, you’ll have a clear picture of how to:
Set up an HTML5 canvas and interact with it using Rust and WebAssembly.
Generate random visual effects with Rust’s rand crate.
Build an animation loop with requestAnimationFrame.
Use shared, mutable state with Rc and RefCell in Rust.
Let’s get started.
Walkthrough
I won’t cover the project setup and basics here. The previous post
has all of that information for you. I will cover some dependencies that you need for your project here:
There’s a number of features in use there from web-sys. These will become clearer as we go through the code. The
getrandom dependency has web assembly support so
we can use this to make our animations slightly generative.
Getting Browser Access
First thing we’ll do is to define some helper functions that will try and acquire different features in the browser.
We need to be able to access the browser’s window object.
fnwindow()->web_sys::Window{web_sys::window().expect("no global `window` exists")}
This function requests the common window object from the Javascript environment. The expect will give us an error
context if it fails, telling us that no window exists.
The function being requested here is documented as the callback.
The window.requestAnimationFrame() method tells the browser you wish to perform an animation. It requests the browser to call a user-supplied callback function before the next repaint.
This will come in handy to do our repaints.
Now, in our run function, we can start to access parts of the HTML document that we’ll need references for. Sitting in
our HTML template, we have the <canvas> tag that we want access to:
When we double-buffer graphics, we need to allocate the block of memory that will act as our “virtual screen”. We draw
to that virtual screen, and then “flip” or “blit” that virtual screen (piece of memory) onto video memory to give the
graphics movement.
The size of our buffer will be width * height * number_of_bytes_per_pixel. With a red, green, blue, and alpha channel
that makes 4 bytes.
Animation Loop
We can now setup our animation loop.
This approach allows the closure to reference itself so it can schedule the next frame, solving Rust’s strict ownership
and borrowing constraints.
letf=Rc::new(RefCell::new(None));letg=f.clone();*g.borrow_mut()=Some(Closure::new(move||{// do the animation code here// queue up another re-draw requestrequest_animation_frame(f.borrow().as_ref().unwrap());});// queue up the first re-draw request, to start animationrequest_animation_frame(g.borrow().as_ref().unwrap());
This pattern is common in Rust for managing shared, mutable state when working with closures in scenarios where you need
to reference a value multiple times or recursively, such as with event loops or callback-based systems. Let me break it
down step-by-step:
The Components
Rc(Reference Counted Pointer):
Rc allows multiple ownership of the same data by creating a reference-counted pointer. When the last reference to the data is dropped, the data is cleaned up.
In this case, it enables both f and g to share ownership of the same RefCell.
RefCell(Interior Mutability):
RefCell allows mutable access to data even when it is inside an immutable container like Rc.
This is crucial because Rc itself does not allow mutable access to its contents by design (to prevent race conditions in a single-threaded context).
Closure:
A closure in Rust is a function-like construct that can capture variables from its surrounding scope.
In the given code, a Closure is being stored in the RefCell for later use.
What’s Happening Here?
Shared Ownership:
Rc is used to allow multiple references (f and g) to the same underlying RefCell. This is required because the closure may need to reference f while being stored in it, which is impossible without shared ownership.
Mutation with RefCell:
RefCell enables modifying the underlying data (None → Some(Closure)) despite Rc being immutable.
Setting the Closure:
The closure is created and stored in the RefCell via *g.borrow_mut().
This closure may reference f for recursive or repeated access.
We follow this particular pattern here because the closure needs access to itself in order to recursively schedule calls
to requestAnimationFrame. By storing the closure in the RefCell, the closure can call itself indirectly.
If we didn’t use this pattern, we’d have some lifetime/ownership issues. Referencing the closure while defining it
would create a circular reference problem that Rust wouldn’t allow.
Drawing
We’re going to find a random point on our virtual screen to draw, and we’re going to pick a random shade of grey. We’re
going to need a random number generator:
Blitting refers to copying pixel data from the backbuffer to the canvas in a single operation. This ensures the displayed
image updates smoothly
Now we need to blit that back buffer onto our canvas. We need to create an ImageData object in order to do this.
Passing in our backbuffer object, we can create one with the following:
letimage_data=ImageData::new_with_u8_clamped_array_and_sh(Clamped(&backbuffer),// Wrap the slice with Clampedwidthasu32,heightasu32,).unwrap();
We then use our 2d context to simply draw the image:
And there you have it—a complete walkthrough of creating dynamic canvas animations with Rust and WebAssembly! We covered
how to:
Set up the canvas and prepare a backbuffer for pixel manipulation.
Use Rust’s rand crate to generate random visual effects.
Manage mutable state with Rc and RefCell for animation loops.
Leverage requestAnimationFrame to achieve smooth, frame-based updates.
This approach combines Rust’s strengths with the accessibility of modern web technologies, allowing you to build fast,
interactive graphics directly in the browser.
In some situations, you may need to build yourself a bare machine binary
file. Some embedded applications can require this, as well as systems programming where you might be building for
scenarios where you don’t have libraries available to you.
In today’s post, we’ll go through building one of these binaries.
Getting Started
Let’s create a standard binary project to start with.
cargo new depfree
This will produce a project that will have the following structure:
This is already a pretty minimal program. Now our job starts!
Standard Library
When you build an application, by default all Rust crates will link to the standard library.
We can get rid of this by using the no_std attribute like so:
#![no_std]fnmain(){println!("Hello, world!");}
After a quick re-build, we quickly run into some issues.
error: cannot find macro `println` in this scope
--> src/main.rs:3:5
|
3 | println!("Hello, world!");
| ^^^^^^^
error: `#[panic_handler]` function required, but not found
error: unwinding panics are not supported without std
Clearly, println is no longer available to us, so we’ll ditch that line.
#![no_std]fnmain(){}
We also need to do some extra work around handling our own panics.
Handling Panics
Without the no_std attribute, Rust will setup a panic handler for you. When you have no_std specified, this
implementation no longer exists. We can use the panic_handler attribute to nominate a function that will handle our
panics.
Now we’ve defined a panic handler (called panic) that will do nothing more than just spin-loop forever. The
return type of ! means that the function won’t ever return.
We’re also being told that unwinding panics are not supported when we’re not using the standard library. To simplify
this, we can just force panics to abort. We can control this in our Cargo.toml:
We’ve just disabled unwinding panics in our programs.
If we give this another rebuild now, we get the following:
error: using `fn main` requires the standard library
|
= help: use `#![no_main]` to bypass the Rust generated entrypoint and declare a platform specific entrypoint yourself, usually with `#[no_mangle]`
This is progress, but it looks like we can’t hold onto our main function anymore.
Entry Point
We need to define a new entry point. By using the no_main attribute, we are free to no longer define a main
function in our program:
We really have no entry point now. Building this will give you a big horrible error and basically boils down to a
linker error:
(.text+0x1b): undefined reference to `main'
/usr/bin/ld: (.text+0x21): undefined reference to `__libc_start_main'
Fair enough. Our linker is taking exception to the fact that we don’t have a _start function which is what the
underlying runtime is going to want to call to start up. The linker will look for this function by default.
So, we can fix that by defining a _start function.
The no_mangle attribute makes sure that the _start function maintains its name, otherwise the compiler will
use its own creativity and generate a name for you. When it does this, it mangles the name so bad that the linker
can no longer find it.
The extern "C" is as you’d expect, giving this function C calling conventions.
The C Runtime
After defining our own _start entrypoint, we can give this another build.
You should see a horrific linker error.
The program that the compiler and linker is trying to produce (for my system here at least) is trying to do so using
the C runtime. As we’re trying to get dependency-free, we need to tell the build chain that we don’t want to use this.
In order to do that, we need to build our program for a bare metal target. It’s worth understanding what a “target triple”
is and what one is made up of that you can start using. The rust lang
book has a great section on this.
These take the structure of cpu_family-vendor-operating_system. A target triple encodes information about the target of a
compilation session.
You can see all of the targets available for you to install with the following:
rustc --print=target-list
You need to find one of those many targets that doesn’t have any underlying dependencies.
In this example, I’ve found x86_64-unknown-none. A 64-bit target produced by unknown for not particular operating
system: none. Install this runtime: