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

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:
cmake --build --preset debug- rebuild playground after modifications./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_clipdoes not pop a scope — callscriblz_pop_clipseparately 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.

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

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.

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

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.

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

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.

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.

| 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 |