stilus

package module
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Oct 6, 2026 License: MIT Imports: 11 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()

// Prepared geometry for banded rendering: set once per page, drawn by
// every band's canvas (concurrently), each replaying only its own rows.
var sh stilus.Shape
sh.SetStroke(path, ctm, style)                    // or SetFill(path, ctm, rule)
c.FillShape(&sh, paint)                           // same bytes as c.Stroke(path, ctm, style, paint)

// Union: many fills and strokes of one paint accumulated together and
// composited once, without seams where they abut or overlap.
var u stilus.Union
u.Stroke(line1, ctm, style)                       // u.Fill(path, ctm, rule), u.Shape(&sh)
u.Stroke(line2, ctm, style)
c.FillUnion(&u, paint)

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

Shaders

Paints beyond a solid colour, each a Shader for Paint.Shader; they know nothing of PDF either, and serve cera as well as any 2D library:

// Images: one byte, one bit or four bytes a pixel, mip levels made on
// first use, nearest or bilinear, with an optional alpha mask.
tex := stilus.NewTexture(stilus.Plane{Kind: stilus.PlaneIndex, W: w, H: h, Stride: w, Pix8: pix, Pal: stilus.GrayPalette})
var img stilus.ImageShader
img.SetImage(tex, toDevice, false, 255)        // toDevice: texture pixels → device

// Tiling patterns: a tile repeated under any affine transform; an alpha
// tile with SetColor is the stencil of an uncoloured pattern.
img.SetImageWrap(tile, toDevice, false, 255)   // or SetColor(c) + SetMaskWrap(alphaTile, …)

// Gradients: a ramp of premultiplied colours over t in [0, 1], evenly
// spaced or at knots (interpolated, with exact hard stops).
var g stilus.LinearGradient                    // or RadialGradient (two circles)
g.Ramp, g.Alpha, g.Extend = ramp, 255, [2]bool{true, true}
g.Knots = knots                                // optional, one per ramp entry
g.Set(x0, y0, x1, y1, m)

// Gouraud meshes, without seams: a shader for the mesh's outline or clip,
// set once for all bands, or drawn into a layer.
var mesh stilus.MeshShader
mesh.Alpha = 255
mesh.Set(triangles, m, nil)
c.Fill(outline, m, stilus.NonZero, &stilus.Paint{Shader: &mesh})
stilus.FillMesh(layer, region, triangles, m, nil)

// Layers with opacity, mask and the 16 W3C/PDF blend modes, exact at
// antialiased edges; a non-isolated group drawn onto a copy of its backdrop
// sets Initial (that backdrop) and Alone (the group drawn alone), so that
// the backdrop is removed before blending. Glyph coverage masks per size
// and quarter pixel.
var gc stilus.GlyphCache
gc.FillGlyph(c, fontID, glyphID, outline, m, paint)

Canvas.ClipStroke clips to the area a stroke paints, for strokes and stroked text painted with a shader.

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).
  • Prepared geometry (Shape, ADR 0005): a page rendered in bands plays every operation in every band, and an operation crossing many bands would be transformed, flattened, dashed and stroked once per band. A Shape records what the rasterizer and the analytic strips receive – device edges with their row limits, strips with their set-up done – binned by rows; a band replays the records that reach it. They are made by the first band that draws the shape, so the work stays in the workers. Byte-identical to Fill/Stroke; 2 000 hatch lines drawn as 64 bands on one core: 87 → 54 ms (one band: 46 ms).
  • 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.

History

BenchmarkScenes at 150 dpi on one core, for every release tag and for main, rendered with figure by the Benchmarks workflow. Timings from different machines do not compare, so each run measures all of them anew on one GitHub runner, round-robin, and replaces the chart: its level can move from one run to the next, the relations within it are what it shows.

BenchmarkScenes at 150 dpi per release

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.
  • Conflation: shapes of one paint drawn one after another leave light seams where they abut or overlap along antialiased edges, 1 − (1 − a)(1 − b) instead of a + b. Union and Canvas.FillUnion sum their coverage in one accumulator instead and match the supersampled union (TestFillUnionRef; ADR 0006).
  • 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
bench/scripts/bench-refs.sh h.json v0.2.0 main   # measure refs side by side, then:
(cd bench && go run ./cmd/benchchart render -db ../h.json -o ../h.svg)
go test -fuzz FuzzStroke                       # fuzzing

The benchmark history (BenchmarkScenes at 150 dpi per release, charted by CI on the badges branch) always uses the scenes of the working tree: bench-refs.sh builds every ref with the current internal/scenes. A scene added now is therefore charted over past releases as well. Files of internal/scenes that do not compile against a release, because they use API it lacks, are left out of that release, so the scene's line starts at the first release that can draw it.

To add a scene, put it in a file of its own in internal/scenes that appends its constructor to more in init. scenes.go holds the framework and must keep compiling against every release. Then chart it before committing, with @ standing for the working tree:

bench/scripts/bench-refs.sh h.json $(git tag -l 'v*') dev=@
(cd bench && go run ./cmd/benchchart render -db ../h.json -o ../h.svg)

Not here

Display lists, PDF interpretation, colour spaces, transparency groups as PDF defines them (knockout, soft masks from groups) and caches across documents belong to the renderer (cera). The Blitter/Shader interfaces are where they 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. Prepared geometry (Shape) removes the per-band stroking for operations spanning three bands or more; for one crossing a single border, drawing it twice stays cheaper.
  • 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 %.
  • Fewer branches in the half-plane area (area from |u|: one test for the linear middle, then the quadratic of max(h − |u|, 0), mirrored by the sign of u): bit-identical, but slower everywhere, measured interleaved: hatching +33…42 %, short strokes +9 %. The cases of area follow the pixels of a strip in a pattern the branch predictor learns; the longer dependency chain costs more than the mispredictions it saves. Choosing it only for strips of few rows: short strokes +7…8 %. There is little to skip either: 95 % of the pixels a hairline hatch row visits are partially covered, none fully, 5 % not at all. Not pursued.

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, ClipStroke, PopClip.
  • Shaders paint what a solid colour cannot, per span: ImageShader (Texture with mip levels, edge-repeating or periodic for pattern tiles), LinearGradient and RadialGradient (a Ramp of colours, evenly spaced or at Knots), MeshShader (Gouraud-shaded triangles), LayerShader (a layer with opacity, mask and BlendMode, removing the backdrop of non-isolated groups), MaskShader and GlyphCache (glyph coverage masks). FillMesh draws Gouraud-shaded triangles into a layer.

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 (
	// MaxCachedEm is the em size in device pixels above which glyphs are
	// filled as paths: large glyphs are few, and their masks big.
	MaxCachedEm = 160
	// DefaultGlyphCacheBytes bounds the masks of a cache whose MaxBytes
	// is zero; a full cache evicts masks to make room.
	DefaultGlyphCacheBytes = 4 << 20
)
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
)
View Source
const DefaultShapeBytes = DefaultMaxEdges * shapeEdgeCost

DefaultShapeBytes is the default per-form budget of a Shape: enough for DefaultMaxEdges edges.

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 GrayPalette, AlphaPalette = func() (*Palette, *Palette) {
	var g, a Palette
	for i := range 256 {
		v := uint8(i)
		g[i] = pack(v, v, v, 255)
		a[i] = pack(v, v, v, v)
	}
	return &g, &a
}()

Shared palettes: opaque greys and levels of alpha (white premultiplied, so all four channels are the level). They must not be modified.

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

Identity is the identity transform.

Functions

func Blend added in v0.5.0

func Blend(bm BlendMode, cb, cs [3]float64) [3]float64

Blend returns B(Cb, Cs), the blend function of bm, for a backdrop and a source colour of straight components in [0, 1].

func FillMesh added in v0.5.0

func FillMesh(dst *image.RGBA, region image.Rectangle, tris []MeshTriangle, m Matrix, ramp Ramp)

FillMesh draws tris, transformed by m into device space, into dst within region, interpolating the colours of their vertices linearly (or, with a non-empty ramp, their parameters, looked up in the ramp). Pixels are replaced, not composited: the mesh is meant to be drawn into a transparent layer that is then composited through a Canvas, for example with a LayerShader, so that the mesh is clipped and blended like any fill. A triangle covers the pixels whose centres lie inside it, by the top-left rule, without antialiasing, so that triangles sharing an edge leave no seam and draw no pixel twice; later triangles overwrite earlier ones where they overlap. MeshShader paints the same pixels without a layer.

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 BlendMode added in v0.5.0

type BlendMode uint8

BlendMode is a separable or non-separable blend mode of the W3C Compositing and Blending specification, which are also those of PDF (ISO 32000-2, 11.3.5) and of SVG and CSS.

const (
	BlendNormal BlendMode = iota
	BlendMultiply
	BlendScreen
	BlendOverlay
	BlendDarken
	BlendLighten
	BlendColorDodge
	BlendColorBurn
	BlendHardLight
	BlendSoftLight
	BlendDifference
	BlendExclusion
	BlendHue
	BlendSaturation
	BlendColor
	BlendLuminosity
)

Blend modes.

func (BlendMode) String added in v0.5.0

func (b BlendMode) String() string

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) ClipStroke added in v0.5.0

func (c *Canvas) ClipStroke(p *Path, m Matrix, st *StrokeStyle)

ClipStroke intersects the clip with the area a stroke of p with style st (in user space) under m paints, with the stroke's coverage: text in a stroking clip mode, or a stroke painted with a shader that only reaches the stroke as a clip. The area is rasterized once into 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) FillShape added in v0.8.0

func (c *Canvas) FillShape(s *Shape, paint *Paint)

FillShape draws the shape s, prepared by SetFill or SetStroke, with paint: the bytes Fill or Stroke draw with the same path, transform and style, at the cost of the records that cross the clip's rows.

func (*Canvas) FillUnion added in v0.8.0

func (c *Canvas) FillUnion(u *Union, paint *Paint)

FillUnion paints the union of the elements of u with paint: their coverage is accumulated in one pass, NonZero, and composited once (see Union). For opaque paint the result is that of drawing them one by one, without the seams between them; translucent paint is applied once where elements overlap.

Strokes take Canvas.Stroke's analytic path, whatever the paint and the clip: the rows between a segment's ends are not edge-walked but their pixels' covered areas computed in closed form, and here they are summed in the accumulator with everything else instead of being composited directly.

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 GlyphCache added in v0.5.0

type GlyphCache struct {
	// MaxBytes bounds the masks kept, counted by their allocations plus
	// 64 bytes an entry; 0 means DefaultGlyphCacheBytes. A mask larger
	// than the budget is drawn but not kept. Evicted masks kept for reuse
	// take at most an eighth more; the rasterizer's scratch, sized by the
	// largest mask made, and the map are not counted.
	MaxBytes int
	// contains filtered or unexported fields
}

GlyphCache is a cache of glyph coverage masks. The zero value is ready to use.

func (*GlyphCache) FillGlyph added in v0.5.0

func (gc *GlyphCache) FillGlyph(c *Canvas, font uint64, glyph int32, outline *Path, m Matrix, paint *Paint)

FillGlyph fills outline (nonzero), transformed by m into device space, with paint through c. The outline is identified by font and glyph: the cache assumes that equal ids have equal outlines. Small glyphs painted with a solid colour are drawn from the cache; large ones, and any glyph painted with a shader, are filled as paths.

FillGlyph makes the glyph's key and looks it up before it learns that the glyph lies outside the clip, and a per-glyph test would need the outline's bounds. When a page is drawn in bands, each playing every glyph, the caller should cull whole text runs against the band (Canvas.Clip) by their box, known from the font's bounding box and the advances, before calling FillGlyph for their glyphs.

type Gradient added in v0.5.0

type Gradient struct {
	// Ramp maps the gradient's parameter to colours.
	Ramp Ramp
	// Extend continues the colour at t = 0 before the start (Extend[0])
	// and the colour at t = 1 past the end (Extend[1]); where the gradient
	// does not extend it paints Outside.
	Extend [2]bool
	// Outside is the premultiplied colour (PackRGBA layout) painted
	// where the gradient is not defined; 0 is transparent.
	Outside uint32
	// Alpha multiplies the gradient's colours; 255 is opaque, 0 paints
	// nothing.
	Alpha uint8
	// Knots, if not empty, are the parameters of the Ramp's entries: as
	// many as entries, non-decreasing, within [0, 1]. Colours are then
	// interpolated linearly between the two entries around t, and a knot
	// given twice is a hard stop exactly there (t at the stop takes the
	// later entry); t before the first knot or past the last takes the
	// colour of the first or last entry. Without knots the entries are
	// evenly spaced and looked up nearest. Set reads the knots: call it
	// again after changing them.
	Knots []float32
	// contains filtered or unexported fields
}

Gradient is what linear and radial gradients share.

type ImageShader added in v0.5.0

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

ImageShader paints a texture (or a solid colour) times a constant alpha, times an optional mask texture whose colours are levels of alpha (all four channels equal, such as AlphaPalette). The zero value paints transparent black; set it up with SetColor or SetImage, and SetMask.

func (*ImageShader) Reset added in v0.5.0

func (s *ImageShader) Reset()

Reset clears the shader, keeping its buffers, and drops its textures.

func (*ImageShader) SetColor added in v0.5.0

func (s *ImageShader) SetColor(c uint32)

SetColor paints the premultiplied colour c (PackRGBA layout) instead of a texture: a stencil.

func (*ImageShader) SetImage added in v0.5.0

func (s *ImageShader) SetImage(t *Texture, toDevice Matrix, smooth bool, alpha uint8) bool

SetImage paints t, whose base pixel space toDevice maps to device space, with constant alpha. It reports false if t cannot be drawn so.

func (*ImageShader) SetImageWrap added in v0.6.0

func (s *ImageShader) SetImageWrap(t *Texture, toDevice Matrix, smooth bool, alpha uint8) bool

SetImageWrap is SetImage for a texture that repeats in both directions with the period of its base size (see Sampler.SetupWrap): the tile of a coloured pattern.

func (*ImageShader) SetMask added in v0.5.0

func (s *ImageShader) SetMask(t *Texture, toDevice Matrix, smooth bool) bool

SetMask multiplies the paint by mask texture t, whose base pixel space toDevice maps to device space. It reports false if t cannot be drawn so.

func (*ImageShader) SetMaskWrap added in v0.6.0

func (s *ImageShader) SetMaskWrap(t *Texture, toDevice Matrix, smooth bool) bool

SetMaskWrap is SetMask for a repeating mask texture: with SetColor, the stencil tile of an uncoloured pattern.

func (*ImageShader) ShadeSpan added in v0.5.0

func (s *ImageShader) ShadeSpan(y, x int, dst []uint32)

ShadeSpan implements Shader.

type Join

type Join uint8

Join is the shape of stroke corners.

const (
	MiterJoin Join = iota
	RoundJoin
	BevelJoin
)

type LayerShader added in v0.5.0

type LayerShader struct {
	// Src is the layer, premultiplied like any image.RGBA; spans must lie
	// within its rectangle.
	Src *image.RGBA
	// Dst is the backdrop the canvas draws onto, read for blend modes
	// other than Normal.
	Dst *image.RGBA
	// Mask, when non-nil, multiplies the layer; it is 0 outside its
	// rectangle.
	Mask *image.Alpha
	// Alpha is the constant opacity of the layer.
	Alpha uint8
	Blend BlendMode
	// Initial and Alone, when both are non-nil, make Src the result of a
	// non-isolated group: Initial is the backdrop the group started from
	// and Alone the same group drawn onto a transparent image, of which
	// only the alpha is read. Both cover Src's rectangle.
	Initial, Alone *image.RGBA
}

LayerShader composites a layer, an isolated group drawn into an image of its own, onto the backdrop it is drawn over: the layer's pixels times Alpha and the optional Mask, blended with the backdrop by Blend. Fill the layer's rectangle with it through a Canvas, so that the layer is composited through the clip stack and antialiased at the clip's edges like any fill.

Blend modes are exact under partial coverage: the canvas composites the shader's colour s over the backdrop d with coverage a as a·s + d·(1−a·αs), and the shader returns s = cs·(1−αb) + αs·αb·B(Cb, Cs) with alpha αs, which makes that the compositing formula of the specification interpolated by coverage.

A non-isolated group is drawn onto a copy of its backdrop, so its layer includes the backdrop; blending or knocking out that layer would count the backdrop twice. With Initial and Alone set, the shader first removes the backdrop's contribution as in PDF 2.0, 11.4.8: C = Cn + (Cn − C0)·(α0/αgn − α0), Cn being the layer's colour, C0 and α0 the backdrop's, and αgn the alpha of the group drawn alone; the result, with alpha αgn, is then composited like any layer.

func (*LayerShader) ShadeSpan added in v0.5.0

func (c *LayerShader) ShadeSpan(y, x int, out []uint32)

ShadeSpan implements Shader.

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.

The outline of a stroke winds in one direction everywhere it covers, inner corners, joins, caps and dashes included: its winding number is never positive where another part of it is negative. The direction is that of a stroke under the identity unless the transform mirrors (negative determinant) and the stroke is not a hairline, whose outline is built in device space. So the union of many strokes, oriented alike, is one NonZero fill of their outlines (see Union, TestStrokeOutlineWinding).

type LinearGradient added in v0.5.0

type LinearGradient struct {
	Gradient
	// contains filtered or unexported fields
}

LinearGradient is an axial gradient: t runs from 0 at P0 to 1 at P1 and is constant on lines perpendicular to P0P1 (in the gradient's own space, which may be mapped to device space by any affine transform).

func (*LinearGradient) Set added in v0.5.0

func (g *LinearGradient) Set(x0, y0, x1, y1 float64, m Matrix) bool

Set places the gradient from (x0, y0) to (x1, y1) in a space that m maps to device space, and reports whether it can be drawn: false for coincident points, singular or non-finite transforms and unusable Knots. It keeps Ramp, Extend, Outside, Alpha and Knots.

func (*LinearGradient) ShadeSpan added in v0.5.0

func (g *LinearGradient) ShadeSpan(y, x int, dst []uint32)

ShadeSpan implements Shader.

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 MaskShader added in v0.5.0

type MaskShader struct {
	Mask  *image.Alpha
	X, Y  int
	Color uint32
}

MaskShader paints a solid premultiplied colour (PackRGBA layout) through a coverage mask whose origin is at (X, Y) in device space. Spans must lie within the mask's rectangle moved by (X, Y).

func (*MaskShader) ShadeSpan added in v0.5.0

func (s *MaskShader) ShadeSpan(y, x int, dst []uint32)

ShadeSpan implements Shader.

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 MeshShader added in v0.7.0

type MeshShader struct {
	// Alpha multiplies the mesh's colours; 255 is opaque, 0 paints
	// nothing.
	Alpha uint8
	// contains filtered or unexported fields
}

MeshShader paints triangles, transformed into device space, interpolating their vertex colours (or ramp parameters) barycentrically, without a layer. Pixels covered by no triangle are transparent. Fill the mesh's outline, the caller's path or the clip with it through a Canvas, so that the outer edge is antialiased and clipped like any fill. Inside the mesh a pixel gets exactly the colour FillMesh gives it.

Set transforms and bins the triangles once; ShadeSpan then costs the triangles that cross the span's row, so one shader serves every band of a page. After Set, ShadeSpan may be called by several workers at once.

func (*MeshShader) Set added in v0.7.0

func (s *MeshShader) Set(tris []MeshTriangle, m Matrix, ramp Ramp) bool

Set prepares the shader for tris, which m maps to device space; with a non-empty ramp the vertices' T is looked up in it. It reports false for singular or non-finite transforms, and keeps its buffers. The shader keeps ramp, not tris.

func (*MeshShader) ShadeSpan added in v0.7.0

func (s *MeshShader) ShadeSpan(y, x int, dst []uint32)

ShadeSpan implements Shader.

type MeshTriangle added in v0.5.0

type MeshTriangle [3]MeshVertex

MeshTriangle is a Gouraud-shaded triangle.

type MeshVertex added in v0.5.0

type MeshVertex struct {
	X, Y float32
	C    uint32  // premultiplied, PackRGBA layout
	T    float32 // parameter into the ramp, 0 to 1
}

MeshVertex is a vertex of a Gouraud-shaded triangle: a point and either a premultiplied colour or a parameter into a Ramp.

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 Palette added in v0.5.0

type Palette [256]uint32

Palette maps samples to premultiplied colours in PackRGBA layout.

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 Plane added in v0.5.0

type Plane struct {
	Kind   PlaneKind
	W, H   int
	Stride int // pixels per row for PlaneRGBA and PlaneIndex, bytes for PlaneBits
	Pix8   []uint8
	Pix32  []uint32
	Pal    *Palette
}

Plane is a picture of W × H pixels.

func (*Plane) At added in v0.5.0

func (p *Plane) At(x, y int) uint32

At returns the premultiplied colour of pixel (x, y), which must be inside the plane.

func (*Plane) Bytes added in v0.5.0

func (p *Plane) Bytes() int

Bytes returns the memory the samples of p take.

type PlaneKind added in v0.5.0

type PlaneKind uint8

PlaneKind is how a Plane stores its samples.

const (
	PlaneRGBA  PlaneKind = iota // Pix32, premultiplied, in PackRGBA layout
	PlaneIndex                  // Pix8, indexes into Pal
	PlaneBits                   // Pix8, one bit a pixel (MSB first) indexing Pal[0] or Pal[1]
)

Plane kinds.

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 RadialGradient added in v0.5.0

type RadialGradient struct {
	Gradient
	// contains filtered or unexported fields
}

RadialGradient is a gradient between two circles: the circle at t has its centre at C0 + t·(C1 − C0) and its radius r0 + t·(r1 − r0), and a point takes the colour of the largest t whose circle passes through it with a radius ≥ 0 (in the extended range, if the gradient extends). This is the two-point conical gradient of PDF (type 3 shadings), of the HTML canvas and of SVG with a focal point.

func (*RadialGradient) Set added in v0.5.0

func (g *RadialGradient) Set(x0, y0, r0, x1, y1, r1 float64, m Matrix) bool

Set places the gradient between the circles (x0, y0, r0) and (x1, y1, r1) in a space that m maps to device space, and reports whether it can be drawn: false for negative radii, singular or non-finite transforms and unusable Knots. It keeps Ramp, Extend, Outside, Alpha and Knots.

func (*RadialGradient) ShadeSpan added in v0.5.0

func (g *RadialGradient) ShadeSpan(y, x int, dst []uint32)

ShadeSpan implements Shader.

type Ramp added in v0.5.0

type Ramp []uint32

Ramp is a colour ramp: premultiplied colours (PackRGBA layout) at evenly spaced parameters from 0 to 1, or at the parameters a gradient's Knots give. A gradient with an empty ramp paints nothing.

func (Ramp) At added in v0.5.0

func (r Ramp) At(t float64) uint32

At returns the colour of the ramp nearest to t, clamped to [0, 1].

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 Sampler added in v0.5.0

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

Sampler reads a texture at device pixels. It caches what nearest sampling along the device axes shares between pixels (the texel column of every device column, the expanded rows of a magnified texture), so it is not safe for concurrent use; use one per worker.

func (*Sampler) Release added in v0.5.0

func (s *Sampler) Release()

Release drops the sampler's reference to its texture.

func (*Sampler) Sample added in v0.5.0

func (s *Sampler) Sample(y, x int, dst []uint32)

Sample writes the colours of pixels [x, x+len(dst)) of row y. Every pixel's coordinates are computed from its own position rather than accumulated along the span, so that a pixel samples the same texel whichever band, tile or span it is drawn in. Outside the texture the edge pixels repeat, or with SetupWrap the texture itself.

func (*Sampler) Setup added in v0.5.0

func (s *Sampler) Setup(t *Texture, toDevice Matrix, smooth bool) bool

Setup chooses the mip level of t for drawing it with toDevice, which maps its base pixel space to device space, and reports whether t can be drawn so (toDevice is invertible). Magnified textures are sampled at the nearest pixel unless smooth is set; reduced ones bilinearly from the level at most twice as fine as the device.

func (*Sampler) SetupWrap added in v0.6.0

func (s *Sampler) SetupWrap(t *Texture, toDevice Matrix, smooth bool) bool

SetupWrap is Setup for a texture that repeats in both directions with the period of its base size: texture coordinates are taken modulo W and H before sampling, and bilinear samples at the border read the opposite edge. Its mip levels are periodic too, of the base size over 2^k rounded, so that a level still repeats with a whole number of pixels. Textures of 2^30 pixels or more along an axis cannot be repeated.

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 Shape added in v0.8.0

type Shape struct {
	// MaxBytes bounds the memory of each form a shape makes: its edges,
	// its strips and their row bins. A fill makes one form, a stroke up
	// to two. Zero means DefaultShapeBytes. It takes effect at the next
	// SetFill or SetStroke.
	MaxBytes int
	// contains filtered or unexported fields
}

Shape is a path prepared for drawing under one transform: its device bounds, and its edges or strips binned by bands of rows, so that a canvas drawing it touches only what crosses its clip's rows. The zero value is empty; SetFill and SetStroke (re)build it, keeping storage. After that it may be drawn by several canvases at once. It copies what it needs from the path and the style and keeps no reference to them.

A shape serves a page rendered in bands, one canvas each, every band playing the whole operation list: the transform of the points, flattening, dashing and the stroke outline with its joins and caps are done once, and a band only replays the edges and analytic strips that cross its rows. They are made by the first FillShape that needs them (a stroke has two forms, for the analytic and the outline path, and a draw uses one), so SetFill and SetStroke cost little more than copying the path, and the work is done by the canvases' workers.

Canvas.FillShape draws the bytes Canvas.Fill or Canvas.Stroke draw with the same path, transform and style, except that Stroke replaces curves lying entirely outside its clip by their chords, which moves a curved stroke's analytic rows (by a few levels of coverage) where the clip cuts it. Drawn in bands, a shape stays within 3/255 of the shape drawn whole; Stroke, per band, does not.

The geometry a form records is bounded by MaxBytes, whatever the clip of the canvas that makes it: recording stops at the budget, before the records and their bins are allocated, and every FillShape and FillUnion drawing the truncated form reports ErrEdgeBudget. The bound belongs to the shape, so canvases with different clips and edge budgets draw the same truncated form.

func (*Shape) Bounds added in v0.8.0

func (s *Shape) Bounds() image.Rectangle

Bounds returns the device pixels the shape can touch.

func (*Shape) SetFill added in v0.8.0

func (s *Shape) SetFill(p *Path, m Matrix, rule FillRule) bool

SetFill prepares p, transformed by m into device space, to be filled with rule. It reports false, leaving the shape empty, for a non-finite transform.

func (*Shape) SetStroke added in v0.8.0

func (s *Shape) SetStroke(p *Path, m Matrix, st *StrokeStyle) bool

SetStroke prepares the stroke of p with style st (in user space) under m. It reports false, leaving the shape empty, for a non-finite transform.

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 Texture added in v0.5.0

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

Texture is a Plane and its mip levels, made on demand. It is safe for concurrent use: levels may be requested by several workers at once. The base plane must not change once the texture is made.

func NewTexture added in v0.5.0

func NewTexture(p Plane) *Texture

NewTexture returns a texture of p.

func (*Texture) Base added in v0.5.0

func (t *Texture) Base() *Plane

Base returns the plane of t at full resolution.

func (*Texture) Level added in v0.5.0

func (t *Texture) Level(k int) *Plane

Level returns mip level k (0 is the base), making it if needed.

func (*Texture) Levels added in v0.5.0

func (t *Texture) Levels() int

Levels returns the number of levels below the base: halvings until the plane is one pixel, at most 11.

func (*Texture) MipBytes added in v0.5.0

func (t *Texture) MipBytes() int

MipBytes returns the sample bytes all levels below the base can take once made: those of Level and, for a texture sampled as repeating, the levels of wrapLevel that are not those of Level. It leaves out the base and the transient scratch of downsampling.

type Union added in v0.8.0

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

Union collects fills and strokes that Canvas.FillUnion paints as one shape: their edges are accumulated together and the result is composited once.

Drawn one after another, shapes of one paint that overlap or abut along antialiased edges leave light seams: each composites its own partial coverage over the last, so two edges that together cover a pixel completely give 1 − (1 − a)(1 − b) < 1. In a union their areas are summed in the accumulator instead, and abutting or overlapping edges cover the pixel completely, as one path with several subpaths filled NonZero does.

That works because every element winds the same way. The outlines the Stroker produces wind in one direction everywhere they cover, joins and caps included (the direction depends only on the sign of the transform's determinant, and hairlines, outlined in device space, always wind the same way). A fill is taken with the NonZero rule; a path whose area winds the other way is reversed as a whole, which keeps its holes. Only a fill whose subpaths wind in both directions where they do not overlap (two separate squares, one clockwise, one counter-clockwise) still has parts that cancel where they overlap other elements.

Elements that cannot be summed are drawn on their own after the union, as Canvas.Fill, Canvas.Stroke or Canvas.FillShape draw them: EvenOdd fills, and strokes whose dash pattern is too dense to resolve (drawn at the pattern's mean coverage).

A Union keeps references to the paths, styles and shapes it is given; they must not change until it is drawn. The zero value is empty; Reset empties it, keeping its storage, so that it does not allocate once warm. FillUnion only reads it, so several canvases may draw one Union at once, one band each.

func (*Union) Fill added in v0.8.0

func (u *Union) Fill(p *Path, m Matrix, rule FillRule)

Fill adds p, transformed by m into device space, filled with rule.

func (*Union) Len added in v0.8.0

func (u *Union) Len() int

Len returns the number of elements added since the last Reset.

func (*Union) Reset added in v0.8.0

func (u *Union) Reset()

Reset empties the union, keeping its storage.

func (*Union) Shape added in v0.8.0

func (u *Union) Shape(s *Shape)

Shape adds the shape s, prepared by SetFill or SetStroke. The shape's records are shared with Canvas.FillShape: a union of shapes drawn in bands does the stroker's work once per page.

func (*Union) Stroke added in v0.8.0

func (u *Union) Stroke(p *Path, m Matrix, st *StrokeStyle)

Stroke adds the stroke of p with style st (in user space) under m.

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