Commita83dc0ffRecorded18 Apr 2026Repositorysigil-graphics
Merge branch 'feat/circles-and-additive' for v0.9.0
Message
draw-filled-circle and additive blend mode primitives. See CHANGELOG v0.9.0 entry for details.
Changed
.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(-)Diff
A merge. Shown against its first parent, so this is the effect of merging rather than the work of the branch.
.gitignoremodified
@@ -1 +1,4 @@
1
build/+2
.sigil/+3
examples/*/build/+4
examples/*/.sigil/CHANGELOG.mdmodified
@@ -5,6 +5,45 @@ All notable changes to this project will be documented in this file.
5
The format is based on [Keep a Changelog](https://keepachangelog.com/), 6
and this project adheres to [Semantic Versioning](https://semver.org/). 7
+8
## [0.9.0] - 2026-04-18+9
+10
> Note: skips 0.8.0. The `v0.8.0` git tag was used on 2026-03-29 for the+11
> initial standalone-repo extraction, but `package.sgl` was never bumped+12
> past 0.7.0 then. To avoid rewriting a published tag, this minor release+13
> jumps to 0.9.0.+14
+15
### Added+16
+17
- `draw-filled-circle x y radius [segments]` — filled circle primitive+18
rendered as a triangle fan via sokol_gp. `segments` defaults to 16 and+19
is clamped to `[3, 128]`. For tiny shapes (radius 4–6 px), 12–16+20
segments is plenty; larger circles benefit from more. Draw cost scales+21
linearly with `segments`, so use the smallest count that still looks+22
round at the target radius — bullet/particle code on hot paths should+23
pick a fixed low count rather than letting it default. Colour comes+24
from the currently bound `set-color`.+25
- `set-blend-mode mode` and `reset-blend-mode` — Sigil-level blend mode+26
control. `mode` is one of `'normal` (alpha blend), `'additive`, or+27
`'none` (no blending). The Sigil wrapper shadows the mode in module+28
state so `with-blend-mode` can restore the previous value.+29
- `with-blend-mode mode body ...` — scoped form that sets `mode`, runs+30
`body`, then restores the previous mode. `with-additive-blend body+31
...` is the recommended convenience wrapper for particle / bullet+32
halo rendering — it avoids forgotten restores that would leak the+33
blend state into other draws.+34
- `current-blend-mode` — accessor for the Sigil-tracked blend mode.+35
+36
### Notes+37
+38
- Only `'normal`, `'additive`, and `'none` are exposed. sokol_gp also+39
supports premultiplied-alpha, modulate, and multiply modes; those can+40
be added when something needs them. Adding a new mode is a one-line+41
change in `blend_mode_from_value` (`src/c/graphics.c`).+42
- See `examples/circles-blend/` for a side-by-side normal-vs-additive+43
demo. Note: the demo binary build currently fails in the transitive+44
`sigil-lib` rebuild on a missing `minicoro.h` include path — this is+45
a pre-existing build-infra bug, not in this package.+46
47
## [0.5.0] - 2026-02-17 48
49
### Addedexamples/circles-blend/dev-redirects.sgladded
@@ -0,0 +1,12 @@
+1
;; Local sigil-graphics is the in-progress version (v0.8.0-dev) we're demoing.+2
(redirects+3
repos: (list+4
(for-repo+5
url: "codeberg:sigil/sigil"+6
use: (from-path dir: "../../../sigil"))+7
(for-repo+8
url: "codeberg:sigil/sigil-app"+9
use: (from-path dir: "../../../sigil-app"))+10
(for-repo+11
url: "codeberg:sigil/sigil-graphics"+12
use: (from-path dir: "../.."))))examples/circles-blend/package.sgladded
@@ -0,0 +1,33 @@
+1
;;; circles-blend - draw-filled-circle + additive-blend demo for sigil-graphics+2
;;;+3
;;; Renders a grid of overlapping coloured circles, alternating each second+4
;;; between normal alpha blending (left half) and additive blending (right+5
;;; half) so the brightening effect is visible. Quits after ~6 seconds or+6
;;; when escape is pressed.+7
+8
(define sigil-repo "codeberg:sigil/sigil")+9
(define app-repo "codeberg:sigil/sigil-app")+10
(define graphics-repo "codeberg:sigil/sigil-graphics")+11
+12
(package+13
name: "circles-blend"+14
version: "0.0.1"+15
description: "sigil-graphics demo: filled circles + additive blending"+16
license: "BSD-3-Clause"+17
authors: (list "David Wilson <[email protected]>")+18
+19
entry: '(circles-blend main)+20
bundle-name: "circles-blend"+21
+22
configs: (list+23
(config+24
name: 'dev+25
output-dir: "build/dev"+26
debug?: #t+27
optimize: 0+28
bundle?: #t))+29
+30
dependencies: (list+31
(from-git url: sigil-repo package: "sigil-stdlib" version: "^0.9.0")+32
(from-git url: app-repo version: "^0.8.0")+33
(from-git url: graphics-repo version: "^0.9.0")))examples/circles-blend/src/circles-blend/main.sgladded
@@ -0,0 +1,91 @@
+1
;;; circles-blend / main - draw-filled-circle + additive blend demo+2
;;;+3
;;; A 5x4 grid of overlapping coloured circles is rendered twice, side by+4
;;; side: left half uses 'normal alpha blending, right half uses 'additive.+5
;;; Where additive circles overlap they brighten toward white, exactly the+6
;;; particle/halo effect Cinder Cantata wants. Quits after ~6 seconds or+7
;;; when escape is pressed.+8
+9
(define-library (circles-blend main)+10
(import (sigil core)+11
(sigil math)+12
(sigil app)+13
(sigil graphics))+14
(export main)+15
+16
(begin+17
+18
(define WINDOW-W 960)+19
(define WINDOW-H 540)+20
(define HALF-W-F 480.0)+21
(define WINDOW-H-F 540.0)+22
+23
;; Grid layout — circles are large enough to overlap so the blend+24
;; difference is obvious.+25
(define COLS 5)+26
(define ROWS 4)+27
(define RADIUS 60.0)+28
(define SPACING-X 80.0)+29
(define SPACING-Y 80.0)+30
+31
;; Five strongly saturated colours that overlap into white when summed+32
;; under additive blending — the design-doc target for bullet halos.+33
(define palette+34
(list (list 1.0 0.2 0.2 0.6) ; red+35
(list 0.2 1.0 0.2 0.6) ; green+36
(list 0.2 0.4 1.0 0.6) ; blue+37
(list 1.0 1.0 0.2 0.6) ; yellow+38
(list 1.0 0.2 1.0 0.6))) ; magenta+39
+40
(define (palette-color i)+41
(list-ref palette (modulo i (length palette))))+42
+43
(define (draw-grid origin-x mode elapsed)+44
;; Pulse radius gently with elapsed time so motion makes the blend+45
;; effect obvious; static frames are still readable.+46
(let* ((pulse (+ 1.0 (* 0.15 (sin (* 2.0 elapsed)))))+47
(r (* RADIUS pulse)))+48
(with-blend-mode mode+49
(let row-loop ((row 0))+50
(when (< row ROWS)+51
(let col-loop ((col 0))+52
(when (< col COLS)+53
(let* ((idx (+ (* row COLS) col))+54
(color (palette-color idx))+55
(cx (+ origin-x 80.0 (* col SPACING-X)))+56
(cy (+ 120.0 (* row SPACING-Y))))+57
(apply set-color color)+58
(draw-filled-circle cx cy r 24))+59
(col-loop (+ col 1))))+60
(row-loop (+ row 1)))))))+61
+62
;; Background reference rectangles so additive brightening is obvious+63
;; against something other than pitch-black.+64
(define (draw-backdrop)+65
(set-color 0.08 0.08 0.10 1.0)+66
(draw-filled-rect 0.0 0.0 HALF-W-F WINDOW-H-F)+67
(set-color 0.06 0.06 0.08 1.0)+68
(draw-filled-rect HALF-W-F 0.0 HALF-W-F WINDOW-H-F))+69
+70
(define (game-loop)+71
(gfx-setup)+72
(set-viewport WINDOW-W WINDOW-H)+73
(let loop ((elapsed 0.0))+74
(let ((dt (wait-frame)))+75
(with-frame+76
(clear-screen 0.0 0.0 0.0 1.0)+77
(draw-backdrop)+78
;; Left: normal alpha blending — overlaps stack but don't brighten.+79
(draw-grid 0 'normal elapsed)+80
;; Right: additive — overlaps brighten toward white.+81
(draw-grid HALF-W-F 'additive elapsed))+82
(cond+83
((key-pressed? 'escape) (request-quit))+84
((quit-requested?) #t)+85
((> elapsed 6.0) (request-quit))+86
(else (loop (+ elapsed dt))))))+87
(gfx-shutdown))+88
+89
(define (main . _args)+90
(run-game "sigil-graphics circles + blend demo" WINDOW-W WINDOW-H game-loop)+91
0)))package.sglmodified
@@ -5,7 +5,7 @@
5
6
(package 7
name: "sigil-graphics"−8
version: "0.7.0"+8
version: "0.9.0" 9
description: "2D graphics, image loading, and font rendering for Sigil" 10
url: "https://codeberg.org/sigil/sigil-graphics" 11
license: "BSD-3-Clause"src/c/graphics.cmodified
@@ -15,6 +15,17 @@
15
#include <stdio.h> 16
#include <stdlib.h> 17
#include <stdbool.h>+18
#include <string.h>+19
#include <math.h>+20
+21
#ifndef M_PI+22
#define M_PI 3.14159265358979323846+23
#endif+24
+25
/* Maximum segments for a circle triangle fan. Stack-allocated buffer is sized+26
* to this. Higher values produce smoother circles at the cost of more triangles+27
* per draw call. 64 is plenty for typical bullet/UI usage. */+28
#define SIGIL_GFX_CIRCLE_MAX_SEGMENTS 128 29
30
/* External: get pixel data from image (defined in image.c) */ 31
extern unsigned char *sigil_graphics_image_pixels(SigilVM *vm, Value img_val, int *width, int *height);@@ -425,6 +436,110 @@ static Value native_fill_triangle(SigilVM *vm, int argc, Value *args)
436
return SIGIL_NIL; 437
} 438
+439
/*+440
* (draw-filled-circle x y radius [segments]) - Draw a filled circle+441
*+442
* Renders the circle as a triangle fan around (x, y). `segments` controls+443
* the polygon resolution; default is 16. For tiny bullets (radius 4-6 px),+444
* 12-16 is plenty. Larger circles benefit from more segments. Clamped to+445
* [3, SIGIL_GFX_CIRCLE_MAX_SEGMENTS].+446
*/+447
static Value native_draw_filled_circle(SigilVM *vm, int argc, Value *args)+448
{+449
if (argc < 3) {+450
sigil__vm_error(vm, SIGIL_ERR_ARITY,+451
"draw-filled-circle: requires x, y, radius arguments");+452
return SIGIL_UNDEFINED;+453
}+454
+455
float cx = value_to_float(args[0]);+456
float cy = value_to_float(args[1]);+457
float radius = value_to_float(args[2]);+458
+459
int segments = 16;+460
if (argc > 3) {+461
segments = (int)value_to_float(args[3]);+462
}+463
if (segments < 3) segments = 3;+464
if (segments > SIGIL_GFX_CIRCLE_MAX_SEGMENTS) {+465
segments = SIGIL_GFX_CIRCLE_MAX_SEGMENTS;+466
}+467
+468
if (radius <= 0.0f) {+469
return SIGIL_NIL;+470
}+471
+472
sgp_triangle tris[SIGIL_GFX_CIRCLE_MAX_SEGMENTS];+473
float step = (float)(2.0 * M_PI) / (float)segments;+474
float prev_x = cx + radius;+475
float prev_y = cy;+476
for (int i = 1; i <= segments; ++i) {+477
float angle = step * (float)i;+478
float nx = cx + radius * cosf(angle);+479
float ny = cy + radius * sinf(angle);+480
tris[i - 1].a.x = cx; tris[i - 1].a.y = cy;+481
tris[i - 1].b.x = prev_x; tris[i - 1].b.y = prev_y;+482
tris[i - 1].c.x = nx; tris[i - 1].c.y = ny;+483
prev_x = nx;+484
prev_y = ny;+485
}+486
+487
sgp_draw_filled_triangles(tris, (uint32_t)segments);+488
+489
return SIGIL_NIL;+490
}+491
+492
/* ============================================================+493
* BLEND MODES+494
* ============================================================ */+495
+496
/* Map a Sigil symbol value to an sgp_blend_mode. Returns -1 if unknown. */+497
static int blend_mode_from_value(Value v)+498
{+499
if (sigil_is_symbol(v)) {+500
const char *name = sigil_symbol_name(v);+501
if (name) {+502
if (strcmp(name, "normal") == 0) return SGP_BLENDMODE_BLEND;+503
if (strcmp(name, "additive") == 0) return SGP_BLENDMODE_ADD;+504
if (strcmp(name, "none") == 0) return SGP_BLENDMODE_NONE;+505
}+506
}+507
return -1;+508
}+509
+510
/*+511
* (set-blend-mode mode) - Set the current blend mode+512
*+513
* mode is one of: 'normal (alpha blend), 'additive, 'none.+514
* Stays in effect until changed or reset-blend-mode is called.+515
*/+516
static Value native_set_blend_mode(SigilVM *vm, int argc, Value *args)+517
{+518
if (argc < 1) {+519
sigil__vm_error(vm, SIGIL_ERR_ARITY,+520
"set-blend-mode: requires mode symbol");+521
return SIGIL_UNDEFINED;+522
}+523
int mode = blend_mode_from_value(args[0]);+524
if (mode < 0) {+525
sigil__vm_set_error(vm, SIGIL_ERR_TYPE,+526
"set-blend-mode: expected 'normal, 'additive, or 'none");+527
return SIGIL_UNDEFINED;+528
}+529
sgp_set_blend_mode((sgp_blend_mode)mode);+530
return SIGIL_NIL;+531
}+532
+533
/*+534
* (reset-blend-mode) - Reset blend mode to sokol_gp default (no blending)+535
*/+536
static Value native_reset_blend_mode(SigilVM *vm, int argc, Value *args)+537
{+538
(void)vm; (void)argc; (void)args;+539
sgp_reset_blend_mode();+540
return SIGIL_NIL;+541
}+542
543
/* ============================================================ 544
* TRANSFORM STACK 545
* ============================================================ */@@ -844,6 +959,15 @@ void sigil__init_sigil_graphics_module(SigilVM *vm)
959
SIGIL_ARITY_EXACT(6), "Draw triangle outline"); 960
sigil_module_register_native(vm, "fill-triangle", native_fill_triangle, 961
SIGIL_ARITY_EXACT(6), "Draw filled triangle");+962
sigil_module_register_native(vm, "draw-filled-circle", native_draw_filled_circle,+963
SIGIL_ARITY_RANGE(3, 4), "Draw filled circle");+964
+965
/* Blend mode (raw C primitives — wrapped by Sigil set-blend-mode for+966
* tracking; users should prefer the Sigil wrapper or with-blend-mode.) */+967
sigil_module_register_native(vm, "%set-blend-mode-native", native_set_blend_mode,+968
SIGIL_ARITY_EXACT(1), "Set blend mode ('normal, 'additive, 'none)");+969
sigil_module_register_native(vm, "%reset-blend-mode-native", native_reset_blend_mode,+970
SIGIL_ARITY_EXACT(0), "Reset blend mode to sokol_gp default"); 971
972
/* Transform stack */ 973
sigil_module_register_native(vm, "push-transform", native_push_transform,@@ -893,6 +1017,9 @@ void sigil__init_sigil_graphics_module(SigilVM *vm)
1017
sigil_module_export(vm, "draw-point"); 1018
sigil_module_export(vm, "draw-triangle"); 1019
sigil_module_export(vm, "fill-triangle");+1020
sigil_module_export(vm, "draw-filled-circle");+1021
sigil_module_export(vm, "%set-blend-mode-native");+1022
sigil_module_export(vm, "%reset-blend-mode-native"); 1023
sigil_module_export(vm, "push-transform"); 1024
sigil_module_export(vm, "pop-transform"); 1025
sigil_module_export(vm, "reset-transform");src/sigil/graphics.sglmodified
@@ -27,6 +27,11 @@
27
draw-filled-rect draw-rect 28
draw-line draw-point 29
draw-triangle fill-triangle+30
draw-filled-circle+31
+32
;; Blend modes+33
set-blend-mode reset-blend-mode current-blend-mode+34
with-blend-mode with-additive-blend 35
36
;; Transform stack 37
push-transform pop-transform reset-transform@@ -56,4 +61,39 @@
61
(begin 62
(begin-frame) 63
body ...−59
(end-frame)))))))+64
(end-frame)))))+65
+66
;; Tracks the current Sigil-visible blend mode so with-blend-mode can+67
;; restore the prior mode rather than blindly resetting to 'normal.+68
;; sokol_gp has no get_blend_mode, so we shadow it here. Always go+69
;; through set-blend-mode (defined here) so the tracker stays in sync.+70
(define *current-blend-mode* 'none)+71
+72
(define (current-blend-mode) *current-blend-mode*)+73
+74
(define (set-blend-mode mode)+75
(set! *current-blend-mode* mode)+76
(%set-blend-mode-native mode))+77
+78
(define (reset-blend-mode)+79
(set! *current-blend-mode* 'none)+80
(%reset-blend-mode-native))+81
+82
;; Run body with the given blend mode, then restore the previous mode.+83
;; mode is one of: 'normal (alpha blend), 'additive, 'none.+84
(define-syntax with-blend-mode+85
(syntax-rules ()+86
((_ mode body ...)+87
(let ((prev (current-blend-mode)))+88
(set-blend-mode mode)+89
(let ((result (begin body ...)))+90
(set-blend-mode prev)+91
result)))))+92
+93
;; Convenience: run body with additive blending, restore the prior mode.+94
;; The recommended path for particle / bullet-halo rendering — avoids+95
;; forgotten restores that would leak blend state into other draws.+96
(define-syntax with-additive-blend+97
(syntax-rules ()+98
((_ body ...)+99
(with-blend-mode 'additive body ...))))))