sdfy

A package implementing signed distance functions

Pure Nim score 15/100 · tests present · no docs generated

Summary

Latest Version 0.8.1
License Apache-2.0
CI Status Failing
Downloads 0
Last Indexed 2026-07-21 05:27

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

All Shapes Example

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 Mode

Clip AA Mode (Anti-aliased Edges)

Clip AA Mode

Annular Mode (Ring Shape)

Annular Mode

Annular AA Mode (Anti-aliased Ring)

Annular AA Mode

Feather Mode (Standard Anti-aliasing)

Feather Mode

Feather Inverse Mode (Inverted Anti-aliasing)

Feather Inverse Mode

Feather Gaussian Mode (Gaussian Anti-aliasing)

Feather Gaussian Mode

Drop Shadow Mode

Drop Shadow Mode

Inset Shadow Mode

Inset Shadow Mode

Inset Shadow Annular Mode

Inset Shadow Annular Mode

Pixie Comparison (Traditional Graphics)

Pixie Comparison

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 test
  • b: 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 test
  • b: 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 test
  • r: 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 test
  • b: 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 test
  • ab: 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 test
  • A, 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 test
  • sc: Sin/cos of the arc's aperture (sc.x = sin, sc.y = cos)
  • ra: Inner radius
  • rb: 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 test
  • wi: Width
  • he: Height
  • sk: 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 test
  • c: 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 test
  • n: Sin/cos of the ring's aperture (n.x = sin, n.y = cos)
  • r: Radius
  • th: 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 to
  • center: Center position of the shape
  • wh: 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 shape
  • neg: Color for outside the shape
  • factor: 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.Image type from the Pixie graphics library
  • SdfImage: The included SdfImage type 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:

MSDF Sample Font Output

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:

MSDF Star BitField

Here's an example rendered output:

MSDF Star Icon

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.