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
- Variables
- func MarkerPath(p *Path, m Marker, size float32)
- type Affine
- type Backend
- type Color
- type Description
- type Fill
- type FillRule
- type FontRef
- type GradientStop
- type HAlign
- type LineCap
- type LineJoin
- type Marker
- type MarkerStyle
- type Measurer
- type Partial
- type Path
- func (p *Path) AsRect() (Rect, bool)
- func (p *Path) Bounds() Rect
- func (p *Path) Circle(c Point, r float32) *Path
- func (p *Path) Close() *Path
- func (p *Path) CubicTo(c1x, c1y, c2x, c2y, x, y float32) *Path
- func (p *Path) Empty() bool
- func (p *Path) Grow(ops, pts int) *Path
- func (p *Path) LineTo(x, y float32) *Path
- func (p *Path) MoveTo(x, y float32) *Path
- func (p *Path) Polyline(pts []Point) *Path
- func (p *Path) Rect(r Rect) *Path
- func (p *Path) Reset()
- func (p *Path) Walk(fn func(op PathOp, pts []Point))
- type PathOp
- type Point
- type Recorder
- func (r *Recorder) Bounds() Rect
- func (r *Recorder) Calls() int
- func (r *Recorder) Describe(d Description)
- func (r *Recorder) Description() Description
- func (r *Recorder) Empty() bool
- func (r *Recorder) FillPath(p *Path, fill Fill, rule FillRule)
- func (r *Recorder) Flush() error
- func (r *Recorder) Image(img image.Image, dst Rect)
- func (r *Recorder) Markers(shape Marker, at []Point, style MarkerStyle)
- func (r *Recorder) Measure(run TextRun) TextMetrics
- func (r *Recorder) Polyline(pts []Point, style Stroke)
- func (r *Recorder) Pop()
- func (r *Recorder) Push(clip *Path, xform Affine)
- func (r *Recorder) Replay(b Backend)
- func (r *Recorder) Reset()
- func (r *Recorder) SetMeasurer(m Measurer)
- func (r *Recorder) StrokePath(p *Path, style Stroke)
- func (r *Recorder) Text(run TextRun)
- type Rect
- type Resizer
- type Semantics
- type Stroke
- type Surface
- type Target
- type TextMetrics
- type TextRun
- type VAlign
Constants ¶
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 ¶
var Identity = Affine{A: 1, D: 1}
Identity is the transform that changes nothing.
var Transparent = Color{}
Transparent is the fully transparent colour. A fill or stroke with A == 0 is skipped by every backend.
Functions ¶
func MarkerPath ¶
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 ¶
Rotate returns a rotation transform by angle radians, clockwise in screen coordinates (Y down).
func (Affine) IsIdentity ¶
IsIdentity reports whether a is the identity transform.
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 ¶
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.
type Description ¶
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.
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 (Fill) IsGradient ¶
IsGradient reports whether f paints a gradient rather than a solid colour.
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 ¶
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.
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.
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 ¶
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
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 ¶
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 ¶
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) Grow ¶
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) Polyline ¶
Polyline appends pts as one open subpath. It is a no-op for fewer than two points.
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.
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 ¶
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 ¶
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) 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) FillPath ¶
FillPath records a filled path. See Backend.FillPath.
func (*Recorder) Flush ¶
Flush completes nothing: a Recorder holds a recording rather than a frame. The recording is finished by Recorder.Replay.
func (*Recorder) Image ¶
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 ¶
Polyline records a stroked polyline. See Backend.Polyline.
func (*Recorder) Push ¶
Push records a transform and clip. See Backend.Push.
func (*Recorder) Replay ¶
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 ¶
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 ¶
StrokePath records a stroked path. See Backend.StrokePath.
func (*Recorder) Text ¶
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 ¶
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.
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.
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).