stilus

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: MIT Imports: 9 Imported by: 0

README

stilus

CI Coverage Go Reference

A sparse CPU rasterizer for 2D vector graphics in pure Go: no cgo, no dependencies, all GOOS/GOARCH including js/wasm, MIT licensed.

The cost of a path is edge length in pixels plus covered spans – never the area of its bounding box or the width of the canvas. After warm-up, drawing performs zero allocations.

stilus is the rasterizer core (milestone M1) of the PDF renderer described in the spec “PDF-Renderer für Go (Testballon)”. It knows nothing about PDF, fonts or color spaces, so it can equally serve lux, figure or act as the CPU filler behind gogpu/gg.

API in three layers

// 1. Spans – plug into any compositor
type Blitter interface {
    BlitRun(y, x0, x1 int, alpha uint8)   // interior: constant runs, no pixel loop
    BlitCoverage(y, x int, cov []uint8)   // antialiased edge pixels
}
r := stilus.NewRasterizer(clip)
r.Fill(path, matrix, stilus.NonZero, blitter)    // or AddLine/AddPath + Rasterize

// 2. Geometry – strokes in user space, exact under anisotropic CTMs
var s stilus.Stroker
s.Stroke(r, path, matrix, &stilus.StrokeStyle{Width: 1, Cap: stilus.RoundCap, Dash: []float64{3, 2}})
r.Rasterize(stilus.NonZero, blitter)

// 3. Canvas – the display-list device: Fill/Stroke/ClipPath/ClipRect/PopClip
c := stilus.NewCanvas(img)                        // *image.RGBA owned by the caller
c.Reset(img, band)                                // tile/band/viewport rendering
c.ClipRect(stilus.Rect{X0: 0, Y0: 0, X1: 842, Y1: 595}, ctm)
c.Stroke(path, ctm, style, &stilus.Paint{Color: color.RGBA{0, 0, 0, 255}})
c.Fill(path, ctm, stilus.EvenOdd, &stilus.Paint{Shader: myShader}) // per-span callback
c.PopClip()

Integration points for other renderers: implement Blitter (own pixel format, GPU upload, coverage masks), Shader (gradients, images – called per span, never per pixel) or consume Stroker output through a LineSink.

How it is fast

  • Band accumulation: edges are accumulated as area/cover into a 32-row band of cells (24.8 fixed point), not a page-sized buffer. Paths are clipped analytically first; curves are flattened in device space (0.1 px) after culling against the clip.
  • Sparse sweep: narrow rows (the common case: strokes, glyphs) are integrated over their touched cell range; wide rows use a dirty bitset and emit the constant stretches between edges as runs. The sweep zeroes what it reads – there is no clearing pass.
  • Hairlines (thinner than one device pixel) are stroked one pixel wide in device space by the same stroker, with exact area coverage.
  • Analytic strokes: between the corners of a polyline (joins, caps, inner notches), each segment is bounded by two parallel lines, so each pixel's area is the difference of two closed-form half-plane areas – no cells, no sweep. Only the bands of rows around corners go through the accumulator, with the stroke's real outline edges, so corners, caps, round joins and segments meeting end to end are exactly as on the outline path. Covers polylines, flattened curves, closed shapes and straight dashes; within 2/255 of the outline path (TestPolylineFastPath).
  • Rectangle clips (91 % of real PDF clips) cost nothing per pixel; other clips are rasterized once into a mask that is never cleared as a whole and keeps fully opaque runs as runs.
  • Compositing on premultiplied RGBA8 with SWAR arithmetic (four channels per 64-bit multiply, no divisions), memmove-speed opaque fills.
  • Destination pre-touch: scattered short strokes are bound by memory latency – every row is a cold cache line, and the arithmetic between two rows fills the out-of-order window, so the misses are taken one after another. Before compositing, Canvas.Stroke loads the destination under the first 64 rows of each segment up to 256 rows tall: independent loads whose misses overlap. Only the rows that fit in the reorder buffer together overlap, so the loop is kept to a few instructions per row (running sums for the row's x and offset, one load at each end of the stretch). Longer segments are left to the hardware prefetcher. Output is unchanged; 20 000 short strokes −13…18 %, contours −15 %, mixed drawing −7 %, hatching unchanged (Ryzen 7 5800H, min of 60). On a cloud Xeon with 4 KB pages the touch itself is 14 % of the short-stroke scene: every row of the destination is another page, and the page walks are what the loads wait for.

Numbers

A3 landscape at 150 dpi (2481×1754 px), rectangle page clip, cloud Xeon @ 2.1 GHz, go run ./cmd/stilus-scenes (minimum of 15 runs):

scene 1 core 4 cores (bands) spec target (1 core)
2 000 long hatch lines, hairline 53 ms 15 ms ≤ 60 ms ✓
2 000 long hatch lines, 0.35 mm 67 ms 19 ms –
20 000 short strokes, 0.35 mm 41 ms 12 ms ≤ 40 ms ✗
20 000 short strokes, hairline 34 ms 11 ms –
30 000 glyph outlines, uncached 55 ms 18 ms –
mixed drawing (fills with alpha, dashes, curves) 49 ms 15 ms –
3 000 polylines + 600 circles, 0.35 mm 84 ms 24 ms –
2 000 hatch lines through an elliptic mask clip 54 ms 19 ms –

Hairlines are drawn one device pixel wide with exact area coverage, like PDFium (an earlier approximation was faster but 14/255 off on dense hatching). Against PDFium on the synthetic drawings of the harness: mean deviation 0.3–1.5/255, SSIM ≥ 0.998 (see harness/).

Allocations per rendered scene after warm-up: 0.

Same machine, same scenes, other pure-Go rasterizers (bench/, separate module):

scene stilus x/image/vector¹ gogpu/gg v0.52.5 (CPU)²
2 000 hatch lines 70 ms 33 400 ms 14 800 ms
20 000 short strokes 55 ms 111 ms 2 090 ms

¹ stilus stroker outlines, filled with one vector.Rasterizer sized to each outline's bounding box. ² No clip; gg's rectangle clip is slower still.

SIMD (experimental, Go 1.26+ simd/archsimd)

Built with GOEXPERIMENT=simd on amd64, the compositing kernels (covOpaque, covOver, runOver) use AVX2 (4 px per iteration) or AVX-512 with VBMI (8 px, VPMOVWB pack, VPERMB coverage broadcast), chosen at runtime. Results are bit-identical to the scalar path (TestSpanKernels). Any other build uses the scalar kernels unchanged.

GOEXPERIMENT=simd go test ./...                # Go 1.26 or 1.27
GOEXPERIMENT=simd go test -run XXX -bench SpanKernels .
STILUS_SIMD=0 | 256   # at run time: scalar only | AVX2 only

Kernel throughput, ns per span (same Xeon):

span kernel scalar AVX2 AVX-512
4 px opaque coverage 13 16 17
16 px opaque coverage 36 16 14
64 px coverage over (alpha) 282 71 35
256 px run with alpha 517 196 74
1024 px opaque coverage 2423 846 483

Spans shorter than 16 px stay scalar: one vector block plus VZEROUPPER costs more than a few scalar pixels. On the scenes this means: fills with transparency (mixed drawing) −15…20 %, stroke-dominated scenes unchanged, because their spans are 2–6 px wide. Same picture on a Ryzen 7 5800H (AVX2 only) with Go 1.27.1: mixed drawing −21 %, contours −8 %, the rest unchanged.

The experiment's API changed in Go 1.27 (loads and stores take slices: LoadUint8x16(s), Store(s), TruncToUint8). The kernels call small wrappers in simd_api_go126.go / simd_api_go127.go, selected by the toolchain's own go1.27 release tag, so no extra flag is needed; the wrappers inline and the generated loops are the same on both. Without GOEXPERIMENT=simd any Go ≥ 1.24 builds the scalar path.

Pitfalls found on the way (Go 1.26, the first still true in 1.27), all avoided in simd_amd64.go:

  • The compiler emits no VZEROUPPER; without archsimd.ClearAVXUpperBits() at the end of each kernel, the rasterizer's legacy-SSE float code ran 3–5× slower afterwards.
  • ShiftAllRight with a count compiles to a legacy-SSE MOVQ inside the AVX loop; division by 255 therefore uses MulHigh ((x+128)·257 >> 16).
  • Vectors inside a struct are copied with legacy-SSE MOVUPS; constants are kept in plain local variables.

Accuracy and robustness

  • Coverage is exact-area accumulation (like FreeType, AGG/PDFium, Skia); tests compare against a supersampling reference integrating the winding number (mean < 0.5/255, max ≤ 16/255 on random polygons).
  • Stroker: AGG-style outlines (inner corners at the offset intersection, no overlap on ordinary polylines), butt/round/square caps, miter/round/bevel joins with miter limit, dashes with phase and zero-length dots. Strokes below one device pixel are drawn one pixel wide, like PDFium.
  • NaN/Inf rejected, huge coordinates clipped analytically, budgets for edges per path, curve/arc subdivision, dash counts and clip depth. Canvas never panics; errors are reported through Err(). Dash patterns beyond the subdivision budget skip or truncate the affected subpath and report ErrDashBudget; Stroker.Truncated() exposes the same state when using the stroker directly. Patterns too dense to resolve (a device period of at most 1/4 px, or more than 32 entries per pixel) are drawn as a solid stroke at the pattern's mean coverage, caps included, so their cost is that of a solid stroke; Stroker.Coverage() reports the factor. Walking a pattern dash by dash is bounded by 32 transitions per device pixel of the subpath.
  • Fuzz targets: FuzzFill, FuzzStroke.

Development

go test ./...                                  # tests, reference comparisons, alloc gates
go test -run XXX -bench BenchmarkScenes .      # scene benchmarks at 72/150/300 dpi
go run ./cmd/stilus-scenes -dpi 150 -out /tmp/scenes   # timings + PNGs
go run ./cmd/stilus-scenes -threads 4          # band-parallel rendering
(cd bench && go test -bench .)                 # comparison with x/image/vector and gg
go test -fuzz FuzzStroke                       # fuzzing

Not in this milestone

Blend modes, transparency groups and soft masks (M6), shadings and images (M5/M7), glyph cache (M4), display list and PDF interpretation (M3). The Blitter/Shader interfaces are where these plug in.

Speed levers tried and measured (Ryzen 7 5800H, A3 at 150 dpi):

  • Band-sorted display list (record once, play band by band so the band stays in cache): slower on every stroke scene, 1 and 4–8 threads, at band heights 16–256. Operations crossing a band border are stroked once per band, which costs more than the cache misses saved; only uncached glyphs gained (−25 % with 8 workers). Still the right structure for M3, but not a speed lever on its own.
  • Deferred span compositing (spans appended to per-band streams, composited band by band): bit-identical, −10…17 % on scattered strokes, but +70 % on long hatch lines (recording every row), and deciding per operation which to defer needed fragile heuristics. Pre-touching the destination (above) gets the same gain without either.
  • Vectorizing the sweep: on a page-sized fill the integration loop is about 13 % of the time and its dirty groups are 4–8 cells, too short for a vector prefix sum; the rest is interior runs and edge compositing (BenchmarkWideFill). Not pursued.
  • Piecewise-quadratic strip coverage: the difference of the two trapezoid distributions along a row is a quadratic on each of up to nine pieces, so a row could walk the pieces instead of evaluating two branchy half-plane areas per pixel. Bit-identical, but slower on thin strokes: a piece ends at nearly every pixel of a 1–2 px strip, and the piece changes cost more than the areas. Hairline hatching +13 %. Not pursued.
  • A row kernel of its own for the analytic strip (so that its loop runs from registers): the call per row costs more than the spills it saves on rows of three pixels. What helped instead was keeping fewer values live across the pixel loop (the clip's border columns and the paint are read through pointers, the border rows are taken out of the loop) and keeping the per-row helpers small enough to inline: hatching −7…8 %.

What remains in stroke scenes is arithmetic: the analytic strip's per-pixel half-plane areas (trapezoid.area, branchy) and the stroker's outline and band setup; on scattered short strokes, the memory latency of the destination rows.

Documentation

Overview

Package stilus is a sparse CPU rasterizer for 2D vector graphics.

Its cost per path is the length of the path's edges in pixels plus the covered spans, never the area of the bounding box or the width of the canvas. It knows nothing about PDF, fonts or color spaces and has no dependencies, so it can serve as the CPU backend of a PDF renderer, a UI toolkit or another 2D library (for example as the CPU filler behind gogpu/gg).

Layers

Each layer can be used on its own:

  • Rasterizer turns device-space paths (Fill, or AddLine/AddPath and Rasterize) into coverage spans delivered to a Blitter: BlitRun for the constant interior runs of wide shapes, BlitCoverage for antialiased edge pixels and for narrow shapes as a whole. Implement Blitter to composite into any pixel format.
  • Stroker turns a stroke (width, caps, joins, miter limit, dashes; all in user space, exact under anisotropic transforms) into fill geometry for a LineSink, such as a Rasterizer or a PathSink.
  • Canvas combines both with a clip stack and compositing onto a caller-owned *image.RGBA. Its methods map one to one onto a display-list device: Fill, Stroke, ClipPath, ClipRect, PopClip.

Performance model

Edges are accumulated as signed area and cover into a band of cells (32 rows × clip width) in 24.8 fixed point. Per row, the touched cell range is integrated directly when it is narrow; wide rows are swept with a dirty bitset that skips untouched cells, and the constant stretches between edges are emitted as runs that blitters fill without a per-pixel loop. Nothing is cleared as a whole: the sweep zeroes what it reads.

Strokes are filled analytically between their corners: rows bounded only by a segment's two parallel sides get each pixel's exact area in closed form; only the rows around joins and caps go through the accumulator. Strokes thinner than one device pixel are drawn one pixel wide. Rectangle clips cost nothing per pixel; other clips are rasterized once into a mask over their bounds.

All buffers grow on demand and are retained: after warm-up, drawing performs no allocations. Rasterizer, Stroker and Canvas are not safe for concurrent use; use one per worker and give each worker its own band or tile of the destination (Canvas.Reset takes a region).

Robustness

NaN and infinite coordinates are rejected, huge ones are clipped analytically, curve and arc subdivision and dash counts are bounded, and the edge count per path is budgeted (Rasterizer.MaxEdges). Canvas never panics; it reports problems through Err and keeps drawing.

Example
package main

import (
	"fmt"
	"image"
	"image/color"

	"github.com/timzifer/stilus"
)

func main() {
	img := image.NewRGBA(image.Rect(0, 0, 200, 100))
	c := stilus.NewCanvas(img)

	// Page space in points, drawn at 144 dpi.
	m := stilus.Scale(2, 2)
	c.ClipRect(stilus.Rect{X0: 5, Y0: 5, X1: 95, Y1: 45}, m)

	var p stilus.Path
	p.MoveTo(10, 40)
	p.CubicTo(30, 0, 70, 80, 90, 10)
	c.Stroke(&p, m, &stilus.StrokeStyle{Width: 1.5, Cap: stilus.RoundCap}, &stilus.Paint{Color: color.RGBA{0, 0, 0, 255}})

	var r stilus.Path
	r.Ellipse(50, 25, 20, 12)
	c.Fill(&r, m, stilus.NonZero, &stilus.Paint{Color: color.RGBA{0, 0, 128, 128}}) // premultiplied

	c.PopClip()
	fmt.Println(c.Err())
}
Output:
<nil>

Index

Examples

Constants

View Source
const DefaultMaxClipDepth = 256

DefaultMaxClipDepth bounds the clip stack of a Canvas.

View Source
const DefaultMaxDashes = 1 << 20

DefaultMaxDashes bounds dash transitions per subpath. Subpaths estimated to exceed the budget are skipped; the subdivision loop also stops at the budget. Stroker.Truncated reports either condition.

View Source
const (

	// DefaultMaxEdges is the default edge budget per path.
	DefaultMaxEdges = 1 << 22
)

Variables

View Source
var (
	ErrEdgeBudget = errors.New("stilus: edge budget exceeded, path truncated")
	ErrDashBudget = errors.New("stilus: dash budget exceeded, stroke truncated")
	ErrClipDepth  = errors.New("stilus: clip stack too deep")
	ErrInternal   = errors.New("stilus: internal error")
)

Errors reported by Canvas.Err.

View Source
var Identity = Matrix{1, 0, 0, 1, 0, 0}

Identity is the identity transform.

Functions

func IsHairline

func IsHairline(m Matrix, st *StrokeStyle) bool

IsHairline reports whether st under m is thinner than one device pixel in every direction. Such strokes are drawn one pixel wide in device space, like PDFium.

func PackRGBA

func PackRGBA(c color.RGBA) uint32

PackRGBA packs a premultiplied color into the native pixel layout used by Shader.ShadeSpan.

func SIMD

func SIMD() bool

SIMD reports whether vectorized span kernels are in use. They require GOEXPERIMENT=simd (Go 1.26+) on amd64 with AVX2.

func UnpackRGBA

func UnpackRGBA(v uint32) color.RGBA

UnpackRGBA is the inverse of PackRGBA.

Types

type Blitter

type Blitter interface {
	// BlitRun paints the constant coverage alpha over [x0, x1) on row y:
	// the constant stretches between the edges of wide shapes.
	BlitRun(y, x0, x1 int, alpha uint8)
	// BlitCoverage paints per-pixel coverage for [x, x+len(cov)) on row y.
	// Narrow shapes (strokes, glyphs) arrive here as a whole, fully
	// covered pixels included.
	BlitCoverage(y, x int, cov []uint8)
}

Blitter receives the output of the rasterizer, one span at a time, in increasing y order and, within a row, in increasing x order. Spans never overlap and always lie inside the rasterizer's clip.

Implementations must not retain cov after the call returns.

type Canvas

type Canvas struct {
	// MaxClipDepth overrides DefaultMaxClipDepth when non-zero.
	MaxClipDepth int
	// contains filtered or unexported fields
}

Canvas draws paths onto a caller-owned *image.RGBA (premultiplied, as image.RGBA always is). It combines the rasterizer, the stroker and a clip stack, and maps one to one onto a display-list device: Fill/Stroke/ClipPath/ClipRect/PopClip.

Device space is the pixel space of the destination image, so a tile or band is rendered by passing an image whose Rect is that tile, or by restricting Reset's region. A Canvas is not safe for concurrent use; use one per worker. After warm-up a Canvas does not allocate.

func NewCanvas

func NewCanvas(dst *image.RGBA) *Canvas

NewCanvas returns a canvas drawing onto dst.

func (*Canvas) Clip

func (c *Canvas) Clip() image.Rectangle

Clip returns the device-space bounds of the current clip.

func (*Canvas) ClipDepth

func (c *Canvas) ClipDepth() int

ClipDepth returns the number of clips pushed since Reset.

func (*Canvas) ClipPath

func (c *Canvas) ClipPath(p *Path, m Matrix, rule FillRule)

ClipPath intersects the clip with p under m. Rectangles ("re W n") take the rectangle fast path; other shapes are rasterized once into a mask over their bounds.

func (*Canvas) ClipRect

func (c *Canvas) ClipRect(r Rect, m Matrix)

ClipRect intersects the clip with rectangle r in user space. An axis-aligned result costs nothing per pixel; a rotated one becomes a mask.

func (*Canvas) Err

func (c *Canvas) Err() error

Err returns the first error since Reset, if any. Drawing continues after an error; the error only reports that output may be incomplete.

func (*Canvas) Fill

func (c *Canvas) Fill(p *Path, m Matrix, rule FillRule, paint *Paint)

Fill fills p, transformed by m into device space, with the fill rule.

func (*Canvas) PopClip

func (c *Canvas) PopClip()

PopClip removes the most recent clip.

func (*Canvas) Reset

func (c *Canvas) Reset(dst *image.RGBA, region image.Rectangle)

Reset retargets the canvas to dst, limited to region (in dst's coordinates), clears the clip stack and the error.

func (*Canvas) Stroke

func (c *Canvas) Stroke(p *Path, m Matrix, st *StrokeStyle, paint *Paint)

Stroke strokes p with style st (in user space) under m. Strokes thinner than a device pixel are drawn one device pixel wide.

type Cap

type Cap uint8

Cap is the shape of open stroke ends.

const (
	ButtCap Cap = iota
	RoundCap
	SquareCap
)

type FillRule

type FillRule uint8

FillRule selects how winding numbers map to inside/outside.

const (
	NonZero FillRule = iota
	EvenOdd
)

type Join

type Join uint8

Join is the shape of stroke corners.

const (
	MiterJoin Join = iota
	RoundJoin
	BevelJoin
)

type LineSink

type LineSink interface {
	AddLine(x0, y0, x1, y1 float64)
}

LineSink receives device-space line segments. Segments emitted by the Stroker form closed polygons that must be filled with the NonZero rule. *Rasterizer implements LineSink.

type MaskBlitter

type MaskBlitter struct {
	Mask *image.Alpha
}

MaskBlitter accumulates coverage into an 8-bit alpha image (for clip masks, soft masks and tests). Coverage is combined with "max", which is exact for the non-overlapping spans of a single rasterization.

func (*MaskBlitter) BlitCoverage

func (b *MaskBlitter) BlitCoverage(y, x int, cov []uint8)

func (*MaskBlitter) BlitRun

func (b *MaskBlitter) BlitRun(y, x0, x1 int, alpha uint8)

type Matrix

type Matrix [6]float64

Matrix is an affine transform in PDF order [a b c d e f]:

x' = a·x + c·y + e
y' = b·x + d·y + f

func Rotate

func Rotate(rad float64) Matrix

Rotate returns a rotation by rad radians.

func Scale

func Scale(sx, sy float64) Matrix

Scale returns a scaling transform.

func Translate

func Translate(tx, ty float64) Matrix

Translate returns a translation.

func (Matrix) Apply

func (m Matrix) Apply(x, y float64) (float64, float64)

Apply transforms (x, y).

func (Matrix) ApplyVec

func (m Matrix) ApplyVec(x, y float64) (float64, float64)

ApplyVec transforms the vector (x, y), ignoring translation.

func (Matrix) Det

func (m Matrix) Det() float64

Det returns the determinant of the linear part.

func (Matrix) Invert

func (m Matrix) Invert() (inv Matrix, ok bool)

Invert returns the inverse transform; ok is false if m is singular.

func (Matrix) Mul

func (m Matrix) Mul(n Matrix) Matrix

Mul returns the transform that applies m first and then n.

type Paint

type Paint struct {
	Color  color.RGBA // premultiplied
	Shader Shader
}

Paint describes what a fill or stroke paints: a solid premultiplied color, or a Shader when Shader is non-nil.

type Path

type Path struct {
	Verbs  []Verb
	Points []Point
}

Path is a sequence of subpaths. The zero value is an empty path ready to use. Reset keeps the backing storage, so a Path can be rebuilt many times without allocating.

func (*Path) Bounds

func (p *Path) Bounds() Rect

Bounds returns the bounding box of all points (including control points).

func (*Path) Close

func (p *Path) Close()

Close closes the current subpath.

func (*Path) CubicTo

func (p *Path) CubicTo(c1x, c1y, c2x, c2y, x, y float32)

CubicTo adds a cubic Bézier segment.

func (*Path) Ellipse

func (p *Path) Ellipse(cx, cy, rx, ry float32)

Ellipse appends a closed ellipse centred at (cx, cy).

func (*Path) Empty

func (p *Path) Empty() bool

Empty reports whether the path has no drawing commands.

func (*Path) LineTo

func (p *Path) LineTo(x, y float32)

LineTo adds a line segment to (x, y).

func (*Path) MoveTo

func (p *Path) MoveTo(x, y float32)

MoveTo starts a new subpath at (x, y).

func (*Path) QuadTo

func (p *Path) QuadTo(cx, cy, x, y float32)

QuadTo adds a quadratic Bézier segment.

func (*Path) Rect

func (p *Path) Rect(x, y, w, h float32)

Rect appends a closed rectangle subpath (PDF "re").

func (*Path) Reset

func (p *Path) Reset()

Reset empties the path, keeping its storage.

type PathSink

type PathSink struct{ P *Path }

PathSink collects line segments into a Path.

func (PathSink) AddLine

func (s PathSink) AddLine(x0, y0, x1, y1 float64)

type Point

type Point struct{ X, Y float32 }

Point is a 2D point. Path coordinates are stored as float32 to keep paths (and display lists built on top of them) compact.

type Rasterizer

type Rasterizer struct {

	// MaxEdges bounds the number of edges per path. Edges beyond it are
	// dropped and Truncated reports true. Zero means DefaultMaxEdges.
	MaxEdges int
	// contains filtered or unexported fields
}

Rasterizer converts paths into coverage spans. Its cost per path is the length of the path's edges in pixels plus the number of covered spans; it never touches the area of the bounding box.

A Rasterizer is not safe for concurrent use; use one per worker. All buffers grow on demand and are retained, so steady-state use performs no allocations.

Example
package main

import (
	"fmt"
	"image"

	"github.com/timzifer/stilus"
)

// areaBlitter shows the span interface used to plug the rasterizer into
// another compositor: it just integrates the coverage.
type areaBlitter struct{ area int }

func (b *areaBlitter) BlitRun(y, x0, x1 int, alpha uint8) { b.area += (x1 - x0) * int(alpha) }
func (b *areaBlitter) BlitCoverage(y, x int, cov []uint8) {
	for _, a := range cov {
		b.area += int(a)
	}
}

func main() {
	r := stilus.NewRasterizer(image.Rect(0, 0, 100, 100))
	var p stilus.Path
	p.Rect(10.5, 10.5, 50, 30)
	p.Rect(20, 60, 400, 10) // wide: interior rows arrive as runs
	var b areaBlitter
	r.Fill(&p, stilus.Identity, stilus.NonZero, &b)
	fmt.Printf("%.0f px²\n", float64(b.area)/255)
}
Output:
2300 px²

func NewRasterizer

func NewRasterizer(clip image.Rectangle) *Rasterizer

NewRasterizer returns a rasterizer clipped to clip.

func (*Rasterizer) AddLine

func (r *Rasterizer) AddLine(x0, y0, x1, y1 float64)

AddLine adds a device-space line segment. Segments are clipped analytically: parts above or below the clip are dropped, parts right of it are dropped, and parts left of it become vertical edges on the clip's left border, which preserves the winding of everything inside.

func (*Rasterizer) AddPath

func (r *Rasterizer) AddPath(p *Path, m Matrix)

AddPath adds the edges of p transformed by m. Open subpaths are closed implicitly. Curves are flattened in device space with a tolerance of a quarter pixel. Paths containing NaN or infinite values are ignored.

func (*Rasterizer) Clip

func (r *Rasterizer) Clip() image.Rectangle

Clip returns the current clip rectangle.

func (*Rasterizer) Fill

func (r *Rasterizer) Fill(p *Path, m Matrix, rule FillRule, b Blitter)

Fill rasterizes p transformed by m with the given fill rule. It is equivalent to Reset, AddPath and Rasterize.

func (*Rasterizer) Rasterize

func (r *Rasterizer) Rasterize(rule FillRule, b Blitter)

Rasterize converts all added edges into spans and resets the rasterizer.

func (*Rasterizer) Reset

func (r *Rasterizer) Reset()

Reset discards all accumulated edges and the Truncated state.

func (*Rasterizer) SetAntialias

func (r *Rasterizer) SetAntialias(aa bool)

SetAntialias enables (default) or disables antialiasing. Without antialiasing a pixel is covered when at least half of it is inside.

func (*Rasterizer) SetClip

func (r *Rasterizer) SetClip(clip image.Rectangle)

SetClip sets the device-space clip rectangle. Every span produced lies inside it. Buffers are resized only when the clip grows.

func (*Rasterizer) Truncated

func (r *Rasterizer) Truncated() bool

Truncated reports whether the last path exceeded the edge budget.

type Rect

type Rect struct{ X0, Y0, X1, Y1 float64 }

Rect is an axis-aligned rectangle [X0,X1)×[Y0,Y1).

func (Rect) Empty

func (r Rect) Empty() bool

Empty reports whether r has no area. NaN bounds count as empty.

func (Rect) Intersect

func (r Rect) Intersect(s Rect) Rect

Intersect returns the intersection of r and s.

type Shader

type Shader interface {
	ShadeSpan(y, x int, dst []uint32)
}

Shader produces premultiplied colors (in PackRGBA layout) for the pixels [x, x+len(dst)) on row y. It is called once per span, never per pixel.

type ShaderBlitter

type ShaderBlitter struct {
	// contains filtered or unexported fields
}

ShaderBlitter composites the output of a Shader onto an image.RGBA.

func NewShaderBlitter

func NewShaderBlitter(dst *image.RGBA, s Shader) *ShaderBlitter

NewShaderBlitter returns a blitter painting s onto dst.

func (*ShaderBlitter) BlitCoverage

func (b *ShaderBlitter) BlitCoverage(y, x int, cov []uint8)

func (*ShaderBlitter) BlitRun

func (b *ShaderBlitter) BlitRun(y, x0, x1 int, alpha uint8)

func (*ShaderBlitter) Reset

func (b *ShaderBlitter) Reset(dst *image.RGBA, s Shader)

Reset retargets the blitter without allocating.

type SolidBlitter

type SolidBlitter struct {
	// contains filtered or unexported fields
}

SolidBlitter composites a single premultiplied color onto an image.RGBA.

func NewSolidBlitter

func NewSolidBlitter(dst *image.RGBA, c color.RGBA) *SolidBlitter

NewSolidBlitter returns a blitter painting c (premultiplied) onto dst.

func (*SolidBlitter) BlitCoverage

func (b *SolidBlitter) BlitCoverage(y, x int, cov []uint8)

func (*SolidBlitter) BlitRun

func (b *SolidBlitter) BlitRun(y, x0, x1 int, alpha uint8)

func (*SolidBlitter) Reset

func (b *SolidBlitter) Reset(dst *image.RGBA, c color.RGBA)

Reset retargets the blitter without allocating.

func (*SolidBlitter) SetColor

func (b *SolidBlitter) SetColor(c color.RGBA)

SetColor changes the paint color (premultiplied).

type StrokeStyle

type StrokeStyle struct {
	Width      float64 // 0 means the thinnest line the device can render
	Cap        Cap
	Join       Join
	MiterLimit float64   // PDF default 10; values < 1 are treated as 1
	Dash       []float64 // on/off lengths; empty means solid
	DashPhase  float64
}

StrokeStyle describes a stroke in user space (the space the path is defined in), like the PDF graphics state.

type Stroker

type Stroker struct {
	// MaxDashes overrides DefaultMaxDashes when non-zero.
	MaxDashes int
	// contains filtered or unexported fields
}

Stroker converts strokes into fill geometry (closed outlines to be filled with NonZero). Offsets are computed in user space and transformed afterwards, which is exact for anisotropic transforms.

A Stroker is not safe for concurrent use. Buffers are retained.

func (*Stroker) Coverage added in v0.1.1

func (s *Stroker) Coverage() float64

Coverage reports the factor to apply to the coverage of the last stroke's outline: the mean on-fraction of a dash pattern emitted as a solid stroke, else 1.

func (*Stroker) Stroke

func (s *Stroker) Stroke(sink LineSink, p *Path, m Matrix, st *StrokeStyle)

Stroke emits the outline of p stroked with st under m into sink.

A dash pattern too dense to resolve in device space is emitted as a solid stroke; Coverage then reports the fraction the caller must scale the resulting coverage by.

func (*Stroker) Truncated

func (s *Stroker) Truncated() bool

Truncated reports whether the last stroke exceeded its dash budget. Canvas reports this condition as ErrDashBudget.

type Verb

type Verb uint8

Verb is a path construction command.

const (
	MoveTo  Verb = iota // 1 point
	LineTo              // 1 point
	QuadTo              // 2 points
	CubicTo             // 3 points
	Close               // 0 points
)

Directories

Path Synopsis
cmd
stilus-scenes command
Command stilus-scenes renders the benchmark scenes, prints timings and allocations and optionally writes PNGs for visual inspection.
Command stilus-scenes renders the benchmark scenes, prints timings and allocations and optionally writes PNGs for visual inspection.
internal
scenes
Package scenes provides deterministic benchmark scenes modelled on technical drawings: an A3 landscape page in PDF points, drawn under a scale to the requested resolution with a rectangular page clip.
Package scenes provides deterministic benchmark scenes modelled on technical drawings: an A3 landscape page in PDF points, drawn under a scale to the requested resolution with a rectangular page clip.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL