Palette cycling is one of those tricks that looks like magic the first time you see it: a completely still image — not a single pixel moving — and yet the water flows, the fire flickers, the waterfall pours, the neon sign pulses. No animation frames, no video, no particles. Just one small picture and a clever idea about colour.
It powered countless effects on the Amiga, on DOS VGA games and in the demoscene, and it costs almost nothing to run. BambooBasic brings it back — and improves on the original in one important way (more on that below). This page explains the whole idea from scratch, so you don't need to have "tickled a micro with pens and inks" to get it.
Normally an image stores a colour in every pixel — "this pixel is sky-blue, that one is white." An indexed image is different. Think of a paint-by-numbers canvas:
When the image is drawn, the computer looks up each pixel's number in the palette and paints that colour. So the picture you see is the numbers "painted in" according to the chart. The image itself is just a grid of numbers; the palette decides how it looks.
This has a wonderful consequence. If you change the chart, the picture changes instantly — without touching a single pixel. Repaint slot 2 from blue to red, and every pixel numbered 2 turns red at once. The numbers never moved; only their meaning did.
Here's the trick. Suppose your waterfall uses numbers 16 to 31 for the falling water, and the palette has a smooth run of blues and whites in slots 16 to 31. Now, every frame, rotate those sixteen palette slots by one: whatever colour was in slot 16 moves to 17, 17→18, and the one that falls off the end (31) wraps back round to 16.
The pixels still say 16, 17, 18… exactly as before. But slot 16 now shows the colour that slot 15's neighbour had a moment ago, slot 17 shows 16's old colour, and so on. To your eye, the band of colour appears to crawl down the picture. Do it every frame and the water flows — endlessly, smoothly — while the actual image sits perfectly still.
| What moves | What doesn't |
| The colours in a range of palette slots (they rotate) | The image's pixels — every pixel's number stays put |
Because no pixels are touched, it is astonishingly cheap: the whole effect is rotating a handful of numbers in a 256-entry list once per frame. That is why a 1985 home computer could do it, and why you can cover the screen in it today without a care.
The old machines had exactly one palette for the whole screen. Cycling it animated everything at once — you couldn't have a calm waterfall next to a raging fire, because they shared the same colour chart.
In BambooBasic, each indexed image carries its own palette and its own cycles. A waterfall, a fire, a spinning coin and a glowing portal can all sit on screen together, each flowing at its own speed and in its own direction, none affecting the others. That is a genuine super-power over the hardware this pays homage to — and it falls out naturally, at no extra cost.
Load an indexed PNG with b2dLoadIndexedImage, tell it which palette slots to cycle with b2dCyclePalette, and then just draw it. That's the whole thing:
waterfall = b2dLoadIndexedImage("Media/waterfall.png")
; Cycle palette slots 16..31 at 12 "slots per second"
b2dCyclePalette(waterfall, 16, 31, 12.0)
; ...then in your loop, simply:
b2dDrawImage(waterfall, 300, 210)
You never call an "update" or "animate" function. The cycles advance on their own from elapsed time, so the picture flows every frame just by being drawn. Load it, cycle it, draw it — done.
| Function | What it does |
| b2dLoadIndexedImage(path$) | Loads an indexed PNG, keeping its numbers and its palette. Returns an image handle. |
| b2dCyclePalette(img, start, end, speed) | Cycle palette slots start..end at speed slots/second. Negative speed flows the other way. |
| b2dAddPaletteCycle(img, start, end, speed) | Same, but stacks another cycle instead of replacing (see below). |
| b2dClearCycles(img) | Stop all cycling on this image. |
| b2dSetImagePaletteColor(img, index, r, g, b, a) | Set palette slot index to an RGBA colour (0–255 each). |
| b2dGetImagePaletteColorR/G/B/A(img, index) | Read a palette slot's red / green / blue / alpha. |
| b2dSetTransparentIndex(img, index) | Make one palette slot render as a see-through hole. |
| b2dFadePalette(img, toR, toG, toB, amount) | Fade the whole palette toward a colour (amount 0–1). Reversible. |
| b2dResetPalette(img) | Put the palette back exactly as the file loaded it. |
| b2dCopyPalette(dstImg, srcImg) | Copy one image's palette onto another. |
Colours are plain RGB integers, 0–255 — the same as everywhere else in the 2D commands. Speeds and fade amounts are decimals (Double).
You are not stuck with the colours in the file. b2dSetImagePaletteColor repaints any slot, and because the image is indexed, that recolours every pixel using that slot instantly. This is how you build ramps — a smooth run of colours for a cycle band to flow through — or reskin the same artwork into a different mood:
; Turn the "water" band (slots 16..31) into a fire ramp: dark red -> orange -> yellow
For i:Int = 0 To 15
b2dSetImagePaletteColor(fire, 16 + i, 100 + i * 10, i * 9, 0, 255)
Next
b2dCyclePalette(fire, 16, 31, 14.0) ; now the SAME artwork looks like flowing lava
This pairs beautifully with PreCalc: a colour ramp is just numbers, so you can bake a gradient, a fire palette or an HSV sweep at compile time and drop it straight into the slots at load.
Team colours, for free. Give one sprite two different palettes and you have two teams from one piece of art — no extra drawing:
b2dCopyPalette(player2, player1) ; share the base look...
b2dSetImagePaletteColor(player2, 5, 200, 30, 30, 255) ; ...then tint player 2 red
b2dCyclePalette(scene, 16, 31, 12.0) ; water, downward
b2dCyclePalette(scene, 8, 15, -5.0) ; mist, upward, slowerb2dFadePalette blends the whole palette toward a colour by an amount from 0 (no change) to 1 (fully that colour). Fade toward black to dim into night; fade toward white for a flash. It is absolute and reversible — setting the amount back to 0 restores the original colours exactly, so you can fade out and back in freely.
b2dFadePalette(scene, 8, 12, 45, 0.6) ; 60% of the way to a dark dusk blue
b2dFadePalette(scene, 8, 12, 45, 0.0) ; back to full daylight
Crucially, fading and cycling happen together without fighting. You can dim a torch to embers while its flames keep flickering, or bring a waterfall to dusk while the water keeps pouring. b2dResetPalette throws away any fade and recolouring and puts the palette back exactly as the file loaded it.
There are two ways an indexed image can have see-through parts:
tRNS
chunk), those slots are already see-through when it loads — nothing to do.By default no slot is forced transparent — the image honours whatever the file declares. So a picture that uses slot 0 as an ordinary dark colour is safe; nothing vanishes unless you ask it to.
This is the part that trips people up, so it's worth being clear. For cycling to work, the PNG must actually be saved as indexed (palette) colour — most image tools default to full "truecolour", which throws the numbers away. In your editor (Photoshop, GIMP, Aseprite, Paint Shop Pro…) look for Image → Mode → Indexed, or "Save as 8-bit / palette PNG."
Then lay your effect out so the colours you want to flow sit in a contiguous run of slots — because b2dCyclePalette works on a slot range. Put the water gradient in slots 16–31, the fire ramp in 32–47, and so on. Aseprite in particular makes this easy: arrange the palette, paint with those indices, export as an indexed PNG.
If you hand b2dLoadIndexedImage a normal (truecolour) PNG, it deliberately fails rather than guessing — use b2dLoadImage for those, and b2dLoadIndexedImage only for genuine indexed art.
One static PNG that flows like a waterfall, with two independent cycles and a slow day→dusk fade running over the top — the whole feature in a dozen lines of actual logic. You'll find this, and a six-panel "one image, six palettes" stress test, in examples/Runtime/15 - Palette Cycling.
Import "BBRuntimeLinux.decls"
Include "BBR_INCLUDE.bam" ; optional - copy it from userlibs/ for the named constants
Const GRAPHICS_WIDTH:Int = 800
Const GRAPHICS_HEIGHT:Int = 600
Const GRAPHICS_MODE:Int = BBR_WINDOW_MODE_WT
Global running:Int = True
Global waterfall:Int
Function Main()
b2dGraphics(GRAPHICS_WIDTH, GRAPHICS_HEIGHT, GRAPHICS_MODE)
; Load the indexed image - its 256-colour palette comes straight from the PNG
waterfall = b2dLoadIndexedImage("Media/waterfall.png")
; Two INDEPENDENT cycles on the SAME image
b2dCyclePalette(waterfall, 16, 31, 12.0) ; water, flowing down
b2dCyclePalette(waterfall, 8, 15, -5.0) ; mist, drifting up, slower
While running
sysUpdateEvents()
If inpIsKeyDown(VKEY_ESCAPE) Then running = False
b2dCls()
; Just draw it - the cycles animate themselves from elapsed time
b2dDrawImage(waterfall, 300, 210)
b2dFlip()
Wend
b2dEnd()
Return False
EndFunction
That's palette cycling: a still picture, a rotating list of colours, and water that pours forever for almost no cost. Old trick, new super-powers. Go make something flow.
BambooBasic © 2026 Michael Denathorn