2 Shapes and Curves
dxtr edited this page 2026-10-14 03:29:19 +02:00

Shapes and Curves

Curves

A shape is given as a vector of reals, six for each quadratic Bézier curve:

X0 Y0  CX CY  X1 Y1
start  control  end

in ems, with y up. Any vector of reals works: a simple-vector, a vector of single-floats such as cl-textrender:outline-quadratics returns, a vector of rationals. The type cl-slug:curves names it.

The curves form closed contours: each curve starts where the one before it ended, and each contour ends where it started. Contours follow one another in the vector. cl-slug does not check that contours close, but the fill is only right for closed ones.

;; A unit square, counterclockwise: four straight curves.
#(0 0  1/2 0  1 0
  1 0  1 1/2  1 1
  1 1  1/2 1  0 1
  0 1  0 1/2  0 0)

Straight lines

A straight line is a curve whose control point lies on it. Give the control point anywhere on the segment: at its middle, as cl-textrender:outline-quadratics packs lines, or at its end.

cl-slug moves the control point of every straight curve to its end point, the form the Slug shaders solve most exactly. A control point counts as on the line if it lies between the ends, within a hundred-thousandth of the line's length of it.

Curves left out

A curve whose three points are the same after rounding, which has no length, is left out. A shape whose curves are all left out, or which has none, is empty: shape-empty-p is true, it takes no texels, and a renderer draws nothing for it. A space is one.

Rounding

The curve texture holds 16-bit floats, so every coordinate is rounded to the nearest one when the shape is added, ties to even, as GPUs convert. Near 1 they are about a two-thousandth of an em apart; near 0.1, a sixteen-thousandth. The bounds a shape reports are the rounded ones:

(cl-slug:shape-xmin (cl-slug:store-add store #(0.1 0.1 0.5 0.1 0.9 0.1 ...)))
;; => 0.099975586

The bands are built from the rounded coordinates, so the shader finds every curve in the bands the builder put it in. A coordinate beyond 65504 signals a slug-error: give shapes in ems, not pixels or font units. half-float-bits and half-float-value convert by hand.

Fill rules

Which parts of a shape are inside is decided by how many times its contours wind around them, counting counterclockwise turns as +1 and clockwise as -1:

Fill rule Inside where the winding number is Used by
:nonzero, the default anything but 0 Fonts, most vector icons
:even-odd odd Some SVG paths

So with :nonzero, a hole runs the other way from the contour around it:

;; A ring: an outer circle counterclockwise, an inner one clockwise.
(cl-slug:store-add store (concatenate 'vector outer-ccw inner-cw))

With :even-odd, a hole may run either way, and where two contours overlap is a hole too:

(cl-slug:store-add store curves :fill-rule :even-odd)

The shader takes coverage's absolute value, so a whole shape may run either way: TrueType glyphs run clockwise and CFF glyphs counterclockwise, and both fill.

Glyphs from a font

cl-textrender gives a glyph's outline. Make the font at size 1, so the outline is in ems, and ask for quadratics, which turns a CFF font's cubic curves into quadratics:

(cl-textrender:with-font (font "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" :size 1)
  (let ((glyph (cl-textrender:font-glyph-id font #\A)))
    (cl-slug:store-find-or-add
     *store* (list font glyph)
     (lambda ()
       (cl-textrender:outline-quadratics
        (cl-textrender:glyph-outline font glyph :quadratic t))))))
;; => #<CL-SLUG:SHAPE 11 curves, 7 by 3 bands {...}>

With a font of another size, divide the coordinates by the size:

(let ((curves (cl-textrender:outline-quadratics
               (cl-textrender:glyph-outline font glyph :quadratic t)))
      (size (float (cl-textrender:font-size font) 1.0)))
  (map-into curves (lambda (value) (/ value size)) curves))

Key glyphs by font and glyph ID, as above. A renderer that looks glyphs up every frame should keep its own table from font and ID to shape: store-find-or-add is cheap, but a fresh key list allocates. Pyssel keeps a hash table per font, by glyph ID.

Vector icons

An icon is a shape like a glyph: quadratic curves in ems. Convert an SVG path's lines and quadratic curves directly; approximate its cubic curves and arcs with quadratics, as cl-textrender does for CFF fonts, within a small tolerance such as a thousandth of an em. Use :even-odd where the SVG says fill-rule="evenodd". Scale the icon's view box to about an em.

How the bands are chosen

For each direction, cl-slug tries every number of bands from 1 to 32, and no more than the shape has curves, and keeps the one whose fullest band holds the fewest curves; the fewest bands among equals. A curve is in a band if its control points reach into it, widened by the store's epsilon, 1/1024 of an em by default. A horizontal line is in no horizontal band and a vertical line in no vertical band, since a ray along them crosses nothing.

Bands of the same curves share one list in the band texture. A square has one band each way; the glyph "A" of DejaVu Sans, 7 vertical bands and 3 horizontal.

Limits

  • A band holds at most 4096 curves, the width of a row: a shape with more curves crossing one band signals a slug-error. Split it into several shapes.
  • A shape's band data spans at most 65536 texels of the band texture.
  • A run of connected curves longer than a row is split, which costs one texel.

See Errors and Limits.