ir

package
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Sep 11, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package ir defines figure's intermediate representation: a small, backend-agnostic scene description, plus the Backend interface every renderer implements.

The IR is deliberately thin. It maps cleanly onto an immediate-mode canvas (github.com/gogpu/gg), onto a retained scene graph, and onto the built-in SVG emitter, without any of them leaking into the model layer. Geoms, scales and layout produce IR; they never talk to a backend directly.

Stability

The primitive set and the Backend interface are frozen for the v0.1 cycle. Pre-1.0 they may still change between minor releases; see docs/adr/0002.

Coordinates

Device coordinates, origin at the top-left, X right, Y down — the same convention as SVG, HTML canvas and gg. Positions are float32: they are device-space values after the model layer has already done its float64 work, and float32 is what a GPU backend ultimately wants.

Index

Constants

View Source
const (
	CircleOps = 6
	CirclePts = 13
)

CircleOps and CirclePts are how much Path.Circle appends: one MoveTo, four CubicTo and a Close, and the thirteen points those consume. They are named so that a caller can reserve room for a known number of circles with Path.Grow rather than rediscovering the arithmetic.

Variables

View Source
var Identity = Affine{A: 1, D: 1}

Identity is the transform that changes nothing.

View Source
var Transparent = Color{}

Transparent is the fully transparent colour. A fill or stroke with A == 0 is skipped by every backend.

Functions

func MarkerPath

func MarkerPath(p *Path, m Marker, size float32)

MarkerPath appends the outline of a marker shape to p, centred on the origin and sized so that its nominal extent is size.

It is exported because every backend has to draw the same diamond. A marker that is a slightly different shape in PDF than in SVG is the kind of difference nobody notices until it matters, and a backend written outside this module has no other way to get the shape right. A backend that draws markers itself calls this rather than reading Marker — which is also what keeps a marker added in a later release from silently becoming a circle everywhere but here.

An unknown Marker appends a circle, because a sink for ink cannot refuse.

Types

type Affine

type Affine struct {
	A, B, C, D, E, F float32
}

Affine is a 2D affine transform in row-major order:

| A C E |
| B D F |
| 0 0 1 |

which is the same element order SVG's matrix(a,b,c,d,e,f) uses.

func Rotate

func Rotate(angle float64) Affine

Rotate returns a rotation transform by angle radians, clockwise in screen coordinates (Y down).

func Scale

func Scale(sx, sy float32) Affine

Scale returns a scaling transform.

func Translate

func Translate(dx, dy float32) Affine

Translate returns a translation transform.

func (Affine) Apply

func (a Affine) Apply(p Point) Point

Apply transforms p by a.

func (Affine) IsIdentity

func (a Affine) IsIdentity() bool

IsIdentity reports whether a is the identity transform.

func (Affine) Mul

func (a Affine) Mul(b Affine) Affine

Mul returns a*b: the transform that applies b first, then a.

type Backend

type Backend interface {
	// Polyline strokes an open sequence of points. It is the fast path for
	// line geoms; it is exactly equivalent to StrokePath of a path built with
	// Path.Polyline, but lets a backend skip path construction entirely.
	Polyline(pts []Point, style Stroke)

	// StrokePath strokes an arbitrary path.
	StrokePath(p *Path, style Stroke)

	// FillPath fills a path's interior according to rule.
	FillPath(p *Path, fill Fill, rule FillRule)

	// Text draws a single-line run. The backend shapes it.
	Text(run TextRun)

	// Markers draws one shape at each of the given positions. This is the
	// scatter fast path: a backend may instance it.
	Markers(shape Marker, at []Point, style MarkerStyle)

	// Image blits a raster into dst, scaling to fit. This carries the
	// density-raster big-data path.
	Image(img image.Image, dst Rect)

	// Push pushes a transform and an optional clip path onto the state stack.
	// The transform composes with whatever is already on the stack; a nil clip
	// means "no additional clipping".
	Push(clip *Path, xform Affine)

	// Pop undoes the most recent Push.
	Pop()

	// Measure reports metrics for a run, from the same font stack that Text
	// will draw with. It must be callable before any drawing, because layout
	// runs first.
	Measure(run TextRun) TextMetrics

	// Flush completes the frame and reports any deferred error. A backend that
	// writes a file writes it here.
	Flush() error
}

Backend is what every renderer implements. It is an immediate-mode drawing sink: figure lowers a chart into calls on a Backend, in paint order.

Stability

This interface has not changed since v0.1 (docs/adr/0002) and does not change from v1: it is implemented outside this module, so it never gains a method. What a backend can additionally do is an optional interface beside it — Partial, Resizer, Semantics are the three that exist — and figure asks with a type assertion and does without when the answer is no. It exists so that figure is insulated from any single rendering library: a change in a backend's own API is contained to that backend's adapter.

State

A Backend carries no style state between calls — every drawing call is self-contained. The only state is the transform/clip stack managed by Push and Pop.

Ownership

Everything a drawing call is given — a point slice, a Path, an image.Image — is **lent for the duration of that call**. figure draws from pooled buffers so that a chart redrawn every frame allocates nothing that grows with its data, which means the next call may write over what the last one was handed. A Backend that needs to keep any of it must copy it; Recorder is the worked example.

Concurrency

A Backend is not safe for concurrent use. figure may build IR on several goroutines but always plays it back into a backend from one.

type Color

type Color = color.NRGBA

Color is figure's colour type: 8-bit non-premultiplied sRGBA.

It is an alias for color.NRGBA rather than a new type so that colours pass straight into any stdlib or backend API taking a color.Color, with no conversion layer and no import of figure in code that only wants to name a colour.

func Fade

func Fade(c Color, f float64) Color

Fade returns c with its alpha scaled by f, clamped to [0, 1].

func RGB

func RGB(r, g, b uint8) Color

RGB returns an opaque colour.

func RGBA

func RGBA(r, g, b, a uint8) Color

RGBA returns a colour with explicit alpha.

type Description

type Description struct {
	Title  string
	Detail string
}

Description is what a chart says about itself in words.

It carries no geometry and changes nothing about what is drawn. It is what a backend needs in order to be readable by something other than an eye: an SVG's <title> and <desc>, a PDF's document title, a canvas element's aria-label, a window's title bar.

Title is a short label — a line of text, the chart's own title where it has one. Detail is the longer reading of the chart: what it plots, over what range, and how much of it there is. Either may be empty, and a backend writes nothing for an empty one rather than an empty element.

func (Description) Empty

func (d Description) Empty() bool

Empty reports whether d says nothing.

type Fill

type Fill struct {
	Color Color
	Start Point
	End   Point
	Stops []GradientStop
}

Fill describes how a path's interior is painted.

A Fill with no Stops is a solid Color. With Stops, it is a linear gradient running from Start to End in the same coordinate space as the path; Color is then ignored. Radial and sweep gradients are not in the v0.1 IR — no v0.1 geom needs them, and adding them later is additive.

func Solid

func Solid(c Color) Fill

Solid returns an opaque-rules solid fill of colour c.

func (Fill) IsGradient

func (f Fill) IsGradient() bool

IsGradient reports whether f paints a gradient rather than a solid colour.

func (Fill) Visible

func (f Fill) Visible() bool

Visible reports whether filling with f would put any ink on the canvas.

type FillRule

type FillRule uint8

FillRule selects how a path's interior is determined.

const (
	NonZero FillRule = iota
	EvenOdd
)

The fill rules.

type FontRef

type FontRef struct {
	Family string  // logical family; "" means the backend's default sans
	Size   float64 // em size in device units
	Weight int     // CSS-style numeric weight; 0 means 400
	Italic bool
}

FontRef names a font logically. figure never carries glyph data or font files through the IR: a backend resolves a FontRef with whatever font stack it has (gg's shaper, the SVG viewer, a host framework's text engine).

type GradientStop

type GradientStop struct {
	Offset float32
	Color  Color
}

GradientStop is one colour stop of a linear gradient, at offset t in [0, 1] along the gradient axis.

type HAlign

type HAlign uint8

HAlign is horizontal alignment of a text run relative to its anchor point.

const (
	AlignStart HAlign = iota
	AlignCenter
	AlignEnd
)

The horizontal alignments.

type LineCap

type LineCap uint8

LineCap is how a stroke terminates.

const (
	CapButt LineCap = iota
	CapRound
	CapSquare
)

The line caps.

type LineJoin

type LineJoin uint8

LineJoin is how two stroke segments meet.

const (
	JoinMiter LineJoin = iota
	JoinRound
	JoinBevel
)

The line joins.

type Marker

type Marker uint8

Marker is a scatter-point shape, drawn centred on its position.

The set grows at the end, so a backend must not assume it is complete: MarkerPath appends the outline of any Marker and appends a circle for one it does not know, which is what keeps a shape added in a later release from being drawn differently — or not at all — by a backend written before it.

const (
	MarkerCircle Marker = iota
	MarkerSquare
	MarkerDiamond
	MarkerTriangle
	MarkerCross
	MarkerPlus
)

The marker shapes.

type MarkerStyle

type MarkerStyle struct {
	Size   float32 // nominal diameter in device units
	Fill   Color
	Stroke Stroke
}

MarkerStyle is the paint applied to every instance of a Markers call.

type Measurer

type Measurer interface {
	// Measure reports metrics for a run. See [Backend.Measure].
	Measure(run TextRun) TextMetrics
}

Measurer is the part of a Backend that is needed before anything is drawn.

Layout runs first and needs to know how wide a tick label will be, so text measurement is separable from drawing — and separating it is what lets a recorder stand in for a backend without being able to draw at all.

type Partial

type Partial interface {
	// Damage limits the next frame to the given rectangles. An empty list
	// means nothing changed and the frame can be skipped entirely; the
	// limitation lasts until the next Flush.
	Damage(rects []Rect)
}

Partial is implemented by a backend that can repaint part of a frame rather than all of it.

It is the seam damage tracking needs and the only one: figure works out *where* a frame changed, and a backend that can act on that says so by implementing this. A backend that cannot — a file emitter, which writes a whole document or none — simply does not, and gets a full repaint like always.

The rectangles are in device space, the same coordinates a drawing call takes. A backend is free to widen them (to whole pixels, to tiles) and must not narrow them.

type Path

type Path struct {
	Ops []PathOp
	Pts []Point
}

Path is a sequence of subpaths built from MoveTo/LineTo/CubicTo/Close.

Ops and Pts are parallel: walking Ops and consuming Op.Points() entries from Pts per op reconstructs the path. Splitting them this way keeps a path two allocations regardless of length and lets callers reuse both slices via Reset.

func (*Path) AsRect added in v0.9.0

func (p *Path) AsRect() (Rect, bool)

AsRect returns the rectangle p draws, and whether it draws exactly one.

It is the inverse of Path.Rect, and it exists for clipping. A clip is almost always a rectangle — a panel, a facet cell, one view of a scene — but it reaches a backend as a path, and a backend that cannot tell the two apart rasterises a coverage mask and then consults it on every drawing call inside the clip. That costs a figure its drawing calls times its area instead of its drawing calls, which is invisible at a hundred calls and ruinous at a thousand. Recognising the shape here rather than in each backend keeps one definition of what "this path is a rectangle" means.

A single closed subpath of four points qualifies when it walks the four distinct corners of its own bounding box, each once, along edges rather than diagonals; both windings and any starting corner are accepted, and a box with no area is not a rectangle.

The corner test is the part that is easy to leave out and it is the part that matters. Axis-aligned edges alone are not enough: (0,0), (1,0), (0,0), (0,1) runs along an axis four times, doubles back on itself, encloses nothing — and has the unit square for a bounding box. Reporting that box would hand a clip *more* room than the path allows, which is the one direction a clip must never be wrong in.

func (*Path) Bounds

func (p *Path) Bounds() Rect

Bounds returns the control-point bounding box of p. For paths containing cubics this is conservative: it bounds the control polygon, not the exact curve. That is what layout and clip-culling need, and it is cheap.

func (*Path) Circle

func (p *Path) Circle(c Point, r float32) *Path

Circle appends a closed circular subpath centred on c.

Four cubics, because OpCubicTo is the only curve the IR has and ADR 0002 froze it there on the claim that every curve a chart needs is expressible as one. The control points sit kappa·r along the tangents, where kappa is 4(√2-1)/3 — the constant that makes the maximum radial error about one part in ten thousand, which is a hundredth of a pixel on a circle the width of a plot.

A radius of zero or less appends nothing: a mark with no area is a mark that is not drawn, which is what a size channel's smallest value asks for.

func (*Path) Close

func (p *Path) Close() *Path

Close closes the current subpath.

func (*Path) CubicTo

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

CubicTo appends a cubic Bézier segment with the given control points.

func (*Path) Empty

func (p *Path) Empty() bool

Empty reports whether p contains no ops.

func (*Path) Grow

func (p *Path) Grow(ops, pts int) *Path

Grow reserves room for ops verbs and pts points, keeping whatever the path already holds.

It exists for the one caller that knows in advance how much it will append: a layer emitting a shape per row. Appending a hundred thousand circles into an empty path walks the doubling ladder twenty times, and a path held in a pool that a garbage collection has emptied walks it again on the next frame — so a chart redrawn over a large table pays a cost that is logarithmic in its rows rather than none. Reserving once makes it two allocations, and none at all once the buffers are warm.

func (*Path) LineTo

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

LineTo appends a straight segment to (x, y).

func (*Path) MoveTo

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

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

func (*Path) Polyline

func (p *Path) Polyline(pts []Point) *Path

Polyline appends pts as one open subpath. It is a no-op for fewer than two points.

func (*Path) Rect

func (p *Path) Rect(r Rect) *Path

Rect appends a closed rectangular subpath.

func (*Path) Reset

func (p *Path) Reset()

Reset clears p while keeping its capacity, so a caller can reuse it across frames without reallocating.

func (*Path) Walk

func (p *Path) Walk(fn func(op PathOp, pts []Point))

Walk calls fn for each op with the points that op consumes. The slice passed to fn aliases p.Pts and must not be retained.

type PathOp

type PathOp uint8

PathOp is a single path-construction verb.

const (
	OpMoveTo  PathOp = iota // 1 point
	OpLineTo                // 1 point
	OpCubicTo               // 3 points: control 1, control 2, end
	OpClose                 // 0 points
)

The path verbs. The set is deliberately minimal: every curve a chart needs (rounded corners, tension-smoothed lines, arcs) is expressible as cubics, and both SVG and gg consume cubics natively.

func (PathOp) Points

func (op PathOp) Points() int

Points reports how many points in Path.Pts the op consumes.

type Point

type Point struct {
	X, Y float32
}

Point is a position in device space.

The concept document calls this f32.Point; figure keeps it here rather than spending a package on two fields (see docs/adr/0002).

type Recorder

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

Recorder is a Backend that stores what was drawn on it so that it can be replayed into another Backend later.

It is how figure draws several panels at once. A Backend is not safe for concurrent use, so each goroutine draws into a Recorder of its own and the recordings are replayed into the real backend afterwards, in panel order. Replaying in a fixed order is what makes a parallel render produce exactly what a serial one does rather than merely something equivalent.

Points and path data go into flat arenas rather than one slice per call, so a Recorder that is Recorder.Reset and reused costs nothing per frame after the first.

A Recorder is not safe for concurrent use either — one per goroutine is the whole idea. Measure is forwarded to the Measurer it was built with, which therefore must be safe to call from every goroutine using a Recorder.

func NewRecorder

func NewRecorder(m Measurer) *Recorder

NewRecorder returns a Recorder measuring through m. m may be nil, in which case Measure reports zero — which is correct for a recorder used only for a data pass, since layout has already run by then.

func (*Recorder) Bounds

func (r *Recorder) Bounds() Rect

Bounds reports the rectangle enclosing everything a recording drew. It is what a caller repaints when Damage says the two frames are not comparable.

func (*Recorder) Calls

func (r *Recorder) Calls() int

Calls reports how many drawing operations were recorded.

func (*Recorder) Describe

func (r *Recorder) Describe(d Description)

Describe implements Semantics: a recording carries what the chart says about itself, so that a description survives being recorded and replayed.

Without it a chart drawn through a Recorder — which is every panel of a parallel render and every frame of a [Live] one — would lose its title and its description on the way to the backend, and an interactive chart exported to SVG would be the one with no accessible name.

func (*Recorder) Description

func (r *Recorder) Description() Description

Description reports what was recorded by Recorder.Describe.

func (*Recorder) Empty

func (r *Recorder) Empty() bool

Empty reports whether nothing has been recorded.

func (*Recorder) FillPath

func (r *Recorder) FillPath(p *Path, fill Fill, rule FillRule)

FillPath records a filled path. See Backend.FillPath.

func (*Recorder) Flush

func (r *Recorder) Flush() error

Flush completes nothing: a Recorder holds a recording rather than a frame. The recording is finished by Recorder.Replay.

func (*Recorder) Image

func (r *Recorder) Image(img image.Image, dst Rect)

Image records an image, copying its pixels. See Backend.Image.

func (*Recorder) Markers

func (r *Recorder) Markers(shape Marker, at []Point, style MarkerStyle)

Markers records a set of markers. See Backend.Markers.

func (*Recorder) Measure

func (r *Recorder) Measure(run TextRun) TextMetrics

Measure forwards to the Measurer the Recorder was built with.

func (*Recorder) Polyline

func (r *Recorder) Polyline(pts []Point, style Stroke)

Polyline records a stroked polyline. See Backend.Polyline.

func (*Recorder) Pop

func (r *Recorder) Pop()

Pop records the end of a Push. See Backend.Pop.

func (*Recorder) Push

func (r *Recorder) Push(clip *Path, xform Affine)

Push records a transform and clip. See Backend.Push.

func (*Recorder) Replay

func (r *Recorder) Replay(b Backend)

Replay makes the recorded calls on b, in the order they were made.

A description recorded along the way is announced first, before any drawing, which is the order Semantics promises a backend.

func (*Recorder) Reset

func (r *Recorder) Reset()

Reset empties the recording, keeping the memory for the next one.

func (*Recorder) SetMeasurer

func (r *Recorder) SetMeasurer(m Measurer)

SetMeasurer points the Recorder at a different Measurer. It exists so that a pooled Recorder can serve one render after another, each with a backend of its own.

func (*Recorder) StrokePath

func (r *Recorder) StrokePath(p *Path, style Stroke)

StrokePath records a stroked path. See Backend.StrokePath.

func (*Recorder) Text

func (r *Recorder) Text(run TextRun)

Text records a text run. See Backend.Text.

type Rect

type Rect struct {
	Min, Max Point
}

Rect is an axis-aligned rectangle in device space. Min is the top-left corner, Max the bottom-right; a Rect with Max below Min on either axis is empty.

func Damage

func Damage(prev, next *Recorder, dst []Rect) (rects []Rect, ok bool)

Damage reports where two recordings differ, in device space.

ok is false when the two are not comparable call for call — a different number of drawing calls, a different kind at the same index, a transform or a clip that moved. That is a chart whose *structure* changed rather than its data, and the honest answer is a full repaint rather than a list of rectangles that describes half of it.

ok is true with an empty list when the two recordings are identical, which is a frame a caller can skip.

Rectangles are appended to dst, which may be nil. Reusing one across frames is what keeps a redraw allocation-free.

func R

func R(x0, y0, x1, y1 float32) Rect

R builds a Rect from its edges.

func (Rect) Contains

func (r Rect) Contains(p Point) bool

Contains reports whether p lies inside r, edges included.

func (Rect) Dx

func (r Rect) Dx() float32

Dx returns the width of r.

func (Rect) Dy

func (r Rect) Dy() float32

Dy returns the height of r.

func (Rect) Empty

func (r Rect) Empty() bool

Empty reports whether r encloses no area.

func (Rect) Inset

func (r Rect) Inset(left, top, right, bottom float32) Rect

Inset returns r shrunk by the given amounts on each side.

type Resizer

type Resizer interface {
	// Resize sets the surface the backend draws into. It means what
	// [Target.Open]'s argument does, and the backend keeps whatever it was
	// drawing into where that is possible. It takes effect on the next frame.
	Resize(s Surface) error
}

Resizer is an optional Backend interface: a backend drawing into a surface whose size can change while it is open implements it.

A file backend never needs it — a document is opened at one size and written once — and a surface that stays open does: a window is dragged wider, a canvas element reflows, a terminal is resized. Rather than closing the target and opening another, which would lose the frame on screen and every zoom in the scales, the caller tells the backend its new size and draws again.

It is optional for the reason every interface in this package is: adding Resize to Backend would break every third-party backend that has no surface to resize.

type Semantics

type Semantics interface {
	// Describe attaches a description to the frame about to be drawn.
	Describe(d Description)
}

Semantics is an optional Backend interface: a backend whose output can carry a description of what it draws implements it.

figure calls Describe once, before any drawing call, so that a backend writing a document header has the description in hand when it writes one. A backend that has nowhere to put words — a raster, which is pixels and nothing else — does not implement it, and the description is simply not written.

type Stroke

type Stroke struct {
	Color      Color
	Width      float32
	Cap        LineCap
	Join       LineJoin
	MiterLimit float32   // 0 means the backend default (4)
	Dash       []float32 // nil or empty means solid
	DashOffset float32
}

Stroke is the full state needed to stroke a polyline or path.

func (Stroke) Visible

func (s Stroke) Visible() bool

Visible reports whether stroking with s would put any ink on the canvas.

type Surface

type Surface struct {
	// WidthPx and HeightPx are the surface's size in device pixels.
	WidthPx, HeightPx int
	// DPR is the device pixel ratio. Coordinates handed to a backend are
	// already in device pixels, so it is informational — backends use it to
	// pick hinting and stroke-snapping strategies.
	DPR float64
}

Surface describes what a Target opens and a Resizer resizes: how much room there is to draw in, and at what device scale.

It is a struct rather than three parameters because both methods are implemented outside this module and so never gain one, and because what a surface *is* has more to say than its size: a colour space, a background, a pixel format, whether it is opaque. None of those are here yet, and every one of them is a field when it arrives rather than a second Target interface. ADR 0060 is the record.

type Target

type Target interface {
	// Open returns a Backend drawing into the surface described by s.
	Open(s Surface) (Backend, error)

	// Close finalises the destination. It is called after the Backend's Flush.
	Close() error
}

Target is a render destination: it opens a Backend sized for one chart, and finalises whatever it is writing to when Close is called.

Splitting Target from Backend is what lets figure.Render take a destination before the chart's pixel size is known to that destination, and lets the same backend implementation serve a file, an io.Writer, or a window surface.

Like Backend, it is implemented outside this module and never gains a method.

type TextMetrics

type TextMetrics struct {
	// Advance is the total advance width of the run.
	Advance float32
	// Ascent and Descent are the font's ascent above and descent below the
	// baseline, both positive. They describe the font box, not this run's ink,
	// so successive runs in one font line up.
	Ascent  float32
	Descent float32
	// Ink is the run's tight bounding box relative to an origin at the
	// baseline start, Y up-negative as usual for screen coordinates.
	Ink Rect
}

TextMetrics is what layout needs back from a backend about a run.

Measure is the only text capability figure requires beyond drawing: layout must size margins, space ticks, and detect label collisions using metrics from the very shaper that will draw the text.

func (TextMetrics) Height

func (m TextMetrics) Height() float32

Height returns the font box height, Ascent + Descent.

type TextRun

type TextRun struct {
	Text     string
	Font     FontRef
	At       Point   // anchor position in device space
	H        HAlign  // horizontal alignment about At
	V        VAlign  // vertical alignment about At
	Rotation float64 // radians, clockwise, about At
	Color    Color
}

TextRun is a single-line string to be drawn.

figure passes strings, never pre-shaped glyphs: shaping belongs to the active backend. figure owns only placement — anchoring, rotation, and overlap avoidance. Paragraph layout is out of scope (see CONCEPT.md §5).

type VAlign

type VAlign uint8

VAlign is vertical alignment of a text run relative to its anchor point.

const (
	AlignBaseline VAlign = iota
	AlignTop
	AlignMiddle
	AlignBottom
)

The vertical alignments. AlignBaseline puts the anchor on the text baseline; the others align the anchor to the run's font box.

Jump to

Keyboard shortcuts

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