Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

raysvg

SVG loading and rendering for raylib.

raysvg parses an SVG into a retained scene graph: geometry is tessellated once and cached, then redrawn each frame through rlgl. Groups stay addressable after load, so you can look elements up by id or class and drive them with per-element channels — transform, pivot, opacity, display, z-index, reparenting, outline, and CSS variables — without re-parsing or re-tessellating.

It works with stock raylib and with the custom backends used by rayact — rlvk (Vulkan), rlwg (WebGPU), rlmt (Metal).

RaysvgDoc *doc = RaysvgLoadFromFile("icon.svg");

while (!WindowShouldClose()) {
    BeginDrawing();
    ClearBackground(RAYWHITE);
    RaysvgDraw(doc, (Rectangle){ 40, 40, 400, 400 });
    EndDrawing();
}

RaysvgUnload(doc);

Animate or recolour any element that has an id:

RaysvgHandle wheel = RaysvgGetElementById(doc, "wheel");
RaysvgSetTransform(doc, wheel, 0, 0, angle, 1.0f, 1.0f);
RaysvgSetVar(doc, "--accent", (Color){ 224, 83, 61, 255 });

The full API is a single header: include/raysvg.h.

Building

raysvg never links raylib itself. It needs raylib.h and rlgl.h at compile time, and the rl* symbols resolve against whatever backend the host application links. That is what lets one build of the source run on desktop GL, Vulkan, Metal and WebGPU.

cmake -S . -B build -DRAYSVG_RAYLIB_DIR=/path/to/raylib
cmake --build build -j
ctest --test-dir build --output-on-failure

As a subproject, add_subdirectory(raysvg) and link raysvg; if a raylib target already exists it is picked up automatically.

Demos

./build/demo_static tests/fixtures/shapes.svg
./build/demo_static tests/fixtures/critter.svg --outline blob-outline
./build/demo_channels tests/fixtures/critter.svg
./build/raysvg_dump some.svg

shapes.svg exercises primitives, gradients, strokes, clips and var(). critter.svg is a small layered fixture used by demo_channels to show transforms, opacity, outline, z-index and reparenting. Both demos take --screenshot out.png to render one frame headlessly and exit.

What is supported

Elements svg g path rect (with rx/ry) circle ellipse line polyline polygon defs clipPath linearGradient stop.

Path data all commands — M L H V C S Q T A Z, absolute and relative. Arcs are converted to cubics via the endpoint-to-centre parameterisation in SVG 1.1 appendix F.6.

Paint hex (3/4/6/8 digit), rgb()/rgba(), hsl()/hsla(), all CSS named colours, none, currentColor, url(#gradient), and var(--name) with an optional fallback. Variables resolve at draw time, so one RaysvgSetVar recolours everything referencing it.

Stroke width, butt/round/square caps, miter/round/bevel joins, miter limit, dash arrays and dash offset.

Other viewBox, preserveAspectRatio (all alignments, meet and slice), nested transform chains, transform-origin, opacity/fill-opacity/stroke-opacity, display, z-index, clip-path with clipPath, and an inline style attribute.

Not supported

<text>, <use>, <image>, <symbol>, <mask>, <marker>, radial gradients, patterns, filters, SMIL animation, and fill-rule (paths are filled by containment nesting). There is no CSS cascade — a <style> block is ignored, and class names are kept for queries only. Bake stylesheet rules into attributes first (see tools/convert-rig if you start from layered HTML/CSS rather than a single SVG).

Unsupported constructs warn instead of failing: see RaysvgGetWarnings.

Design notes

Clipping is done on the CPU. Triangles are clipped against a convex polygon (Sutherland–Hodgman) in the shape's own local space, and the clip polygon is carried down the tree by the inverse of each local transform. A clip therefore belongs to the referencing element's content space, which is what lets an animated child slide under a stationary clip. Concave clip outlines fall back to their bounding box, reported as a warning.

The outline pass replaces feMorphology. RaysvgSetOutline sweeps the subtree twice: once emitting a fat round-joined stroke of every contour, then again for the artwork. Adjacent parts merge into one silhouette rather than each being ringed individually. It needs no offscreen target and no shader.

Gradients are a 256×1 ramp texture plus per-vertex UVs projected onto the gradient axis, built lazily on first draw. RaysvgNotifyGpuReset drops them after a device loss.

Frame scheduling. Channel writes set a dirty flag that RaysvgNeedsRedraw reports and drawing clears. Writing a value identical to the current one does not dirty the document, so a host that re-applies a static pose every frame still goes idle.

Portability contract

The renderer restricts itself to the rlgl subset that behaves identically on stock raylib, rlvk, rlwg and rlmt. These constraints are not stylistic; each one is a bug that was hit:

Rule Why
Never rlMultMatrixf Stock raylib and rlwg compose it as mul(new, current); rlvk and rlmt as mul(current, new). Nested transforms would nest backwards on half the backends. Only rlTranslatef/rlRotatef/rlScalef agree, so non-decomposable matrices (shears) are folded into vertices on the CPU instead.
Never rlSetTexture(0) to unbind It clears rlgl's current-texture field without retargeting the open draw call, so a gradient ramp keeps tinting everything drawn after it. Use rlSetTexture(rlGetTextureIdDefault()).
Normalise triangle winding raylib leaves backface culling on. earcut output and stroke join wedges have no predictable orientation, so cached geometry is normalised once at tessellation time and flipped per-triangle when a mirroring transform is active. Toggling rlDisableBackfaceCulling does not work: stock rlgl does not flush the batch, so the state at flush time is whatever the host left.
No stencil rlvk writes stencil state that never reaches a pipeline; rlwg and rlmt stub the entry points entirely. Hence CPU clipping.
No custom shaders Three hand-maintained variants (GLSL, MSL, WGSL) and rlwg cannot compile at runtime. Hence ramp textures for gradients.
No rlDrawVertexArray* No-ops on rlwg. Everything goes through the immediate-mode batch.

Emission is chunked to 1512 vertices per rlBegin/rlEnd with a rlCheckRenderBatchLimit ahead of each run, which keeps every backend flush well inside rlmt's 32k-vertex ceiling.

Optional: converting layered HTML/CSS into one SVG

Most users can ignore this. If your artwork already lives as a single .svg, load it directly.

tools/convert-rig is for the case where art is authored as stacked HTML <div> layers (each wrapping its own <svg>) plus a stylesheet. It flattens that into one document raysvg can drive:

cd tools/convert-rig
npm install          # or symlink an existing node_modules with htmlparser2 + css-tree
node convert.mjs --rig /path/to/your/layers

--rig has no default: the output encodes whatever input you point it at. Generated files land in assets/ by default (gitignored) — produce them locally rather than committing them here.

The converter writes:

  • rig.svg — layers as <g> elements in paint order, class-derived presentation baked into inline styles, transform-origin preserved as declared.
  • rig-manifest.json — pivots, visibility rules, palettes, outline configuration, and element ids an animation layer may write to.
  • rig-preview.svg — one resolved state, useful for demos that should not need a JSON reader. Pick a state with --state "<space-separated class list>".

Unrecognised selectors are a hard error, not a silent drop: an authoring change should break the conversion rather than the rendering.

Licence

zlib/libpng. Vendored: earcut.hpp (ISC, Mapbox). The elliptical-arc conversion follows the approach in nanosvg (zlib, Mikko Mononen).

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages