gb-sprite: Procedural 2D sprite and VFX generation

[ bsd3, graphics, library ] [ Propose Tags ] [ Report a vulnerability ]

A pure Haskell library for generating 2D sprites, animations, and visual effects. No external image libraries — just math. Generate sprites, draw primitives, compose layers, pack sprite sheets, render bitmap text, and create procedural VFX. Export to BMP and PNG natively.


[Skip to Readme]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.1.0.0, 0.2.0.0, 0.2.1.0, 0.2.1.1, 0.2.1.2, 0.3.0.0, 0.3.0.1, 0.4.0.0
Change log CHANGELOG.md
Dependencies base (>=4.18 && <5), bytestring (>=0.11 && <0.13), zlib (>=0.6 && <0.8) [details]
Tested with ghc ==9.8.4
License BSD-3-Clause
Author Devon Tomlin
Maintainer devon.tomlin@novavero.ai
Uploaded by aoinoikaz at 2026-03-05T09:21:11Z
Category Graphics
Home page https://github.com/Gondola-Bros-Entertainment/gb-sprite
Bug tracker https://github.com/Gondola-Bros-Entertainment/gb-sprite/issues
Source repo head: git clone https://github.com/Gondola-Bros-Entertainment/gb-sprite.git
Distributions
Downloads 29 total (29 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs available [build log]
Last success reported on 2026-03-05 [all 1 reports]

Readme for gb-sprite-0.4.0.0

[back to package description]

gb-sprite

Procedural 2D Sprite & VFX Generation

Pure Haskell — no image files, no textures, no asset pipeline. Just math.

Overview · Architecture · Usage · API · Example

CI Hackage Haskell


Overview

gb-sprite is a pure Haskell library for procedurally generating 2D sprites, animations, and visual effects. Define pixel art, VFX, and sprite sheets programmatically — render to BMP with zero external image dependencies.

Companion to gb-synth (procedural audio) and gb-vector (SVG generation).

Features:

  • ByteString-backed RGBA canvas with drawing primitives (lines, circles, polygons, Bezier curves)
  • Transforms: flip, rotate, scale, outline, drop shadow
  • Alpha-blended compositing and layering
  • Sprite sheet packing (shelf bin packing) with metadata
  • Built-in 8x8 pixel font (full printable ASCII)
  • Procedural VFX generators (explosion, ring, glow, trail, flash, sparks)
  • Procedural noise (value noise, fractal Brownian motion)
  • Gradients (linear, radial, diagonal)
  • Nine-slice UI panel scaling
  • Ordered Bayer dithering for palette reduction
  • Tilemap rendering from sprite atlas
  • BMP export (32-bit BGRA, raw header bytes)
  • PNG export (32-bit RGBA, zlib compressed)

Core dependencies: base, bytestring, zlib.


Architecture

src/GBSprite/
├── Color.hs       RGBA type, named colors, lerp, multiply, alpha blend
├── Canvas.hs      2D pixel grid (strict ByteString), drawing primitives
├── Draw.hs        Thick lines, polygons, ellipses, arcs, Bezier curves
├── Palette.hs     Indexed palettes (gameboy, NES), palette swap
├── Sprite.hs      Named sprite with origin, frames, bounding box
├── Animation.hs   Loop/Once/PingPong frame sequencing
├── Transform.hs   Flip, rotate, scale, outline, drop shadow
├── Compose.hs     Alpha-blended layering and stamping
├── Sheet.hs       Shelf bin packing into sprite atlas
├── Text.hs        Built-in 8x8 pixel font rendering
├── Tilemap.hs     Tile-based map rendering from atlas
├── VFX.hs         Procedural effects (explosion, ring, glow, trail, etc.)
├── Noise.hs       Value noise, fractal Brownian motion
├── Gradient.hs    Linear, radial, diagonal gradients
├── NineSlice.hs   UI panel scaling with border preservation
├── Dither.hs      Ordered Bayer dithering for palette reduction
├── BMP.hs         32-bit BGRA BMP export
├── PNG.hs         32-bit RGBA PNG export (zlib compressed)
└── Export.hs      Unified export API (BMP + PNG)

Pipeline

Canvas → Draw/Transform/Compose → Sprite → Animation → Sheet (atlas) → BMP/PNG
                                                  ↗
VFX generators → [Canvas] frames ─────────────────┘

Usage

As a dependency

Add to your .cabal file:

build-depends: gb-sprite >= 0.3

Generating sprites

import GBSprite.Canvas (newCanvas, fillCircle)
import GBSprite.Color (red, transparent)
import GBSprite.BMP (writeBmp)

main :: IO ()
main = do
  let canvas = fillCircle (newCanvas 32 32 transparent) 16 16 12 red
  writeBmp "sprite.bmp" canvas

API

Color

data Color = Color { colorR, colorG, colorB, colorA :: !Word8 }

-- Named colors
red, green, blue, white, black, transparent :: Color

-- Math
lerp       :: Double -> Color -> Color -> Color   -- linear interpolation
multiply   :: Color -> Color -> Color              -- component-wise multiply
alphaBlend :: Color -> Color -> Color              -- Porter-Duff "over"
withAlpha  :: Word8 -> Color -> Color              -- replace alpha channel
scaleAlpha :: Double -> Color -> Color             -- scale alpha channel

Canvas

data Canvas = Canvas { cWidth :: !Int, cHeight :: !Int, cPixels :: !BS.ByteString }

newCanvas  :: Int -> Int -> Color -> Canvas        -- blank canvas filled with color
setPixel   :: Canvas -> Int -> Int -> Color -> Canvas
getPixel   :: Canvas -> Int -> Int -> Color
drawLine   :: Canvas -> Int -> Int -> Int -> Int -> Color -> Canvas  -- Bresenham
drawRect   :: Canvas -> Int -> Int -> Int -> Int -> Color -> Canvas
fillRect   :: Canvas -> Int -> Int -> Int -> Int -> Color -> Canvas
drawCircle :: Canvas -> Int -> Int -> Int -> Color -> Canvas
fillCircle :: Canvas -> Int -> Int -> Int -> Color -> Canvas
floodFill  :: Canvas -> Int -> Int -> Color -> Canvas

Draw

drawThickLine :: Canvas -> Int -> Int -> Int -> Int -> Int -> Color -> Canvas
drawPolygon   :: Canvas -> [(Int, Int)] -> Color -> Canvas
fillPolygon   :: Canvas -> [(Int, Int)] -> Color -> Canvas  -- scanline fill
drawEllipse   :: Canvas -> Int -> Int -> Int -> Int -> Color -> Canvas
fillEllipse   :: Canvas -> Int -> Int -> Int -> Int -> Color -> Canvas
drawArc       :: Canvas -> Int -> Int -> Int -> Int -> Double -> Double -> Color -> Canvas
drawBezier    :: Canvas -> (Int,Int) -> (Int,Int) -> (Int,Int) -> Color -> Canvas
drawRoundRect :: Canvas -> Int -> Int -> Int -> Int -> Int -> Color -> Canvas
fillRoundRect :: Canvas -> Int -> Int -> Int -> Int -> Int -> Color -> Canvas

Transform

flipH, flipV         :: Canvas -> Canvas
rotate90, rotate180, rotate270 :: Canvas -> Canvas
scaleNearest :: Int -> Canvas -> Canvas            -- nearest-neighbor upscale
outline      :: Color -> Canvas -> Canvas          -- outline non-transparent pixels
dropShadow   :: Int -> Int -> Color -> Canvas -> Canvas

Compose

stamp      :: Canvas -> Int -> Int -> Canvas -> Canvas  -- direct overwrite
stampAlpha :: Canvas -> Int -> Int -> Canvas -> Canvas   -- alpha blended
overlay    :: Canvas -> Canvas -> Canvas                 -- same-size blend
overlayAt  :: Canvas -> Int -> Int -> Canvas -> Canvas   -- offset blend

Noise

Deterministic procedural noise for textures and terrain:

valueNoise      :: Int -> Int -> Int -> Double -> Canvas              -- width, height, seed, scale
valueNoiseColor :: Int -> Int -> Int -> Double -> Color -> Color -> Canvas  -- noise between two colors
fbm             :: Int -> Int -> Int -> Int -> Double -> Canvas       -- fractal Brownian motion

Gradient

linearGradient   :: Int -> Int -> Color -> Color -> Bool -> Canvas  -- horizontal or vertical
radialGradient   :: Int -> Int -> Int -> Int -> Int -> Color -> Color -> Canvas  -- center, radius
diagonalGradient :: Int -> Int -> Color -> Color -> Canvas          -- top-left to bottom-right

NineSlice

Scale UI panels while preserving borders:

data NineSlice = NineSlice
  { nsCanvas :: !Canvas, nsLeft :: !Int, nsRight :: !Int, nsTop :: !Int, nsBottom :: !Int }

nineSlice       :: Canvas -> Int -> Int -> Int -> Int -> NineSlice  -- define border regions
renderNineSlice :: NineSlice -> Int -> Int -> Canvas                -- render at target size

Palette

Indexed color palettes for retro-style coloring and palette swaps:

newtype Palette = Palette { paletteColors :: [Color] }

-- Built-in palettes
grayscale4, grayscale8 :: Palette            -- 4/8-shade grayscale ramps
gameboy              :: Palette              -- authentic DMG green palette
nes                  :: Palette              -- 16-color NES-inspired palette

-- Operations
fromColors   :: [Color] -> Palette           -- build palette from color list
paletteColor :: Palette -> Int -> Color      -- look up color by index (clamped)
paletteSwap  :: Palette -> Palette -> Color -> Color  -- remap source to destination

Sprite

Named multi-frame images with origin and bounding box:

data Sprite = Sprite
  { spriteName :: !String, spriteOriginX :: !Int, spriteOriginY :: !Int
  , spriteFrames :: ![Canvas], spriteBounds :: !(Maybe BoundingBox) }

data BoundingBox = BoundingBox
  { bbX :: !Int, bbY :: !Int, bbWidth :: !Int, bbHeight :: !Int }

singleFrame  :: String -> Canvas -> Sprite                 -- single-frame at origin (0,0)
multiFrame   :: String -> Int -> Int -> [Canvas] -> Sprite -- name, originX, originY, frames
spriteWidth  :: Sprite -> Int
spriteHeight :: Sprite -> Int
frameCount   :: Sprite -> Int
getFrame     :: Sprite -> Int -> Maybe Canvas              -- safe index lookup

Dither

Ordered dithering for retro palette reduction:

data DitherMatrix = Bayer2 | Bayer4 | Bayer8

orderedDither :: DitherMatrix -> Palette -> Canvas -> Canvas  -- reduce to palette with dithering

Sheet

data SpriteSheet = SpriteSheet { sheetCanvas :: !Canvas, sheetEntries :: ![SheetEntry] }
data SheetEntry  = SheetEntry  { entryName :: !String, entryX, entryY, entryWidth, entryHeight :: !Int }

packSheet :: Int -> [(String, Canvas)] -> SpriteSheet   -- shelf bin packing

Text

defaultFont :: Font                                     -- built-in 8x8 pixel font
renderText  :: Font -> Color -> String -> Canvas
renderChar  :: Font -> Color -> Char -> Canvas
textWidth   :: Font -> String -> Int
textHeight  :: Font -> Int

VFX

explosionFrames  :: ExplosionConfig -> [Canvas]   -- radial particle burst
ringExpandFrames :: RingConfig -> [Canvas]         -- expanding ring
glowPulseFrames  :: GlowConfig -> [Canvas]        -- pulsing aura
trailFrames      :: TrailConfig -> [Canvas]        -- fading trail
flashFrames      :: Int -> Color -> [Canvas]       -- solid color fade-out
sparksFrames     :: SparksConfig -> [Canvas]       -- directional spark burst

Animation

data LoopMode = Loop | Once | PingPong

data Animation = Animation
  { animFrameDelay :: !Int, animFrameCount :: !Int, animLoopMode :: !LoopMode }

animation         :: Int -> Int -> LoopMode -> Animation -- delay, frame count, mode
loopAnimation     :: Int -> Int -> Animation           -- delay, frame count
onceAnimation     :: Int -> Int -> Animation
pingPongAnimation :: Int -> Int -> Animation
animationFrame    :: Animation -> Int -> Int            -- animation, tick → frame index
animationDone     :: Animation -> Int -> Bool           -- animation, tick → finished?

Tilemap

Tile-based map rendering from a sprite atlas:

data TilemapConfig = TilemapConfig
  { tmTileWidth :: !Int, tmTileHeight :: !Int
  , tmGridWidth :: !Int, tmGridHeight :: !Int
  , tmTiles :: ![Int] }                                    -- row-major tile indices

renderTilemap :: SpriteSheet -> TilemapConfig -> Canvas     -- render grid from atlas

BMP / Export

encodeBmp :: Canvas -> BL.ByteString               -- pure: canvas → BMP bytes (lazy)
writeBmp  :: FilePath -> Canvas -> IO ()            -- write BMP file
encodePng :: Canvas -> BL.ByteString               -- pure: canvas → PNG bytes (lazy)
writePng  :: FilePath -> Canvas -> IO ()            -- write PNG file
exportBmp :: FilePath -> Canvas -> IO ()            -- re-export of writeBmp (GBSprite.Export)
exportPng :: FilePath -> Canvas -> IO ()            -- re-export of writePng (GBSprite.Export)

Example

Generate a sprite sheet with explosion VFX and a dithered background:

import GBSprite.Canvas (newCanvas, fillCircle)
import GBSprite.Color (red, yellow, transparent)
import GBSprite.Dither (DitherMatrix (..), orderedDither)
import GBSprite.Gradient (linearGradient)
import GBSprite.Palette (gameboy)
import GBSprite.Sheet (SpriteSheet (..), packSheet)
import GBSprite.VFX (ExplosionConfig (..), explosionFrames)
import GBSprite.BMP (writeBmp)

main :: IO ()
main = do
  -- Dithered background using Game Boy palette
  let gradient = linearGradient 64 64 red yellow True
      background = orderedDither Bayer4 gameboy gradient

  -- Simple character sprite
  let character = fillCircle (newCanvas 16 16 transparent) 8 8 6 red

  -- Generate explosion frames
  let explosion = explosionFrames ExplosionConfig
        { expSize = 32
        , expFrameCount = 8
        , expParticleCount = 20
        , expColor = yellow
        , expSeed = 42
        }

  -- Pack everything into a sprite sheet
  let items = ("background", background)
            : ("character", character)
            : zip (map (\i -> "explosion_" ++ show i) [0::Int ..]) explosion
      sheet = packSheet 1 items

  -- Export the atlas
  writeBmp "sprites.bmp" (sheetCanvas sheet)

Build & Test

Requires GHCup with GHC >= 9.8.

cabal build                              # Build library
cabal test                               # Run all tests (366 pure tests)
cabal build --ghc-options="-Werror"      # Warnings as errors
cabal haddock                            # Generate docs

BSD-3-Clause License · Gondola Bros Entertainment