Scriblz is a vector graphics library. Built for game development, embedded UI work, and any other software requiring vector graphics.

About Scriblz


Scriblz, A Vector Graphics Library

Scriblz is a vector graphics library. Built for game development, embedded UI work, and any other software requiring vector graphics. Follows the STB guidelines.

https://codeberg.org/scriblz/scriblz/

Major Features

  • Easy-to-use immediate mode API
  • C99-compatible
  • No standard C library/runtime requirements, perfect for embedded
  • Single, static memory allocation
  • Pathing with lines, quadratic beziers and cubic beziers
  • Path filling with non-zero and even-odd winding
  • Path stroking and dashing
  • 16-bit HDR color support
  • Linear, radial, and sweep gradient brushes
  • Bitmap image brushes
  • Fast path for blurred, rounded rectangles
  • Font management and text rendering ala kb_text_shape.h and stb_truetype.h
  • Modern vector graphics rendering architecture: Potato Architecture
  • Backend agnostic: use WebGPU, WebGL, Vulkan, Metal, Software, or bring your own

Screenshots

blit_terrain.avif clips.avif colors_hdr.avif layers.avif

Quickstart

Windows build have only been tested with MinGW.

# (MacOS) install dependencies
brew install cmake ninja

# (Linux) install dependencies
sudo apt install build-essential ninja-build libxrandr-dev libx11-dev \
    libxinerama-dev libxcursor-dev libxi-dev \
    libxext-dev cmake libwayland-dev wayland-protocols \
    libxkbcommon-dev git pkg-config

# (Windows) requires mingw to compile, along with these dependencies:
#   - cmake ninja-build mingw-w64 git wget unzip pkg-config ca-certificates

# Configure (debug build with UBSan)
cmake --preset debug
# cmake --preset mingw-debug # for windows

# Configure (optimized release build)
cmake --preset release
# cmake --preset mingw-release # for windows

# Build
cmake --build --preset debug
# cmake --build --preset mingw-release # for windows

# List available examples
./build-debug/examples/glfw3-harness/scriblz_examples_glfw3_harness

# Run an example
./build-debug/examples/glfw3-harness/scriblz_examples_glfw3_harness playground

# Render all examples to avif files
./build-debug/examples/glfw3-harness/scriblz_examples_glfw3_harness render

# Render an example directly to an avif file
./build-debug/examples/glfw3-harness/scriblz_examples_glfw3_harness render playground

# Run the example regression tests
./build-debug/examples/glfw3-harness/scriblz_examples_regression

Emscripten Quickstart

# Configure
cmake --preset wasm

# Build
cmake --build --preset wasm

# Run in Chrome
./build-wasm/.emsdk/upstream/emscripten/emrun \
  --browser chrome \
  build-wasm/examples/glfw3-harness/scriblz_examples_glfw3_harness.html

# For a specific example, use the url param "example"
# http://localhost:6931/scriblz_examples_glfw3_harness.html?example=playground

Getting Started

The following example draws a magenta heart centered on a 1024×1024 surface using the WebGPU backend.

Initialization

#define WIDTH 1024
#define HEIGHT 1024

const scriblzp_options p_opts = { .width = WIDTH, .height = HEIGHT };
const scriblzw_options w_opts = { .format = WGPUTextureFormat_BGRA8Unorm };

scriblz_api *api;
if (scriblzw_api_init_alloc(
    &api,
    &p_opts,
    &w_opts,
    device,
    queue,
    NULL
) != scriblz_ok) abort();

Drawing

Using the full scriblz_ API:

WGPUTextureView target = GetSurfaceTextureView();

scriblz_set_target(api, target);
scriblz_clear_target(api, scriblz_color_rgba(0.0, 0.0, 0.0, 0.0));

scriblz_begin_frame(api);
    // draws a magenta heart in the middle of the screen
    scriblz_begin_path(api);
        scriblz_set_fill_rgba(api, 1.0, 0.0, 0.5, 1.0);
        scriblz_mul_transform(api, &(scriblz_transform)SCRIBLZ_TRANSLATE(WIDTH / 2.0, HEIGHT / 2.0));

        scriblz_move_to(api, 0.0, 290.0);
        scriblz_cubic_to(api, -20.0, 270.0, -250.0, 70.0, -250.0, -130.0);
        scriblz_cubic_to(api, -250.0, -260.0, -120.0, -290.0, -40.0, -210.0);
        scriblz_cubic_to(api, -20.0, -190.0, -10.0, -170.0, 0.0, -160.0);
        scriblz_cubic_to(api, 10.0, -170.0, 20.0, -190.0, 40.0, -210.0);
        scriblz_cubic_to(api, 120.0, -290.0, 250.0, -260.0, 250.0, -130.0);
        scriblz_cubic_to(api, 250.0, 70.0, 20.0, 270.0, 0.0, 290.0);
        scriblz_close(api);
    scriblz_end_path(api);
if (scriblz_end_frame(api) != scriblz_ok) abort();

Or using the short vg_ API (disable with #define SCRIBLZ_NO_VG_API):

vg_set_target(api, target);
vg_clear_target(api, scriblz_color_rgba(0.0, 0.0, 0.0, 0.0));

vg_begin_frame(api);
    // draws a magenta heart in the middle of the screen
    vg_begin_path(api);
        vg_set_fill_rgba(api, 1.0, 0.0, 0.5, 1.0);
        vg_mul_transform(api, &(scriblz_transform)SCRIBLZ_TRANSLATE(WIDTH / 2.0, HEIGHT / 2.0));

        vg_move_to(api, 0.0, 290.0);
        vg_cubic_to(api, -20.0, 270.0, -250.0, 70.0, -250.0, -130.0);
        vg_cubic_to(api, -250.0, -260.0, -120.0, -290.0, -40.0, -210.0);
        vg_cubic_to(api, -20.0, -190.0, -10.0, -170.0, 0.0, -160.0);
        vg_cubic_to(api, 10.0, -170.0, 20.0, -190.0, 40.0, -210.0);
        vg_cubic_to(api, 120.0, -290.0, 250.0, -260.0, 250.0, -130.0);
        vg_cubic_to(api, 250.0, 70.0, 20.0, 270.0, 0.0, 290.0);
        vg_close(api);
    vg_end_path(api);
if (vg_end_frame(api) != scriblz_ok) abort();

Playground

The playground is a skeleton for a very simple scene. Edit the file examples/glfw3-harness/playground.h, then run:

  1. cmake --build --preset debug - rebuild playground after modifications
  2. ./build-debug/examples/glfw3-harness/scriblz_examples_glfw3_harness playground - run the playground

High-Level Documentation

Scriblz uses the Potato Architecture, which is a hybrid CPU/GPU rendering architecture. The CPU performs coarse, tile-based rasterization while the GPU performs fine rasterization of each pixel.

It is built to support multiple hardware backends. Currently it only supports WebGPU, but OpenGL ES 2.0 and Software backends are planned for the future.

Eventually, Scriblz will support a pure GPU backend as well, which will require compute shaders to run.


Setting Up

Scriblz follows the single-header library convention. In exactly one .c file, define the feature flags and implementation macros before including the header:

// Feature flags — enable only what you need
#define SCRIBLZ_POTATO     // enable the Potato CPU/GPU renderer
#define SCRIBLZ_WEBGPU     // enable the WebGPU backend
#define SCRIBLZ_TEXT       // enable text rendering (optional)

// Enable the implementation in exactly this one translation unit
#define SCRIBLZ_IMPLEMENTATION
#include "scriblz.h"

All other files include scriblz.h without any defines. Defining SCRIBLZ_IMPLEMENTATION in more than one translation unit will cause linker errors.

Initializing the API requires a scriblzp_options struct (renderer configuration) and a backend-specific options struct. For the WebGPU backend:

const scriblzp_options p_opts = {
    .width  = 1024,
    .height = 1024,
};
const scriblzw_options w_opts = {
    .format = WGPUTextureFormat_BGRA8Unorm,
};

scriblz_api *api;
if (scriblzw_api_init_alloc(&api, &p_opts, &w_opts, device, queue, NULL) != scriblz_ok) {
    abort();
}

Call scriblz_set_size whenever the render buffer is resized.


Error Handling

All scriblz_ API functions return a scriblz_result. Scriblz uses a sticky error model — once any call fails, all subsequent calls on that scriblz_api instance return the same error and perform no work. You do not need to check every call; checking at scriblz_end_frame is the minimum required:

scriblz_result result = scriblz_end_frame(api);
if (result != scriblz_ok) {
    fprintf(stderr, "scriblz error: %s\n", scriblz_result_name(result));
}

scriblz_result_name(result) returns a human-readable string for any result code. The SCRIBLZ_TRY(expr) macro evaluates expr and returns early if it is not scriblz_ok, which is convenient for propagating errors through initialization functions.


Scopes and Transforms

Scriblz manages drawing state — transform, fill and stroke brushes, style, font, and text style — through a scope stack. Each scope inherits its parent's state and can override it without affecting the parent.

The following operations push a scope onto the stack:

Operation Paired pop
scriblz_begin_frame scriblz_end_frame
scriblz_begin_path scriblz_end_path
scriblz_begin_clip scriblz_pop_clip
scriblz_push_layer scriblz_pop_layer
scriblz_push_scope scriblz_pop_scope

Note: scriblz_end_clip does not pop a scope — call scriblz_pop_clip separately to pop both the clip and its scope.

Transforms within a scope accumulate using MVP-style ordering: write outermost (world-space) transforms first and innermost (local-space) transforms last. The most recently applied transform is applied first to vertices. For example, to draw a circle at position (cx, cy) scaled by s:

vg_push_scope(api);
    vg_translate(api, cx, cy);   // outermost — positions the object in the scene
    vg_scale_uniform(api, s);    // innermost — scales within that local space
    vg_move_to(api, 0.0f, 0.0f);
    vg_circle(api, radius);
vg_pop_scope(api);

The scriblz_mul_transform / vg_mul_transform function post-multiplies a raw matrix, following the same ordering: T = T_current × M_new.


Paths

Paths are drawn using a virtual pen. The pen can move in lines, quadratic beziers, cubic beziers, and arcs. Here are some examples showing all of the basic drawing primitives. From top row to bottom row: lines, quadratic beziers, cubic beziers, and arcs.

Paths can be filled based on a fill rule or paths can be outlined with a stroke style. Strokes can be solid or dashed, and the joins and caps can be styled.

paths_outline.avif shapes_dash.avif

Brushes

Brushes are used to paint your paths. Brushes can be solid colors, gradients, bitmap images, or blurred, rounded rectangles.

Colors

colors_hdr.avif

Gradients

Gradients define smooth color transitions between two or more color stops. Three gradient types are available: linear (along a line segment), radial (between two circles), and sweep (rotating around a center point).

Color stops are defined with the SCRIBLZ_STOPS macro. Stops must start at offset 0.0, end at 1.0, and increase monotonically. The extend mode controls behavior beyond the gradient's bounds:

Mode Behavior
pad First and last stop colors extend outward
repeat Gradient pattern tiles
reflect Gradient pattern mirrors and tiles

Gradient coordinates (line, center, start_center/end_center, etc.) are always in screen space, not in the path's local coordinate space. Use the optional transform field to adjust the gradient's coordinate space independently.

gradients_linear_hdr.avif gradients_radial_hdr.avif gradients_sweep_hdr.avif

Images

Images are bitmaps that can be used to paint shapes. They can be reflected, repeated, or padded on the X and Y axes.

images.avif

External Textures

External textures are bitmaps that can be used to paint shapes. They can be reflected, repeated, or padded on the X and Y axes. Textures are completely owned and managed by the user, unlike images, which are managed and used by Scriblz.

While images and textures have many similar uses, and are sometimes interchangable, the big differences are these:

  • Images are used by Scriblz internally for things like glyph caching and intermediate texture rendering
  • External textures are useful for things like video buffers and other external render targets
  • Pretty much any texture data that you don't want Scriblz to manage should be handled as a texture

Blitting External Textures

Using external textures with the fast-path scriblz_blit draw command is useful for drawing many rectangles to the screen quickly.

NOTE: Unlike drawing rectangles (scriblz_rectangle, scriblz_ellipse, etc.) or other paths (scriblz_line_to, scriblz_quad_to, scriblz_cubic_to, etc.) using the other path commands, drawing with scriblz_blit does not go through coarse rendering, and winding numbers are never calculated. So you cannot put negative space into the rectangle when blitting by drawing another path inside of it.

blit_terrain.avif

Blurred, Rounded Rectangles

Blurred, rounded rectangles, blurs for short, are an optimization. Instead of generating geometry for the rounded corners, the rounding and blurring is drawn using the brush. This allows someone to draw rectangles, circles, drop shadows, and other shapes using cheap code path.

See the example code here: examples/glfw3-harness/blur.h

blur_circle_hdr.avif blur_hdr.avif

Clips

Clip paths are arbitrary paths used to clip the content contained inside of them. When creating a new clip path, the contents inside of it are transparent pixels. This is important to know when blending. Clips can be nested — each new clip intersects with any active parent clips.

clips.avif

Blend Layers

Blend layers composite their contents onto the scene using a mix mode and a Porter-Duff composition mode. The layer contents are rendered into an intermediate buffer, then blended onto the scene when the layer is popped.

layers_whale.avif layers.avif

Mix Description
normal Source replaces destination with no blending
multiply Multiplies source and destination colors — always darker than either input
screen Inverts, multiplies, inverts again — always lighter than either input
overlay Multiply where destination is dark, screen where destination is light
darken Keeps whichever of source or destination is darker, per channel
lighten Keeps whichever of source or destination is lighter, per channel
color_dodge Brightens destination to reflect source; white source gives maximum brightness
color_burn Darkens destination to reflect source; black source gives maximum darkness
hard_light Multiply where source is dark, screen where source is light
soft_light Softer version of hard light; source darkens or lightens destination without full multiply/screen
difference Absolute difference between source and destination — identical colors produce black
exclusion Like difference but lower contrast; identical colors produce medium gray
hue Hue of source combined with saturation and luminance of destination
saturation Saturation of source combined with hue and luminance of destination
color Hue and saturation of source combined with luminance of destination
luminosity Luminance of source combined with hue and saturation of destination
Compose Description
clear Output is fully transparent — neither source nor destination is visible
src Only the source layer is visible; destination is discarded
dst Only the destination layer is visible; source is discarded
src_over Source is drawn over destination — standard alpha compositing
dst_over Destination is drawn over source
src_in Source is visible only where destination exists; transparent elsewhere
dst_in Destination is visible only where source exists; transparent elsewhere
src_out Source is visible only where destination does not exist
dst_out Destination is visible only where source does not exist
src_atop Source drawn over destination, but only within the destination bounds
dst_atop Destination drawn over source, but only within the source bounds
xor Source and destination are each shown only where the other does not exist
plus Source and destination colors are added together and clamped to 1
plus_lighter Like plus but alpha is also summed; intended for pre-multiplied light accumulation
Read more
Filters

Recent Activity

&scriblz Update: Scriblz x Clay UI Demo

I've been adding in a bunch of examples to showcase Scriblz (https://codeberg.org/scriblz/scriblz) capabilities.
Here is an example that uses Clay (https://github.com/nicbarker/clay) as the UI layout library and Scriblz
as the rendering backend.

See an interactive browser demo here: https://hollinwilkins.com/scriblz/example.html?example=clay

It showcases some more interesting features like text layout, layer blending, gradients, and images.
Every pixel is rendered with Scriblz and its WebGPU backend.

For full disclosure: AI was used for CMake builds and the examples code. All code in scriblz.h and hw/ folder is my own handwritten

View original message on Discord