2D Signed Distance Field creation from @thi.ng/geom shapes, conversions, sampling, combinators
npm install @thi.ng/geom-sdf
!npm downloads

> [!NOTE]
> This is one of 214 standalone projects, maintained as part
> of the @thi.ng/umbrella monorepo
> and anti-framework.
>
> 🚀 Please help me to work full-time on these projects by sponsoring me on
> GitHub. Thank you! ❤️
- About
- SDF creation
- Supported thi.ng/geom shape types
- SDF combinators
- SDF discretization, sampling & domain modifiers
- Status
- Related packages
- Installation
- Dependencies
- Usage examples
- API
- Authors
- License
2D Signed Distance Field creation from @thi.ng/geom shapes, conversions, sampling, combinators.
Includes several distance functions and SDF operators ported from GLSL
implementations by:
- Inigo Quilez (Article)
- Mercury demogroup (HG_SDF)
SDFs can be directly defined/composed via provided shape primitive functions and
combinators OR via automatic conversion from @thi.ng/geom geometry
types/hierarchies. In the latter case various
attributes
can be used to control the conversion process. Regardless of approach, the
result will be a single distance function which accepts a world position and
returns the signed distance to the encoded scene.
``ts
// via direct SDF composition
import { circle2, union } from "@thi.ng/geom-sdf";
const f = union([circle2([-50, 0], 100), circle2([50, 0], 100)]);
// via conversion
import { circle, group } from "@thi.ng/geom";
import { asSDF } from "@thi.ng/geom-sdf";
const f = asSDF(group({}, [circle([-50, 0], 100), circle([50, 0], 100)]));
`
- circle
- complexPoly (polygon w/ holes)
- cubic
- ellipse
- group (of supported shapes)
- line
- path (w/ holes and/or sub-paths, multiple curves)
- points
- polygon
- polyline
- quad
- quadratic
- rect
- triangle
The following table illustrates various options how SDFs can be combined. When
using the asSDF()
geometry converter, these operators can be specified and configured (most are
parametric) via a shape group()'s
attributes,
e.g.
`ts
import { group, rectFromCentroid } from "@thi.ng/geom";
group({ __sdf: { combine: "diff", chamfer: 50 }}, [
rectFromCentroid([-50,-50], 200),
rectFromCentroid([50,50], 200),
]);
`
| Operator | Union | Difference | Intersection |
|----------|----------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
| default |  |  |  |
| chamfer |  |  |  |
| round |  |  |  |
| smooth |  |  |  |
| steps |  |  |  |
The package provides the
sample2d() and
asPolygons()
functions to discretize an SDF and cache results in a buffer (image) and then
extract contour polygons from it, i.e. convert the 2D back into geometry (see
example further below). The SDF will be sampled in a user defined bounding
rectangle (with customizable resolution) and the sampling positions can be
modulated via several provided domain modifiers to create various axial/spatial
repetions, symmetries etc. Modifiers are nestable/composable via standard
functional composition (e.g. using
compL()) and also
support custom modfifiers. The table below illustrates a few examples effects:
| Modifier | | | |
|-------------------|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
| repeat2() |  |  |  |repeatGrid2()
| |  |  |  |repeatMirror2()
| |  |  |  |repeatPolar2()
| |  |  |  |
ALPHA - bleeding edge / work-in-progress
Search or submit any issues for this package
- @thi.ng/distance-transform - Binary image to Distance Field transformation
- @thi.ng/geom-isoline - Fast 2D contour line extraction / generation
- @thi.ng/pixel - Typedarray integer & float pixel buffers w/ customizable formats, blitting, drawing, convolution
- @thi.ng/shader-ast-stdlib - Function collection for modular GPGPU / shader programming with @thi.ng/shader-ast
`bash`
yarn add @thi.ng/geom-sdf
ESM import:
`ts`
import * as sdf from "@thi.ng/geom-sdf";
Browser ESM import:
`html`
For Node.js REPL:
`js`
const sdf = await import("@thi.ng/geom-sdf");
Package sizes (brotli'd, pre-treeshake): ESM: 3.76 KB
- @thi.ng/api
- @thi.ng/checks
- @thi.ng/defmulti
- @thi.ng/errors
- @thi.ng/geom
- @thi.ng/geom-isoline
- @thi.ng/geom-poly-utils
- @thi.ng/geom-resample
- @thi.ng/math
- @thi.ng/transducers
- @thi.ng/vectors
Note: @thi.ng/api is in _most_ cases a type-only import (not used at runtime)
Two projects in this repo's
/examples
directory are using this package:
| Screenshot | Description | Live demo | Source |
|:---------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------|:----------------------------------------------------|:---------------------------------------------------------------------------------|
|
| (Re)Constructing the thi.ng logo using a 2D signed-distance field | Demo | Source |
|
| SVG path to SDF, applying deformation and converting back to SVG | Demo | Source |
`js
import { asSvg, bounds, circle, group, svgDoc } from "@thi.ng/geom";
import { asPolygons, asSDF, sample2d } from "@thi.ng/geom-sdf";
import { range, repeatedly } from "@thi.ng/transducers";
import { randMinMax2 } from "@thi.ng/vectors";
import { writeFileSync } "node:fs";
const RES = [256, 256];
// create a group of 20 random circle shapes
// the special __sdf attrib object is used to control the conversion latersmooth
// the option will combine the circles using the smoothUnion() operator
// see: https://docs.thi.ng/umbrella/geom-sdf/interfaces/SDFAttribs.html
const scene = group({ stroke: "red", __sdf: { smooth: 20 } }, [
...repeatedly(
() =>
circle(
randMinMax2([], [-100, -100], [100, 100]),
5 + Math.random() * 15
),
20
),
]);
// compute bounding box + some extra margin
// the extra margin is to ensure the SDF can be fully sampled
// at some distance from the original boundary (see further below)
const sceneBounds = bounds(scene, 40);
// convert to an SDF distance function
// more information about supported shape types:
// https://docs.thi.ng/umbrella/geom-sdf/functions/asSDF.html
const sdf = asSDF(scene);
// sample SDF in given bounding rect and resolution
const image = sample2d(sdf, sceneBounds, RES);
// extract contour polygons from given image
// in this case the contours extracted are at distances in the 0..32) interval
// the function also simplifies the resulting polygons using the Douglas-Peucker algorithm
// with the given threshold (0.25) - the default setting only removes co-linear vertices...
// see: https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
const contours = asPolygons(image, sceneBounds, RES, range(0, 32, 4), 0.25);
// convert to SVG and output as file
writeFileSync(
"export/metaballs.svg",
asSvg(
svgDoc(
{ fill: "none" },
// contour polygons
group({ stroke: "#000" }, contours),
// original geometry
scene
)
)
);
`
Results:
| circle() | rect() |circle()
|------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| ![metaballs based on circles | !metaballs based on rectangles |
| (smooth) | rect() (smooth) |
| !metaballs w/ smooth union | !metaballs w/ smooth union |
If this project contributes to an academic publication, please cite it as:
`bibtex``
@misc{thing-geom-sdf,
title = "@thi.ng/geom-sdf",
author = "Karsten Schmidt",
note = "https://thi.ng/geom-sdf",
year = 2022
}
© 2022 - 2026 Karsten Schmidt // Apache License 2.0