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
### Added
examples/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 ...))))))