Add draw-filled-circle and additive blend mode for v0.9.0
draw-filled-circle x y radius [segments] renders a triangle fan via sgpdrawfilled_triangles. Stack-allocated buffer, no per-circle heap allocation, safe to call thousands of times per frame for bullet-style rendering. segments defaults to 16 and is clamped to [3, 128].
set-blend-mode 'normal|'additive|'none, reset-blend-mode, and the with-blend-mode / with-additive-blend macros expose sokolgp's blend state. The Sigil wrapper shadows the mode in module state so with-blend-mode can restore the prior mode rather than blindly resetting to 'normal — sokolgp itself has no getblendmode. The C natives are registered with %-prefix as private; callers should go through the public Sigil wrappers to keep the shadow in sync.
examples/circles-blend/ ships a side-by-side normal-vs-additive demo of overlapping coloured circles for visual verification.
Skips v0.8.0: that git tag was used 2026-03-29 for the initial standalone-repo extraction without a matching package.sgl version bump. Jumping to v0.9.0 avoids rewriting a published tag.
.gitignore | 3 +++
CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++
examples/circles-blend/dev-redirects.sgl | 12 ++++++++++++
examples/circles-blend/package.sgl | 33 +++++++++++++++++++++++++++++++++
examples/circles-blend/src/circles-blend/main.sgl | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
package.sgl | 2 +-
src/c/graphics.c | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/sigil/graphics.sgl | 42 +++++++++++++++++++++++++++++++++++++++++-
8 files changed, 347 insertions(+), 2 deletions(-).gitignoremodified
build/.sigil/examples/*/build/examples/*/.sigil/CHANGELOG.mdmodified
The format is based on [Keep a Changelog](https://keepachangelog.com/),and this project adheres to [Semantic Versioning](https://semver.org/).## [0.9.0] - 2026-04-18> Note: skips 0.8.0. The `v0.8.0` git tag was used on 2026-03-29 for the> initial standalone-repo extraction, but `package.sgl` was never bumped> past 0.7.0 then. To avoid rewriting a published tag, this minor release> jumps to 0.9.0.### Added- `draw-filled-circle x y radius [segments]` — filled circle primitive rendered as a triangle fan via sokol_gp. `segments` defaults to 16 and is clamped to `[3, 128]`. For tiny shapes (radius 4–6 px), 12–16 segments is plenty; larger circles benefit from more. Draw cost scales linearly with `segments`, so use the smallest count that still looks round at the target radius — bullet/particle code on hot paths should pick a fixed low count rather than letting it default. Colour comes from the currently bound `set-color`.- `set-blend-mode mode` and `reset-blend-mode` — Sigil-level blend mode control. `mode` is one of `'normal` (alpha blend), `'additive`, or `'none` (no blending). The Sigil wrapper shadows the mode in module state so `with-blend-mode` can restore the previous value.- `with-blend-mode mode body ...` — scoped form that sets `mode`, runs `body`, then restores the previous mode. `with-additive-blend body ...` is the recommended convenience wrapper for particle / bullet halo rendering — it avoids forgotten restores that would leak the blend state into other draws.- `current-blend-mode` — accessor for the Sigil-tracked blend mode.### Notes- Only `'normal`, `'additive`, and `'none` are exposed. sokol_gp also supports premultiplied-alpha, modulate, and multiply modes; those can be added when something needs them. Adding a new mode is a one-line change in `blend_mode_from_value` (`src/c/graphics.c`).- See `examples/circles-blend/` for a side-by-side normal-vs-additive demo. Note: the demo binary build currently fails in the transitive `sigil-lib` rebuild on a missing `minicoro.h` include path — this is a pre-existing build-infra bug, not in this package.## [0.5.0] - 2026-02-17### Addedexamples/circles-blend/dev-redirects.sgladded
;; Local sigil-graphics is the in-progress version (v0.8.0-dev) we're demoing.(redirects repos: (list (for-repo url: "codeberg:sigil/sigil" use: (from-path dir: "../../../sigil")) (for-repo url: "codeberg:sigil/sigil-app" use: (from-path dir: "../../../sigil-app")) (for-repo url: "codeberg:sigil/sigil-graphics" use: (from-path dir: "../.."))))examples/circles-blend/package.sgladded
;;; circles-blend - draw-filled-circle + additive-blend demo for sigil-graphics;;;;;; Renders a grid of overlapping coloured circles, alternating each second;;; between normal alpha blending (left half) and additive blending (right;;; half) so the brightening effect is visible. Quits after ~6 seconds or;;; when escape is pressed.(define sigil-repo "codeberg:sigil/sigil")(define app-repo "codeberg:sigil/sigil-app")(define graphics-repo "codeberg:sigil/sigil-graphics")(package name: "circles-blend" version: "0.0.1" description: "sigil-graphics demo: filled circles + additive blending" license: "BSD-3-Clause" authors: (list "David Wilson <[email protected]>") entry: '(circles-blend main) bundle-name: "circles-blend" configs: (list (config name: 'dev output-dir: "build/dev" debug?: #t optimize: 0 bundle?: #t)) dependencies: (list (from-git url: sigil-repo package: "sigil-stdlib" version: "^0.9.0") (from-git url: app-repo version: "^0.8.0") (from-git url: graphics-repo version: "^0.9.0")))examples/circles-blend/src/circles-blend/main.sgladded
;;; circles-blend / main - draw-filled-circle + additive blend demo;;;;;; A 5x4 grid of overlapping coloured circles is rendered twice, side by;;; side: left half uses 'normal alpha blending, right half uses 'additive.;;; Where additive circles overlap they brighten toward white, exactly the;;; particle/halo effect Cinder Cantata wants. Quits after ~6 seconds or;;; when escape is pressed.(define-library (circles-blend main) (import (sigil core) (sigil math) (sigil app) (sigil graphics)) (export main) (begin (define WINDOW-W 960) (define WINDOW-H 540) (define HALF-W-F 480.0) (define WINDOW-H-F 540.0) ;; Grid layout — circles are large enough to overlap so the blend ;; difference is obvious. (define COLS 5) (define ROWS 4) (define RADIUS 60.0) (define SPACING-X 80.0) (define SPACING-Y 80.0) ;; Five strongly saturated colours that overlap into white when summed ;; under additive blending — the design-doc target for bullet halos. (define palette (list (list 1.0 0.2 0.2 0.6) ; red (list 0.2 1.0 0.2 0.6) ; green (list 0.2 0.4 1.0 0.6) ; blue (list 1.0 1.0 0.2 0.6) ; yellow (list 1.0 0.2 1.0 0.6))) ; magenta (define (palette-color i) (list-ref palette (modulo i (length palette)))) (define (draw-grid origin-x mode elapsed) ;; Pulse radius gently with elapsed time so motion makes the blend ;; effect obvious; static frames are still readable. (let* ((pulse (+ 1.0 (* 0.15 (sin (* 2.0 elapsed))))) (r (* RADIUS pulse))) (with-blend-mode mode (let row-loop ((row 0)) (when (< row ROWS) (let col-loop ((col 0)) (when (< col COLS) (let* ((idx (+ (* row COLS) col)) (color (palette-color idx)) (cx (+ origin-x 80.0 (* col SPACING-X))) (cy (+ 120.0 (* row SPACING-Y)))) (apply set-color color) (draw-filled-circle cx cy r 24)) (col-loop (+ col 1)))) (row-loop (+ row 1))))))) ;; Background reference rectangles so additive brightening is obvious ;; against something other than pitch-black. (define (draw-backdrop) (set-color 0.08 0.08 0.10 1.0) (draw-filled-rect 0.0 0.0 HALF-W-F WINDOW-H-F) (set-color 0.06 0.06 0.08 1.0) (draw-filled-rect HALF-W-F 0.0 HALF-W-F WINDOW-H-F)) (define (game-loop) (gfx-setup) (set-viewport WINDOW-W WINDOW-H) (let loop ((elapsed 0.0)) (let ((dt (wait-frame))) (with-frame (clear-screen 0.0 0.0 0.0 1.0) (draw-backdrop) ;; Left: normal alpha blending — overlaps stack but don't brighten. (draw-grid 0 'normal elapsed) ;; Right: additive — overlaps brighten toward white. (draw-grid HALF-W-F 'additive elapsed)) (cond ((key-pressed? 'escape) (request-quit)) ((quit-requested?) #t) ((> elapsed 6.0) (request-quit)) (else (loop (+ elapsed dt)))))) (gfx-shutdown)) (define (main . _args) (run-game "sigil-graphics circles + blend demo" WINDOW-W WINDOW-H game-loop) 0)))package.sglmodified
(package name: "sigil-graphics" version: "0.7.0" version: "0.9.0" description: "2D graphics, image loading, and font rendering for Sigil" url: "https://codeberg.org/sigil/sigil-graphics" license: "BSD-3-Clause"src/c/graphics.cmodified
#include <stdio.h>#include <stdlib.h>#include <stdbool.h>#include <string.h>#include <math.h>#ifndef M_PI#define M_PI 3.14159265358979323846#endif/* Maximum segments for a circle triangle fan. Stack-allocated buffer is sized * to this. Higher values produce smoother circles at the cost of more triangles * per draw call. 64 is plenty for typical bullet/UI usage. */#define SIGIL_GFX_CIRCLE_MAX_SEGMENTS 128/* External: get pixel data from image (defined in image.c) */extern unsigned char *sigil_graphics_image_pixels(SigilVM *vm, Value img_val, int *width, int *height); return SIGIL_NIL;}/* * (draw-filled-circle x y radius [segments]) - Draw a filled circle * * Renders the circle as a triangle fan around (x, y). `segments` controls * the polygon resolution; default is 16. For tiny bullets (radius 4-6 px), * 12-16 is plenty. Larger circles benefit from more segments. Clamped to * [3, SIGIL_GFX_CIRCLE_MAX_SEGMENTS]. */static Value native_draw_filled_circle(SigilVM *vm, int argc, Value *args){ if (argc < 3) { sigil__vm_error(vm, SIGIL_ERR_ARITY, "draw-filled-circle: requires x, y, radius arguments"); return SIGIL_UNDEFINED; } float cx = value_to_float(args[0]); float cy = value_to_float(args[1]); float radius = value_to_float(args[2]); int segments = 16; if (argc > 3) { segments = (int)value_to_float(args[3]); } if (segments < 3) segments = 3; if (segments > SIGIL_GFX_CIRCLE_MAX_SEGMENTS) { segments = SIGIL_GFX_CIRCLE_MAX_SEGMENTS; } if (radius <= 0.0f) { return SIGIL_NIL; } sgp_triangle tris[SIGIL_GFX_CIRCLE_MAX_SEGMENTS]; float step = (float)(2.0 * M_PI) / (float)segments; float prev_x = cx + radius; float prev_y = cy; for (int i = 1; i <= segments; ++i) { float angle = step * (float)i; float nx = cx + radius * cosf(angle); float ny = cy + radius * sinf(angle); tris[i - 1].a.x = cx; tris[i - 1].a.y = cy; tris[i - 1].b.x = prev_x; tris[i - 1].b.y = prev_y; tris[i - 1].c.x = nx; tris[i - 1].c.y = ny; prev_x = nx; prev_y = ny; } sgp_draw_filled_triangles(tris, (uint32_t)segments); return SIGIL_NIL;}/* ============================================================ * BLEND MODES * ============================================================ *//* Map a Sigil symbol value to an sgp_blend_mode. Returns -1 if unknown. */static int blend_mode_from_value(Value v){ if (sigil_is_symbol(v)) { const char *name = sigil_symbol_name(v); if (name) { if (strcmp(name, "normal") == 0) return SGP_BLENDMODE_BLEND; if (strcmp(name, "additive") == 0) return SGP_BLENDMODE_ADD; if (strcmp(name, "none") == 0) return SGP_BLENDMODE_NONE; } } return -1;}/* * (set-blend-mode mode) - Set the current blend mode * * mode is one of: 'normal (alpha blend), 'additive, 'none. * Stays in effect until changed or reset-blend-mode is called. */static Value native_set_blend_mode(SigilVM *vm, int argc, Value *args){ if (argc < 1) { sigil__vm_error(vm, SIGIL_ERR_ARITY, "set-blend-mode: requires mode symbol"); return SIGIL_UNDEFINED; } int mode = blend_mode_from_value(args[0]); if (mode < 0) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "set-blend-mode: expected 'normal, 'additive, or 'none"); return SIGIL_UNDEFINED; } sgp_set_blend_mode((sgp_blend_mode)mode); return SIGIL_NIL;}/* * (reset-blend-mode) - Reset blend mode to sokol_gp default (no blending) */static Value native_reset_blend_mode(SigilVM *vm, int argc, Value *args){ (void)vm; (void)argc; (void)args; sgp_reset_blend_mode(); return SIGIL_NIL;}/* ============================================================ * TRANSFORM STACK * ============================================================ */ SIGIL_ARITY_EXACT(6), "Draw triangle outline"); sigil_module_register_native(vm, "fill-triangle", native_fill_triangle, SIGIL_ARITY_EXACT(6), "Draw filled triangle"); sigil_module_register_native(vm, "draw-filled-circle", native_draw_filled_circle, SIGIL_ARITY_RANGE(3, 4), "Draw filled circle"); /* Blend mode (raw C primitives — wrapped by Sigil set-blend-mode for * tracking; users should prefer the Sigil wrapper or with-blend-mode.) */ sigil_module_register_native(vm, "%set-blend-mode-native", native_set_blend_mode, SIGIL_ARITY_EXACT(1), "Set blend mode ('normal, 'additive, 'none)"); sigil_module_register_native(vm, "%reset-blend-mode-native", native_reset_blend_mode, SIGIL_ARITY_EXACT(0), "Reset blend mode to sokol_gp default"); /* Transform stack */ sigil_module_register_native(vm, "push-transform", native_push_transform, sigil_module_export(vm, "draw-point"); sigil_module_export(vm, "draw-triangle"); sigil_module_export(vm, "fill-triangle"); sigil_module_export(vm, "draw-filled-circle"); sigil_module_export(vm, "%set-blend-mode-native"); sigil_module_export(vm, "%reset-blend-mode-native"); sigil_module_export(vm, "push-transform"); sigil_module_export(vm, "pop-transform"); sigil_module_export(vm, "reset-transform");src/sigil/graphics.sglmodified
draw-filled-rect draw-rect draw-line draw-point draw-triangle fill-triangle draw-filled-circle ;; Blend modes set-blend-mode reset-blend-mode current-blend-mode with-blend-mode with-additive-blend ;; Transform stack push-transform pop-transform reset-transform (begin (begin-frame) body ... (end-frame))))))) (end-frame))))) ;; Tracks the current Sigil-visible blend mode so with-blend-mode can ;; restore the prior mode rather than blindly resetting to 'normal. ;; sokol_gp has no get_blend_mode, so we shadow it here. Always go ;; through set-blend-mode (defined here) so the tracker stays in sync. (define *current-blend-mode* 'none) (define (current-blend-mode) *current-blend-mode*) (define (set-blend-mode mode) (set! *current-blend-mode* mode) (%set-blend-mode-native mode)) (define (reset-blend-mode) (set! *current-blend-mode* 'none) (%reset-blend-mode-native)) ;; Run body with the given blend mode, then restore the previous mode. ;; mode is one of: 'normal (alpha blend), 'additive, 'none. (define-syntax with-blend-mode (syntax-rules () ((_ mode body ...) (let ((prev (current-blend-mode))) (set-blend-mode mode) (let ((result (begin body ...))) (set-blend-mode prev) result))))) ;; Convenience: run body with additive blending, restore the prior mode. ;; The recommended path for particle / bullet-halo rendering — avoids ;; forgotten restores that would leak blend state into other draws. (define-syntax with-additive-blend (syntax-rules () ((_ body ...) (with-blend-mode 'additive body ...))))))