NanoVG-REPL is a simple wrapper around NanoVG, a small antialiased
vector graphics rendering library for OpenGL. It makes NanoVG’s API
accessible through a simple text protocol. In other words,
NanoVG-REPL provides a Read-Eval-Print Loop for running NanoVG
commands. You can easily use it from any programming language. Just
make your program send commands to stdout
, read responses from
stdin
, and listen for asychronous events (e.g. keyboard and mouse
events) on a named pipe.
Communicating with the graphics system over pipes is not as fast as making direct foreign function calls. However, for simple animations, it’s easy to reach more than 200 frames per second on modern hardware this way. For many applications, the benefits of isolating C code into a separate process and using a simple, portable text protocol outweigh the drawbacks. It’s easy to experiment, so it’s a great way to get started on a new idea. The graphics primitives are universal, so if performance becomes an issue, it should be possible to port your program to another, faster system.
NanoVG-REPL includes a wrapper that makes it accessible from Scheme. It has been ported to Chibi Scheme and MIT/GNU Scheme. Porting it to other Schemes — or even other languages — should be straightforward.
While NanoVG itself is known to work on Linux, MacOS, and Windows,
I’ve only tested NanoVG-REPL on Linux and MacOS. At least one
part of the Scheme wrapper, make-fifo
, needs porting to work on
Windows.
The animation above shows Stomachion, one of the demos in the
examples/
directory.
- NanoVG and NanoVG-REPL must be in peer directories, i.e. in directories that have the same parent directory.
- Premake must be installed.
- All the dependencies NanoVG requires for your platform, e.g. GLFW
and OpenGL, must be installed. For example, on Linux:
sudo apt install libglew-dev sudo apt install libglfw3-dev
- Build NanoVG:
cd ~/project/nanovg/ premake5 gmake # For Linux. See Premake documentation for other platforms. cd build/ make nanovg
- Build NanoVG-REPL:
cd ~/project/nanovg-repl/ premake5 gmake # For Linux. See Premake documentation for other platforms. cd build/ make repl
To get an idea of how NanoVG-REPL works, take a look at these
examples, which are written as bash
shell scripts in order to prove
that they’re entirely portable even to least-common-denominator
languages:
image.sh
, which displays a rotating imagemoire.sh
, which displays a flashing Moiré patternstomachion.sh
, which displays a rotating Ostomachion puzzle
There are Scheme versions of each of those demos, too:
image.scm
moire.scm
stomachion.scm
To run the Stomachion demo under bash
:
cd nanovg-repl/examples/ && ./stomachion.sh
To run the Image demo under bash
:
cd nanovg-repl/examples/ && ./image.sh
To run the Moire demo under bash
:
cd nanovg-repl/examples/ && ./moire.sh
To run the Stomachion demo under Chibi Scheme:
cd nanovg-repl/
chibi-scheme \
-I src/ \
-I examples/ \
-x '(stomachion)' \
-e '(stomachion ".")'
To run the Image demo under Chibi Scheme:
cd nanovg-repl/
chibi-scheme \
-I src/ \
-I examples/ \
-x '(image)' \
-e '(image-demo ".")'
To run the Moire demo under Chibi Scheme:
cd nanovg-repl/
chibi-scheme \
-I src/ \
-I examples/ \
-x '(moire)' \
-e '(moire ".")'
To run the demos under MIT/GNU Scheme version 11.2 and later, first:
cd nanovg-repl/
scheme
(load-option 'synchronous-subprocess)
(find-scheme-libraries! ".")
Then, for the Stomachion demo:
,(import (stomachion))
(stomachion ".")
or for the Image demo:
,(import (image))
(image-demo ".")
or for the Moire demo:
,(import (moire))
(moire ".")
The NanoVG-REPL protocol follows NanoVG’s API closely, but not exactly. There are NanoVG commands for most NanoVG functions, but some are omitted. Some commands don’t correspond to any NanoVG function, e.g. commands for turning on and off delivery of keyboard, mouse, and window events.
Before your program begins sending commands to NanoVG-REPL, it must
start the repl
program, passing it the desired initial window width
and height, the window title, and the filename of a named pipe that
you have already created. The pipe will be used to deliver
asynchronous events to your program.
repl
initial-window-width initial-window-height window-title events-filename
For example:
rm -f /tmp/nanovg-repl-events
mkfifo -m 600 /tmp/nanovg-repl-events
repl 1024 768 "Hello, world." /tmp/nanovg-repl-events
Once the NanoVG-REPL subprocess has been started, it displays its
window. The subprocess will accept commands, one per line, on
stdin
. Any return values for a command will be printed on a single
line on stdout
.
Any asynchronous events, including keyboard or mouse input or notification of window size changes, will be delivered on the named pipe events-filename. (See command line.) The caller should make sure to read that named pipe frequently so that it can act on those events.
The subprocess will continue running until either an error occurs or
the shutdown
command is invoked. When either of those happens, the
subprocess will close the window and exit.
Each command takes specific argument types, separated by spaces. Each
type is represented using its standard textual representation from C’s
printf
. The types are:
byte
(Cunsigned char
)Boolean values are represented by
byte
.1
is true, and0
is false.float
For example:
3.14159265359
int
For example:
-123
string
(Cchar *
)Strings are delivered as two values: a byte count, followed by a space, followed by the bytes that make up the string. For example:
13 Hello, world.
When the NanoVG protocol expects a color, it is broken down into the
four RGBA components, each as a separate argument. In most cases,
each is a byte
. For example, here’s a translucent green:
0 255 0 127
The float
and int
types are also used for return values. Some
commands return more than one value, or an array of values. For
example, current-transform
returns six floats, which are listed as
float[6]
in the documentation, and might appear like this in a
response:
1.000000 0.000000 0.000000 1.000000 0.000000 0.000000
Some NanoVG functions return paint values (NVGpaint
). The
corresponding commands register the value in a table, then return an
ID instead. When the caller is finished with a paint value, it must
invoke unregister
on the value’s ID. This allows NanoVG-REPL to
reclaim the corresponding memory.
There are other return values that are IDs, e.g. font IDs and image
IDs, but unregister
is only used for paint values.
The delete-image
command is used to reclaim the memory used by an
image.
Each command below is listed with its arguments and their types. If the command returns any values, they are listed after the “→” arrow. Except where noted, each command does the same thing as the similarly named NanoVG function.
add-fallback-font
string base-font, string fallback-fontadd-fallback-font-id
int base-font, int fallback-fontarc
float cx, float cy, float r, float a0, float a1, int dirarc-to
float x1, float y1, float x2, float y2, float radiusbegin-frame
float window-width, float window-height, float device-pixel-ratiobegin-path
bezier-to
float c1x, float c1y, float c2x, float c2y, float x, float ybox-gradient
float x, float y, float w, float h, float r, float f, byte icolr, byte icolg, byte icolb, byte icola, byte ocolr, byte ocolg, byte ocolb, byte ocola → int paint-idcircle
float cx, float cy, float rclear
byte color-buffer-bit, byte depth-buffer-bit, byte stencil-buffer-bitThis command corresponds to
glClear
. Each byte is a Boolean value representing whether that bit is turned on.clear-color
float r, float g, float b, float aclose-path
close-window
This command corresponds to
glfwSetWindowShouldClose
withGL_TRUE
.create-font
string name, string filename → int font-idcreate-font-at-index
string name, string filename, int index → int font-idcreate-image
string filename, int image-flags → int image-idcurrent-transform
→ float[6] transformdelete-image
int imageellipse
float cx, float cy, float rx, float ryend-frame
fill
fill-color
byte r, byte g, byte b, byte afill-paint
int paint-idfind-font
string name → int font-idfont-blur
float blurfont-face
string fontfont-face-id
int fontfont-size
float sizeframe-buffer-size
→ int fb-width, int fb-heightThis command corresponds to
glfwGetFramebufferSize
.global-alpha
float alphaimage-pattern
float cx, float cy, float w, float h, float angle, int image, float alpha → int paint-idimage-size
int image → int w, int hintersect-scissor
float x, float y, float w, float hkey-input-events
byte onThis command turns delivery of key input events on or off. It corresponds to
glfwSetKeyCallback
.linear-gradient
float sx, float sy, float ex, float ey, byte icolr, byte icolg, byte icolb, byte icola, byte ocolr, byte ocolg, byte ocolb, byte ocola → int paint-idline-cap
int capline-join
int joinline-to
float x, float ymiter-limit
float limitmouse-button-events
byte onThis command turns delivery of mouse button events on or off. It corresponds to
glfwSetMouseButtonCallback
.mouse-position-events
byte onThis command turns delivery of mouse position events on or off. It corresponds to
glfwSetCursorPosCallback
.move-to
float x, float ypath-winding
int dirping
string stringpoll-events
This command corresponds to
glfwPollEvents
.quad-to
float cx, float cy, float x, float yradial-gradient
float cx, float cy, float inr, float outr, byte icolr, byte icolg, byte icolb, byte icola, byte ocolr, byte ocolg, byte ocolb, byte ocola → int paint-idrect
float x, float y, float w, float hreset
reset-fallback-fonts
string base-fontreset-fallback-fonts-id
int base-fontreset-scissor
reset-transform
restore
rotate
float anglerounded-rect
float x, float y, float w, float h, float rrounded-rect-varying
float x, float y, float w, float h, float rad-top-left, float rad-top-right, float rad-bottom-right, float rad-bottom-leftsave
scale
float x, float yscissor
float x, float y, float w, float hshape-anti-alias
int enabledshutdown
This command closes the NanoVG-REPL window and exits its process.
skew-x
float angleskew-y
float anglestroke
stroke-color
byte r, byte g, byte b, byte astroke-paint
int paint-idstroke-width
float widthswap-buffers
This command correpsonds to
glfwSwapBuffers
.text
float x, float y, string string → float resulttext-align
int aligntext-bounds
float x, float y, string string → float result, float[4] boundstext-box
float x, float y, float break-row-width, string stringtext-box-bounds
float x, float y, float break-row-width, string string → float[4] boundstext-input-events
byte onThis command turns delivery of text input events on or off. It corresponds to
glfwSetCharCallback
.text-letter-spacing
float spacingtext-line-height
float line-heighttext-metrics
→ float ascender, float descender, float linehtransform
float a, float b, float c, float d, float e, float ftranslate
float x, float yunregister
int idThis command is used to free memory associated with paint objects returned by other commands.
viewport
int x, int y, int w, int hThis corresponds to
glViewport
.window-size
→ int window-width, int win-heightThis command corresponds to
glfwGetWindowSize
.window-should-close?
This command corresponds to
glfwWindowShouldClose
.window-size-change-events
byte onThis command corresponds to
glfwSetWindowSizeCallback
.
Asynchronous events are delivered on the named pipe events-filename. Events are encoded like commands, with spaces separating their arguments.
key-input
int key, int code, int modsThis event corresponds to the callback set by
glfwSetKeyCallback
. Only key presses, not releases, are delivered.mouse-button
int button, int action, int modsThis event corresponds to the callback set by
glfwSetMouseButtonCallback
.mouse-position
float xpos, float yposThis event corresponds to the callback set by
glfwSetCursorPosCallback
.text-input
int code-pointThis event corresponds to the callback set by
glfwSetCharCallback
.window-size-changed
int width, int heightThis event corresponds to the callback set by
glfwSetWindowSizeCallback
.
The Scheme API for NanoVG-REPL corresponds closely to the protocol described above. There is a procedure corresponding to each command listed above.
The argument types and return types of the procedures are the same as
those of the corresponding commands except that the following
procedures take a Scheme Boolean value instead of a 1
or 0
:
key-input-events
mouse-button-events
mouse-position-events
text-input-events
window-size-change-events
and the window-should-close?
procedure returns a Boolean.
To create a window using the Scheme API, call make-nanovg-window
.
It takes the pathname of the repl
program, an initial width and
height, and a window title:
(
make-nanovg-window
repl-pathname initial-window-width initial-window-height window-title)
For example:
(make-nanovg-window "nanovg-repl/build/repl" 800 800 "Example Window Title")
Most procedures in the Scheme API take an implicit parameter, the
current window. This is set in a dynamic scope using Scheme’s
standard parameterize
syntax. For example:
(parameterize ((current-nanovg-window (make-nanovg-window ...)))
do-something)
In order for programs to handle events, they must check for them at least periodically. This can be done in a separate thread, or it can be done by checking frequently in the same thread in which drawing occurs.
To read an event, call read-nanovg-event
. It returns two values:
the event name and a list of the event’s arguments. If there is no
event ready, it returns #f
for both.
For convenience, dispatch-event
can be used instead. It provides a
simple way to declare what should be done if no event is ready, what
should be done for each possible event type, and what should be done
in every other case.
(dispatch-event [(no-events action ...)]
((event-name) action ...) ...
[(else action ...)])
For example:
(dispatch-event (no-events #f)
((mouse-button button action mods) '(click))
((mouse-position xpos ypos) `(position ,xpos ,ypos))
(else (error "Unexpected event.")))
When make-nanovg-window
is called to create a window, it creates a
named pipe as well. Once the current NanoVG-REPL window has been
closed using shutdown
, it’s good to delete that named pipe. There
are two ways to do that: manual and automatic. To delete it manually,
call delete-nanovg-fifo
. To delete it automatically, wrap all the
code using the window in nanovg-cleanup
:
(parameterize ((current-nanovg-window
(make-nanovg-window repl-pathname
width
height
"Example Window Title")))
(nanovg-cleanup
(lambda ()
;; ... Use the window.
)))
When nanovg-cleanup
returns, even due to an error, it will delete
the named pipe.