2026-05-16

1. Using NanoVG in Odin

Firstly, NanoVG is a small graphical UI library built with OpenGL. It has a similar API to the HTML5 Canvas (i.e.: lineTo() and the like). It is also one of the libraries that come vendored with the Odin programming language.

I sometimes use NanoVG as an alternative to Raylib and SDL2, since:
  1. Raylib does not have fallback font support and nanovg seems to be able to load fonts and draw text a lot faster than either Raylib or SDL2.
  2. Unlike Raylib, it has support for multiple windows!
  3. Unlike SDL, NanoVG is statically linked, so you won't have to deal with DLLs in order for Windows users to start your program.

2. Creating a Window (With GLFW)

GLFW is a library that adds window handling support to OpenGL, Vulkan and more. This can be done with many other libraries. For example, Microslop Windows provides wglCreateContextAttribsARB. By the way, GLFW + Wayland kind of sucks.

First off, you will, likely, need these imports:

import gl   "vendor:OpenGL"     // the GPU API that is used to render stuff
import glfw "vendor:glfw"       // window handling library
import nvg  "vendor:nanovg"     // nanovg's main interface (shapes, text, and so on...)
import ngl  "vendor:nanovg/gl"  // used to CreateContext and CreateFramebuffer


Then creating the actual window is pretty simple: init GLFW, make the window and load part of OpenGL. Just be careful, since every function here is necessary!
1. segfault CreateWindow, MakeContextCurrent or load_up_to were not called;
2. invalid instruction glfw.Init() was not called;
3. version error you need to hint at a version.

init_glfw_window :: proc() {
    assert( bool(glfw.Init()) )
    glfw.WindowHint(glfw.CONTEXT_VERSION_MAJOR, 4)

    window.handle = glfw.CreateWindow(600, 400, "Example window", nil, nil)
    fmt.assertf(window.handle != nil, "Failed to create window with error: \n%v %v\n", glfw.GetError())

    glfw.MakeContextCurrent(window.handle)
    gl.load_up_to(4, 5, glfw.gl_set_proc_address)
}


Then, to initialize NanoVG, you can use the below code block:

init_nanovg_context :: proc() {
    window.ctx = ngl.Create({ .ANTI_ALIAS, .STENCIL_STROKES })
    ngl.CreateFramebuffer(window.ctx, 123, 456, { .REPEAT_X, .REPEAT_Y })
}


And finally, to begin a frame, while allowing the user to resize the window:

begin_frame :: proc() {
    fw, fh := glfw.GetFramebufferSize(window.handle)
    w,  h  := glfw.GetWindowSize(window.handle)
    window.size = { f32(fw), f32(fh) }
    gl.Viewport(0, 0, fw, fh)

    nvg.BeginFrame(window.ctx, f32(w), f32(h), f32(fw) / f32(w))

    // if your window is blinking, this may be missing:
    BACKGROUND := [4] f32 { 20, 23, 31, 255 } / 255
    gl.ClearColor(BACKGROUND.r, BACKGROUND.g, BACKGROUND.b, BACKGROUND.a)
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
}


To run the actual program, you can combine all previous parts. Draw to a buffer. Actually show the graphical buffer you have built up and receive messages from the user.

main :: proc() {
    init_glfw_window()
    init_nanovg_context()
    
    for !glfw.WindowShouldClose(window.handle) {
        begin_frame()

        // ...

        nvg.EndFrame(window.ctx)
        glfw.SwapBuffers(window.handle)
        glfw.WaitEventsTimeout(1) // <-- don't forget this!
    }
}

3. Rendering Stuff

First of all, NanoVG uses 4 floats from 0.0 to 1.0 for colors, sooo... I like making:

nvg_color :: proc(color: [4] u8) -> nvg.Color { 
    return nvg.RGBA(color.r, color.g, color.b, color.a) 
}

Secondly, every shape should start with nvg.BeginPath. And end with nvg.Stroke to draw the path's outline and/or nvg.Fill to fill the outline. nvg.ClosePath may also be used, but it is there to connect first and last points.

Moreover, you should just wrap every color, size and alignment change in:
nvg.Save and nvg.Restore. NanoVG, like OpenGL, is a massive state machine. State machines are hard. You can mitigate some problems by Restoring the previous state as soon as possible.

3.1. Drawing Text

NanoVG does not include a default font, but loading a custom font is quite easy. With #load(filepath) → [] byte you can embed files inside the executable and load fonts from memory. Most software with user input should also have a couple of fallback fonts, like: CJK, math, emoji, etc. Although, color emoji is not supported by anything. Color emoji is ass... 🥀 💀 -🫃.

init_fonts :: proc() {
    nvg.CreateFontMem(window.ctx, "main",  #load("libertinus.ttf"), false)
    nvg.CreateFontMem(window.ctx, "emoji", #load("noto-emoji.ttf"), false)

    // I haven't yet found a single .ttf font with every unicode character
    // So, if a font doesn't have an emoji, NanoVG will fall back to Noto Emoji
    nvg.AddFallbackFont(window.ctx, "main", "emoji") 
                                                     
    // NanoVG uses first loaded font
    // or you can set it at the start of every frame:
    // NOT HERE: nvg.FontFace(window.ctx, "main")
}
P.S. License your fonts! By copy-pasting their OFL / buying their license... (some font files may have licenses in their metadata (in theory), which are enough).

Then, drawing text is as easy as:

text :: proc(text: string, pos: [2] f32, size: f32 = 0, color := [4] u8 {}) -> [2] f32 {
    size  := size  if size  != {} else 18
    color := color if color != {} else { 211, 224, 221, 255 }

    nvg.FillColor(window.ctx, nvg_color(color))
    nvg.FontSize(window.ctx, size)          // needs to be set every frame
    nvg.TextAlignVertical(window.ctx, .TOP) // needs to be set every frame

    end := nvg.Text(window.ctx, pos.x, pos.y, text)
    return { end - pos.x, size } // <-- approximate
}
Just kidding, it's never that easy

3.2. Lines

MoveTo specifies the starting point, although it is not the same as nvg.Translate, which also sets the matrix rotation center. And all other parts are obvious.

line :: proc(start, end: [2] f32, thickness: f32, color: [4] u8) {
    nvg.BeginPath(window.ctx)
    nvg.MoveTo(window.ctx, start.x, start.y)
    nvg.LineTo(window.ctx, end.x,   end.y)

    nvg.StrokeWidth(window.ctx, thickness)
    nvg.StrokeColor(window.ctx, nvg_color(color))
    nvg.Stroke(window.ctx)
}
There are also bezier curves, a couple line joining-methods, and more.

3.3. Filled Shapes

These work, basically, the same way as lines, except FillColor & Fill may be used...

rect :: proc(pos, size: [2] f32, color: [4] u8) { 
    nvg.BeginPath(window.ctx)
    nvg.Rect(window.ctx, pos.x, pos.y, size.x, size.y) 
    nvg.FillColor(window.ctx, nvg_color(color)) 
    nvg.Fill(window.ctx)
}
There is also: Rect, RoundedRect, Ellipse, Circle, ClosePath, Arc and, probably, more...

3.4. Miscellaneous Concepts

GUIs that have scrollable content or text boxes will almost certainly have to clip the content that goes outside of the box. This can be done with nvg.Scissor.

text_input :: proc(state: TextInput, pos, size: [2] f32) {
    // draw background rectangle
    // draw border
    
    nvg.Scissor(window.ctx, pos.x - 1, pos.y, size.x + 2, size.y)
    // draw text
    nvg.ResetScissor(window.ctx)

    // draw cursor if text input has focus
}


Another concept you might occasionally need is transform matrices. NanoVG has: Rotate, Translate, Scale, SkewX and SkewY to set up a matrix and ResetTransform to clean up then and there (instead of at the end of the frame).
Just be aware that nvg.Rotate rotates neither around the shape's center, nor it's top-left corner. Instead, it rotates around the top-left corner of the window (the coordinate {0, 0}). To rotate the shape around another origin, use nvg.Translate.

PI :: 3.14159 // better version in core:math

size := [2] f32 { 32, 32 }
pos  := window.size / 4

// rotate rectangle around top-left corner
nvg.Translate(window.ctx, pos.x, pos.y)
nvg.Rotate(window.ctx, PI/4)
rect({ 0, 0 }, size, { 192, 192, 63, 255 })
nvg.ResetTransform(window.ctx)

pos.x += window.size.x / 4

// rotate rectangle around its center
nvg.Translate(window.ctx, pos.x, pos.y)
nvg.Rotate(window.ctx, PI/8)
rect(-size/2, size, { 63, 192, 63, 255 })
nvg.ResetTransform(window.ctx)

4. Extras

  1. The full source code can be found at codeberg.org/ulti/nanovg-odin
  2. Project that uses NanoVG with Raylib and implements a text input, scrollbar and more (but has messy code) can be found at codeberg.org/ulti/utxt
  3. Older project that supports multiple windows can be found at codeberg.org/ulti/frigg
  4. Obviously, NanoVG source code can be found at github.com/memononen/nanovg