[!NOTE] This is one of 199 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! ❤️
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:
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.
// 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.
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
yarn add @thi.ng/geom-sdf
ESM import:
import * as sdf from "@thi.ng/geom-sdf";
Browser ESM import:
<script type="module" src="https://esm.run/@thi.ng/geom-sdf"></script>
For Node.js REPL:
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 |
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 later
// the `smooth` 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() (smooth) |
rect() (smooth) |
If this project contributes to an academic publication, please cite it as:
@misc{thing-geom-sdf,
title = "@thi.ng/geom-sdf",
author = "Karsten Schmidt",
note = "https://thi.ng/geom-sdf",
year = 2022
}
© 2022 - 2024 Karsten Schmidt // Apache License 2.0