sdfy
A package implementing signed distance functions
Summary
| Latest Version | 0.8.1 |
|---|---|
| License | Apache-2.0 |
| CI Status | Failing |
| Downloads | 0 |
| Last Indexed | 2026-07-21 05:27 |
Tags
Authors
- Jaremy Creechley
Installation
nimble install sdfy
choosenim install sdfy
git clone https://github.com/elcritch/sdfy
OS Compatibility
| Platform | Linux | macOS | Windows | FreeBSD | OpenBSD | NetBSD | Android | iOS | WASM | Embedded |
|---|---|---|---|---|---|---|---|---|---|---|
| sdfy | ✓ | ✓ | ✓ | - | - | - | - | - | - | - |
Dependencies
| Package | Version | Optional |
|---|---|---|
| nimsimd | - | No |
| chroma | - | No |
| vmath | - | No |
| pixie >= | 0.5.0 | No |
| pixie >= | 0.5.0 | No |
| https://github.com/Chlumsky/msdfgen | - | No |
| https://github.com/ShoYamanishi/SDFont | - | No |
Source
| Repository | https://github.com/elcritch/sdfy |
|---|---|
| Homepage | https://github.com/elcritch/sdfy |
| Registry Source | nimble_official |
README
SDFY - Fast 2D Vector Shapes with Shadows for GUIs and Images using SDFs

A high-performance library implementing 2D signed distance functions (SDFs) with multiple rendering modes and SIMD optimizations. Implemented with Nim but compatible with C/C++. Drop a note if you'd like a C API.
These SDFs are targeted and tune for quickly making drop shadows for GUIs. These can be used with graphical renderers to speed up expensive drop and inset shadows.
Features
- Fast SDF Implementation: Optimized implementations of common 2D shapes
- Multiple Rendering Modes: Support for various anti-aliasing and effect techniques
- SIMD Acceleration: Leverages SIMD (SSE2/NEON) instructions for maximum CPU performance
- Pixie Integration: Seamless integration with the Pixie graphics library
- Flexible API: Easy-to-use interface for rendering SDFs to images
How It Works
Creating good looking drop and inset shadows is traditionally done using Gaussian Blur. This requires a 2D convolution which is slow even if done in X and then Y directions.
It's also a bit wasteful since we know the vector shape for most basic GUI elements. Why can't we use that to reduce the computational overhead?
That's where SDFs come in. They allow efficiently getting the nearest distance to a shape for any point and whether it's inside or outside the shape. We can then take this value (a linear gradient) and apply a 1D gaussian function to it.
From this we get a good looking, if not perfect, drop shadow! There's a slight difference it seems between the SDF + Gaussian 1D function vs a 2D true Gaussian blur on corners.
Supported Shapes
- Rounded Rectangle: Fully configurable rounded rectangles with independent corner radii
- Chamfer Box: Rectangles with chamfered (cut) corners
- Circle: Perfect circles with configurable radius
- Box: Simple rectangles/boxes with sharp corners
- Ellipse: Elliptical shapes with configurable semi-axes
- Quadratic Bézier Curve: Smooth curves defined by three control points
- Arc: Circular arc segments with configurable aperture and thickness
- Parallelogram: Four-sided shapes with parallel opposite sides and configurable skew
- Pie: Pie slice/sector shapes with configurable aperture and radius
- Ring: Ring/annular segments with configurable aperture, radius, and thickness
Rendering Modes
- Clip: Sharp edges without anti-aliasing
- Clip AA: Sharp edges with anti-aliasing
- Annular: Creates ring/annular shapes
- Annular AA: Anti-aliased ring shapes
- Feather: Standard anti-aliased edges with customizable factor
- Feather Inverse: Inverted feather anti-aliasing
- Feather Gaussian: Gaussian-based anti-aliasing for smooth edges
- Drop Shadow: Gaussian-based drop shadow effects
- Inset Shadow: Inner shadow effects
- Inset Shadow Annular: Annular inner shadow effects
Performance
| Mode | With SIMD | Without SIMD | Speedup |
|---|---|---|---|
| Pixie Shadow | 456 ms | 476 ms | 1.0x |
| Clip | 5 ms | 20 ms | 4.0x |
| Clip AA | 6 ms | 30 ms | 5.0x |
| Annular | 5 ms | 22 ms | 4.4x |
| Annular AA | 6 ms | 33 ms | 5.5x |
| Feather | 6 ms | 23 ms | 3.8x |
| Feather Inverse | 6 ms | 26 ms | 4.3x |
| Feather Gaussian | 7 ms | 24 ms | 3.4x |
| Drop Shadow | 7 ms | 24 ms | 3.4x |
| Inset Shadow | 8 ms | 24 ms | 3.0x |
| Inset Shadow Annular | 7 ms | 24 ms | 3.4x |
Performance measured on rounded rectangles (300x300 image). SIMD provides 3-5x performance improvement. SDF functions are 15-65x faster than traditional Pixie rendering with shadows.**
Shape-specific Performance: - Simple shapes (Circle, Box): Fastest rendering, full SIMD optimization - Medium complexity (Rounded Box, Chamfer Box, Arc, Pie, Ring): Good SIMD optimization - Complex shapes (Ellipse, Bézier, Parallelogram): Partial SIMD optimization with scalar fallbacks for complex math
Examples
Here are examples of the different rendering modes applied to rounded rectangles:
Clip Mode (Sharp Edges)

Clip AA Mode (Anti-aliased Edges)

Annular Mode (Ring Shape)

Annular AA Mode (Anti-aliased Ring)

Feather Mode (Standard Anti-aliasing)

Feather Inverse Mode (Inverted Anti-aliasing)

Feather Gaussian Mode (Gaussian Anti-aliasing)

Drop Shadow Mode

Inset Shadow Mode

Inset Shadow Annular Mode

Pixie Comparison (Traditional Graphics)

Installation
Add to your .nimble file:
requires "sdfy"
Or install directly:
nimble install sdfy
Quick Start
import pixie
import sdfy
let image = newImage(300, 300)
let center = vec2(150.0, 150.0)
let size = vec2(200.0, 200.0)
let corners = vec4(0.0, 20.0, 40.0, 80.0) # Different radius per corner
let fillColor = rgba(255, 0, 0, 255) # Red fill
let bgColor = rgba(0, 0, 255, 255) # Blue background
# Render a rounded rectangle with anti-aliasing
drawSdfShape(
image,
center = center,
wh = size,
params = RoundedBoxParams(r: corners),
pos = fillColor,
neg = bgColor,
mode = sdfModeFeatherInv
)
image.writeFile("output.png")
API Reference
Core Functions
sdRoundedBox(p: Vec2, b: Vec2, r: Vec4): float32
Calculate the signed distance from a point to a rounded rectangle.
p: Point to testb: Box half-extents (width/2, height/2)r: Corner radii as Vec4 (x=top-right, y=bottom-right, z=bottom-left, w=top-left)- Returns: Signed distance (negative inside, positive outside)
sdChamferBox(p: Vec2, b: Vec2, chamfer: float32): float32
Calculate the signed distance from a point to a chamfered rectangle.
p: Point to testb: Box half-extents (width/2, height/2)chamfer: Chamfer amount- Returns: Signed distance (negative inside, positive outside)
sdCircle(p: Vec2, r: float32): float32
Calculate the signed distance from a point to a circle.
p: Point to testr: Circle radius- Returns: Signed distance (negative inside, positive outside)
sdBox(p: Vec2, b: Vec2): float32
Calculate the signed distance from a point to a box/rectangle.
p: Point to testb: Box half-extents (width/2, height/2)- Returns: Signed distance (negative inside, positive outside)
sdEllipse(p: Vec2, ab: Vec2): float32
Calculate the signed distance from a point to an ellipse.
p: Point to testab: Ellipse semi-axes (width/2, height/2)- Returns: Signed distance (negative inside, positive outside)
sdBezier(p: Vec2, A: Vec2, B: Vec2, C: Vec2): float32
Calculate the signed distance from a point to a quadratic Bézier curve.
p: Point to testA,B,C: Control points of the Bézier curve- Returns: Distance to the curve (always positive for curves)
sdArc(p: Vec2, sc: Vec2, ra: float32, rb: float32): float32
Calculate the signed distance from a point to an arc.
p: Point to testsc: Sin/cos of the arc's aperture (sc.x = sin, sc.y = cos)ra: Inner radiusrb: Thickness (outer radius difference)- Returns: Signed distance (negative inside, positive outside)
sdParallelogram(p: Vec2, wi: float32, he: float32, sk: float32): float32
Calculate the signed distance from a point to a parallelogram.
p: Point to testwi: Widthhe: Heightsk: Skew- Returns: Signed distance (negative inside, positive outside)
sdPie(p: Vec2, c: Vec2, r: float32): float32
Calculate the signed distance from a point to a pie slice.
p: Point to testc: Sin/cos of the pie's aperture (c.x = sin, c.y = cos)r: Radius- Returns: Signed distance (negative inside, positive outside)
sdRing(p: Vec2, n: Vec2, r: float32, th: float32): float32
Calculate the signed distance from a point to a ring.
p: Point to testn: Sin/cos of the ring's aperture (n.x = sin, n.y = cos)r: Radiusth: Thickness- Returns: Signed distance (negative inside, positive outside)
drawSdfShape(image, center, wh, params, pos, neg, factor, spread, mode)
Generic function to render shapes to an image using SDF.
image: Target image to render tocenter: Center position of the shapewh: Width and height of the shape (ignored for some shapes like circles, arcs, etc.)params: Shape parameters (see Shape Parameters section)pos: Color for inside the shapeneg: Color for outside the shapefactor: Anti-aliasing factor (default: 4.0)spread: Spread amount for shadow effects (default: 0.0)mode: Rendering mode (see SDFMode enum)
Shape Parameters
type
RoundedBoxParams* = object
r*: Vec4 # corner radii (top-right, bottom-right, bottom-left, top-left)
ChamferBoxParams* = object
chamfer*: float32 # chamfer amount
CircleParams* = object
r*: float32 # radius
BoxParams* = object
b*: Vec2 # box half-extents (width/2, height/2)
EllipseParams* = object
ab*: Vec2 # ellipse semi-axes (width/2, height/2)
BezierParams* = object
A*: Vec2 # first control point
B*: Vec2 # second control point
C*: Vec2 # third control point
ArcParams* = object
sc*: Vec2 # sin/cos of the arc's aperture
ra*: float32 # inner radius
rb*: float32 # thickness (outer radius difference)
ParallelogramParams* = object
wi*: float32 # width
he*: float32 # height
sk*: float32 # skew
PieParams* = object
c*: Vec2 # sin/cos of the pie's aperture
r*: float32 # radius
RingParams* = object
n*: Vec2 # sin/cos of the ring's aperture (n.x = sin, n.y = cos)
r*: float32 # radius
th*: float32 # thickness
Rendering Modes
type SDFMode* = enum
sdfModeFeather # Standard anti-aliasing
sdfModeFeatherInv # Inverted anti-aliasing
sdfModeClip # Sharp edges without anti-aliasing
sdfModeClipAA # Sharp edges with anti-aliasing
sdfModeFeatherGaussian # Gaussian anti-aliasing
sdfModeDropShadow # Drop shadow effect
sdfModeInsetShadow # Inset shadow effect
sdfModeInsetShadowAnnular # Annular inset shadow effect
sdfModeAnnular # Ring/annular shape
sdfModeAnnularAA # Anti-aliased ring/annular shape
Image Compatibility
SDFY is designed to work with multiple image types through a flexible generic interface. You can use:
- Pixie Image: The standard
pixie.Imagetype from the Pixie graphics library - SdfImage: The included
SdfImagetype for lightweight image operations - Custom Image Types: Any type that implements the required interface
Required Image Interface
For an image type to work with SDFY's drawSdfShape function, it must provide:
# Required fields/properties:
image.width: int # Image width in pixels
image.height: int # Image height in pixels
image.data: seq[ColorRGBX] # Pixel data as RGBX color sequence
# Required function/template:
image.dataIndex(x, y: int): int # Calculate array index for pixel at (x, y)
Implementation Example
type
CustomImage* = object
width*, height*: int
data*: seq[ColorRGBX]
# Implement the dataIndex template
template dataIndex*(image: CustomImage, x, y: int): int =
image.width * y + x
# Now you can use it with SDFY
let myImage = CustomImage(width: 300, height: 300)
myImage.data = newSeq[ColorRGBX](300 * 300)
drawSdfShape(
myImage, # Works with any compatible image type
center = vec2(150, 150),
wh = vec2(200, 200),
params = CircleParams(r: 100.0),
pos = rgba(255, 100, 100, 255),
neg = rgba(50, 50, 50, 255),
mode = sdfModeFeatherInv
)
SdfImage vs Pixie type for Image
SDFY includes an image type equivalent to Pixie's Image type for compatability with Pixie.
Examples
Basic Rounded Rectangle
import pixie, sdfy
let image = newImage(300, 300)
drawSdfShape(
image,
center = vec2(150, 150),
wh = vec2(200, 200),
params = RoundedBoxParams(r: vec4(20, 20, 20, 20)), # 20px radius on all corners
pos = rgba(255, 100, 100, 255),
neg = rgba(50, 50, 50, 255),
mode = sdfModeFeatherInv
)
Asymmetric Corners
# Different radius for each corner
let corners = vec4(
0.0, # top-right: sharp corner
20.0, # bottom-right: small radius
40.0, # bottom-left: medium radius
80.0 # top-left: large radius
)
drawSdfShape(
image,
center = center,
wh = size,
params = RoundedBoxParams(r: corners),
pos = fill,
neg = bg,
mode = sdfModeFeatherInv
)
Drop Shadow Effect
drawSdfShape(
image,
center = vec2(150, 150),
wh = vec2(200, 200),
params = RoundedBoxParams(r: vec4(20, 20, 20, 20)),
pos = rgba(255, 255, 255, 255),
neg = rgba(0, 0, 0, 0),
factor = 10.0,
spread = 20.0,
mode = sdfModeDropShadow
)
MSDF (Multi-channel Signed Distance Fields)
SDFY includes an MSDF generator for fonts and SVG paths plus helpers to render MSDF bitmaps based on MSDFGen. This is based on Valve's original SDF bitmap field fonts for OpenGL.
Use generateMsdfGlyph / generateMsdfPath from sdfy/msdfgen, then render with
renderMsdf, blitMsdfGlyph, or drawSdfShape via MsdfBitmapParams.
Sample Font Output
This works, but as you can see isn't super high quality for small fonts. You would probably need to user super-sampling to get this to work well:

Star Icon (SVG Path)
For generic SVG paths, MSDF works rather well and scales very nicely.
Note the shadow artifacts - the normal SDFModes for drop shadow and gaussian feathers don't work well with MSDF fields. It might be a matter of tweaking.
This star is generated at 32x32 and scales up to 512x512 nicely:

Here's an example rendered output:
![]()
Inspiration
This library is based on the excellent work by Íñigo Quílez on 2D distance functions.
License
Licensed under the Apache License 2.0. See LICENSE file for details.
Contributing
Contributions are welcome! Please feel free to submit pull requests or open issues for bugs and feature requests.