recording/recorder.go

Functions Structs

Functions

func AntiAlias

AntiAlias returns whether anti-aliasing is enabled.

func (r *Recorder) AntiAlias() bool {
	return r.antiAlias
}

func Clear

Clear resets the entire canvas to transparent (zero alpha),

matching [gg.Context.Clear] semantics. To fill with a specific

background color, use [Recorder.ClearWithColor].

func (r *Recorder) Clear() {
	r.ClearWithColor(gg.Transparent)
}

func ClearDash

ClearDash removes the dash pattern, returning to solid lines.

func (r *Recorder) ClearDash() {
	r.dashPattern = nil
	r.dashOffset = 0
	r.commands = append(r.commands, SetDashCommand{Pattern: nil, Offset: 0})
}

func ClearPath

ClearPath clears the current path.

func (r *Recorder) ClearPath() {
	r.currentPath.Clear()
}

func ClearWithColor

ClearWithColor fills the entire canvas with the specified color.

This is the recommended way to set a background color before drawing.

func (r *Recorder) ClearWithColor(c gg.RGBA) {
	oldBrush := r.fillBrush
	r.fillBrush = NewSolidBrush(c)
	r.FillRectangle(0, 0, float64(r.width), float64(r.height))
	r.fillBrush = oldBrush
}

func Clip

Clip sets the current path as the clipping region and clears the path.

func (r *Recorder) Clip() {
	if r.currentPath.NumVerbs() == 0 {
		return
	}

	pathRef := r.resources.AddPath(r.currentPath)
	r.commands = append(r.commands, SetClipCommand{
		Path:	pathRef,
		Rule:	r.fillRule,
	})

	r.currentPath = gg.NewPath()
}

func ClipPreserve

ClipPreserve sets the current path as the clipping region but keeps the path.

func (r *Recorder) ClipPreserve() {
	if r.currentPath.NumVerbs() == 0 {
		return
	}

	pathRef := r.resources.AddPath(r.currentPath)
	r.commands = append(r.commands, SetClipCommand{
		Path:	pathRef,
		Rule:	r.fillRule,
	})
}

func ClipRoundRect

ClipRoundRect sets a rounded rectangle as the clipping region.

The rectangle is defined in user-space coordinates with uniform corner radius.

func (r *Recorder) ClipRoundRect(x, y, w, h, radius float64) {
	r.commands = append(r.commands, ClipRoundRectCommand{
		X:	x, Y: y, W: w, H: h, Radius: radius,
	})
}

func ClosePath

ClosePath closes the current subpath.

func (r *Recorder) ClosePath() {
	r.currentPath.Close()
}

func Commands

Commands returns the recorded commands.

func (r *Recording) Commands() []Command {
	return r.commands
}

func CubicTo

CubicTo adds a cubic Bezier curve to the current path.

func (r *Recorder) CubicTo(c1x, c1y, c2x, c2y, x, y float64) {
	cp1x, cp1y := r.transform.TransformPoint(c1x, c1y)
	cp2x, cp2y := r.transform.TransformPoint(c2x, c2y)
	px, py := r.transform.TransformPoint(x, y)
	r.currentPath.CubicTo(cp1x, cp1y, cp2x, cp2y, px, py)
}

func DrawArc

DrawArc draws a circular arc.

func (r *Recorder) DrawArc(x, y, radius, angle1, angle2 float64) {
	r.drawArcPath(x, y, radius, angle1, angle2)
}

func DrawCircle

DrawCircle draws a circle.

func (r *Recorder) DrawCircle(x, y, radius float64) {
	const k = 0.5522847498307936	// 4/3 * (sqrt(2) - 1)
	offset := radius * k

	r.MoveTo(x+radius, y)
	r.CubicTo(x+radius, y+offset, x+offset, y+radius, x, y+radius)
	r.CubicTo(x-offset, y+radius, x-radius, y+offset, x-radius, y)
	r.CubicTo(x-radius, y-offset, x-offset, y-radius, x, y-radius)
	r.CubicTo(x+offset, y-radius, x+radius, y-offset, x+radius, y)
	r.ClosePath()
}

func DrawEllipse

DrawEllipse draws an ellipse.

func (r *Recorder) DrawEllipse(x, y, rx, ry float64) {
	const k = 0.5522847498307936
	ox := rx * k
	oy := ry * k

	r.MoveTo(x+rx, y)
	r.CubicTo(x+rx, y+oy, x+ox, y+ry, x, y+ry)
	r.CubicTo(x-ox, y+ry, x-rx, y+oy, x-rx, y)
	r.CubicTo(x-rx, y-oy, x-ox, y-ry, x, y-ry)
	r.CubicTo(x+ox, y-ry, x+rx, y-oy, x+rx, y)
	r.ClosePath()
}

func DrawEllipticalArc

DrawEllipticalArc draws an elliptical arc.

func (r *Recorder) DrawEllipticalArc(x, y, rx, ry, angle1, angle2 float64) {
	r.Save()
	r.Translate(x, y)
	r.Scale(rx, ry)
	r.DrawArc(0, 0, 1, angle1, angle2)
	// Restore state but keep path changes
	if len(r.stateStack) > 0 {
		state := r.stateStack[len(r.stateStack)-1]
		r.stateStack = r.stateStack[:len(r.stateStack)-1]
		r.fillBrush = state.fillBrush
		r.strokeBrush = state.strokeBrush
		r.lineWidth = state.lineWidth
		r.lineCap = state.lineCap
		r.lineJoin = state.lineJoin
		r.miterLimit = state.miterLimit
		r.dashPattern = state.dashPattern
		r.dashOffset = state.dashOffset
		r.fillRule = state.fillRule
		r.transform = state.transform
	}
	r.commands = append(r.commands, RestoreCommand{})
}

func DrawImage

DrawImage draws an image at the specified position.

func (r *Recorder) DrawImage(img image.Image, x, y int) {
	if img == nil {
		return
	}

	bounds := img.Bounds()
	w := bounds.Dx()
	h := bounds.Dy()

	imageRef := r.resources.AddImage(img)

	// Transform destination rectangle
	x1, y1 := r.transform.TransformPoint(float64(x), float64(y))
	x2, y2 := r.transform.TransformPoint(float64(x+w), float64(y+h))

	srcRect := NewRect(0, 0, float64(w), float64(h))
	dstRect := NewRectFromPoints(x1, y1, x2, y2)

	r.commands = append(r.commands, DrawImageCommand{
		Image:		imageRef,
		SrcRect:	srcRect,
		DstRect:	dstRect,
		Options:	DefaultImageOptions(),
	})
}

func DrawImageAnchored

DrawImageAnchored draws an image with an anchor point.

func (r *Recorder) DrawImageAnchored(img image.Image, x, y int, ax, ay float64) {
	if img == nil {
		return
	}

	bounds := img.Bounds()
	w := bounds.Dx()
	h := bounds.Dy()

	// Adjust position based on anchor
	drawX := float64(x) - float64(w)*ax
	drawY := float64(y) - float64(h)*ay

	imageRef := r.resources.AddImage(img)

	// Transform destination rectangle
	x1, y1 := r.transform.TransformPoint(drawX, drawY)
	x2, y2 := r.transform.TransformPoint(drawX+float64(w), drawY+float64(h))

	srcRect := NewRect(0, 0, float64(w), float64(h))
	dstRect := NewRectFromPoints(x1, y1, x2, y2)

	r.commands = append(r.commands, DrawImageCommand{
		Image:		imageRef,
		SrcRect:	srcRect,
		DstRect:	dstRect,
		Options:	DefaultImageOptions(),
	})
}

func DrawImageScaled

DrawImageScaled draws an image scaled to fit the specified rectangle.

func (r *Recorder) DrawImageScaled(img image.Image, x, y, w, h float64) {
	if img == nil {
		return
	}

	bounds := img.Bounds()
	srcW := bounds.Dx()
	srcH := bounds.Dy()

	imageRef := r.resources.AddImage(img)

	// Transform destination rectangle
	x1, y1 := r.transform.TransformPoint(x, y)
	x2, y2 := r.transform.TransformPoint(x+w, y+h)

	srcRect := NewRect(0, 0, float64(srcW), float64(srcH))
	dstRect := NewRectFromPoints(x1, y1, x2, y2)

	r.commands = append(r.commands, DrawImageCommand{
		Image:		imageRef,
		SrcRect:	srcRect,
		DstRect:	dstRect,
		Options:	DefaultImageOptions(),
	})
}

func DrawLine

DrawLine draws a line between two points.

func (r *Recorder) DrawLine(x1, y1, x2, y2 float64) {
	r.MoveTo(x1, y1)
	r.LineTo(x2, y2)
}

func DrawPoint

DrawPoint draws a single point at the given coordinates.

func (r *Recorder) DrawPoint(x, y, radius float64) {
	r.DrawCircle(x, y, radius)
}

func DrawRectangle

DrawRectangle draws a rectangle.

func (r *Recorder) DrawRectangle(x, y, w, h float64) {
	r.MoveTo(x, y)
	r.LineTo(x+w, y)
	r.LineTo(x+w, y+h)
	r.LineTo(x, y+h)
	r.ClosePath()
}

func DrawRoundedRectangle

DrawRoundedRectangle draws a rectangle with rounded corners.

func (r *Recorder) DrawRoundedRectangle(x, y, w, h, radius float64) {
	// Clamp radius to half of the smaller dimension
	maxR := math.Min(w, h) / 2
	if radius > maxR {
		radius = maxR
	}

	r.MoveTo(x+radius, y)
	r.LineTo(x+w-radius, y)
	r.drawArcPath(x+w-radius, y+radius, radius, -math.Pi/2, 0)
	r.LineTo(x+w, y+h-radius)
	r.drawArcPath(x+w-radius, y+h-radius, radius, 0, math.Pi/2)
	r.LineTo(x+radius, y+h)
	r.drawArcPath(x+radius, y+h-radius, radius, math.Pi/2, math.Pi)
	r.LineTo(x, y+radius)
	r.drawArcPath(x+radius, y+radius, radius, math.Pi, 3*math.Pi/2)
	r.ClosePath()
}

func DrawString

DrawString draws text at position (x, y) where y is the baseline.

func (r *Recorder) DrawString(s string, x, y float64) {
	// Transform position
	px, py := r.transform.TransformPoint(x, y)

	brushRef := r.resources.AddBrush(r.fillBrush)

	r.commands = append(r.commands, DrawTextCommand{
		Text:		s,
		X:		px,
		Y:		py,
		FontSize:	r.fontSize,
		FontFamily:	r.fontFamily,
		Brush:		brushRef,
	})
}

func DrawStringAnchored

DrawStringAnchored draws text with an anchor point.

The anchor point is specified by ax and ay, which are in the range [0, 1].

func (r *Recorder) DrawStringAnchored(s string, x, y, ax, ay float64) {
	// For recording, we store the base position and let the backend handle anchoring
	// This is a simplification; a full implementation would measure text
	r.DrawString(s, x, y)
}

func DrawStringWrapped

DrawStringWrapped wraps text to the given width and draws it with alignment.

Each wrapped line is recorded as a separate DrawText command.

func (r *Recorder) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align text.Alignment) {
	lines := r.WordWrap(s, width)
	if len(lines) == 0 {
		return
	}

	var fh float64
	if r.fontFace != nil {
		fh = r.fontFace.Metrics().LineHeight()
	} else {
		fh = r.fontSize * 1.2
	}

	// Total height (same formula as MeasureMultilineString)
	n := float64(len(lines))
	h := n*fh*lineSpacing - (lineSpacing-1)*fh

	// Adjust starting position by anchor
	x -= ax * width
	y -= ay * h

	// Adjust x base for alignment
	switch align {
	case text.AlignCenter:
		x += width / 2
	case text.AlignRight:
		x += width
	}

	for _, line := range lines {
		drawX := x
		switch align {
		case text.AlignCenter:
			lw, _ := r.MeasureString(line)
			drawX = x - lw/2
		case text.AlignRight:
			lw, _ := r.MeasureString(line)
			drawX = x - lw
		}
		r.DrawString(line, drawX, y)
		y += fh * lineSpacing
	}
}

func Fill

Fill fills the current path and clears it.

func (r *Recorder) Fill() {
	if r.currentPath.NumVerbs() == 0 {
		return
	}

	pathRef := r.resources.AddPath(r.currentPath)
	brushRef := r.resources.AddBrush(r.fillBrush)

	r.commands = append(r.commands, FillPathCommand{
		Path:	pathRef,
		Brush:	brushRef,
		Rule:	r.fillRule,
	})

	r.currentPath = gg.NewPath()
}

func FillPreserve

FillPreserve fills the current path without clearing it.

func (r *Recorder) FillPreserve() {
	if r.currentPath.NumVerbs() == 0 {
		return
	}

	pathRef := r.resources.AddPath(r.currentPath)
	brushRef := r.resources.AddBrush(r.fillBrush)

	r.commands = append(r.commands, FillPathCommand{
		Path:	pathRef,
		Brush:	brushRef,
		Rule:	r.fillRule,
	})
}

func FillRectangle

FillRectangle fills a rectangle without adding it to the path.

This is an optimized operation for the common case of axis-aligned rectangles.

func (r *Recorder) FillRectangle(x, y, w, h float64) {
	// Transform corners
	x1, y1 := r.transform.TransformPoint(x, y)
	x2, y2 := r.transform.TransformPoint(x+w, y+h)

	rect := NewRectFromPoints(x1, y1, x2, y2)
	brushRef := r.resources.AddBrush(r.fillBrush)

	r.commands = append(r.commands, FillRectCommand{
		Rect:	rect,
		Brush:	brushRef,
	})
}

func FillStroke

FillStroke fills and then strokes the current path, then clears it.

func (r *Recorder) FillStroke() {
	r.FillPreserve()
	r.Stroke()
}

func FinishRecording

FinishRecording returns an immutable Recording containing all recorded commands.

After calling FinishRecording, the Recorder should not be used again.

func (r *Recorder) FinishRecording() *Recording {
	return &Recording{
		width:		r.width,
		height:		r.height,
		commands:	r.commands,
		resources:	r.resources,
	}
}

func GetCurrentPoint

GetCurrentPoint returns the current point of the path.

Returns (0, 0, false) if there is no current point.

func (r *Recorder) GetCurrentPoint() (x, y float64, ok bool) {
	if r.currentPath == nil || !r.currentPath.HasCurrentPoint() {
		return 0, 0, false
	}
	pt := r.currentPath.CurrentPoint()
	return pt.X, pt.Y, true
}

func GetTransform

GetTransform returns a copy of the current transformation matrix.

func (r *Recorder) GetTransform() Matrix {
	return r.transform
}

func Height

Height returns the height of the recording canvas.

func (r *Recorder) Height() int {
	return r.height
}

func Height

Height returns the height of the recording canvas.

func (r *Recording) Height() int {
	return r.height
}

func Identity

Identity resets the transformation matrix to identity.

func (r *Recorder) Identity() {
	r.transform = Identity()
	r.commands = append(r.commands, SetTransformCommand{Matrix: r.transform})
}

func InvertY

InvertY inverts the Y axis (useful for coordinate system changes).

func (r *Recorder) InvertY() {
	r.Translate(0, float64(r.height))
	r.Scale(1, -1)
}

func LineTo

LineTo adds a line to the current path.

func (r *Recorder) LineTo(x, y float64) {
	px, py := r.transform.TransformPoint(x, y)
	r.currentPath.LineTo(px, py)
}

func MeasureMultilineString

MeasureMultilineString measures text that may contain newlines.

Returns (width, height) where width is the maximum line width.

If no font face is set, returns (0, 0).

func (r *Recorder) MeasureMultilineString(s string, lineSpacing float64) (width, height float64) {
	if r.fontFace == nil {
		return 0, 0
	}
	lines := recorderSplitLines(s)
	fh := r.fontFace.Metrics().LineHeight()
	for _, line := range lines {
		lw, _ := text.Measure(line, r.fontFace)
		if lw > width {
			width = lw
		}
	}
	n := float64(len(lines))
	height = n*fh*lineSpacing - (lineSpacing-1)*fh
	return
}

func MeasureString

MeasureString returns approximate dimensions of text.

Note: Actual measurement depends on the backend and font.

Returns (0, 0) if no font is set.

func (r *Recorder) MeasureString(s string) (w, h float64) {
	if r.fontFace == nil {
		// Approximate measurement based on font size
		// Average character width is roughly 0.6 * font size
		w = float64(len(s)) * r.fontSize * 0.6
		h = r.fontSize * 1.2
		return
	}
	return text.Measure(s, r.fontFace)
}

func MoveTo

MoveTo starts a new subpath at the given point.

func (r *Recorder) MoveTo(x, y float64) {
	px, py := r.transform.TransformPoint(x, y)
	r.currentPath.MoveTo(px, py)
}

func NewRecorder

NewRecorder creates a new Recorder for the given dimensions.

The Recorder starts with default state: black fill/stroke, 1px line width,

butt caps, miter joins, non-zero fill rule, and identity transform.

func NewRecorder(width, height int) *Recorder {
	defaultBrush := NewSolidBrush(gg.Black)
	return &Recorder{
		width:		width,
		height:		height,
		commands:	make([]Command, 0, 256),
		resources:	NewResourcePool(),
		currentPath:	gg.NewPath(),
		fillBrush:	defaultBrush,
		strokeBrush:	defaultBrush,
		lineWidth:	1.0,
		lineCap:	LineCapButt,
		lineJoin:	LineJoinMiter,
		miterLimit:	4.0,
		fillRule:	FillRuleNonZero,
		antiAlias:	true,
		transform:	Identity(),
		stateStack:	make([]recorderState, 0, 8),
	}
}

func NewSubPath

NewSubPath starts a new subpath without closing the previous one.

This is a no-op as MoveTo already creates a new subpath.

func (r *Recorder) NewSubPath() {
	// No-op, provided for API compatibility
}

func Playback

Playback replays the recording to the given backend.

func (r *Recording) Playback(backend Backend) error {
	// Initialize backend
	if err := backend.Begin(r.width, r.height); err != nil {
		return err
	}

	// Replay each command
	for _, cmd := range r.commands {
		switch c := cmd.(type) {
		case SaveCommand:
			backend.Save()
		case RestoreCommand:
			backend.Restore()
		case SetTransformCommand:
			backend.SetTransform(c.Matrix)
		case SetClipCommand:
			path := r.resources.GetPath(c.Path)
			backend.SetClip(path, c.Rule)
		case ClearClipCommand:
			backend.ClearClip()
		case ClipRoundRectCommand:
			// Convert RRect clip to a path for backends that don't
			// natively support rounded rectangle clipping.
			rrPath := gg.BuildPath().RoundRect(c.X, c.Y, c.W, c.H, c.Radius).Build()
			backend.SetClip(rrPath, FillRuleNonZero)
		case FillPathCommand:
			path := r.resources.GetPath(c.Path)
			brush := r.resources.GetBrush(c.Brush)
			backend.FillPath(path, brush, c.Rule)
		case StrokePathCommand:
			path := r.resources.GetPath(c.Path)
			brush := r.resources.GetBrush(c.Brush)
			backend.StrokePath(path, brush, c.Stroke)
		case FillRectCommand:
			brush := r.resources.GetBrush(c.Brush)
			backend.FillRect(c.Rect, brush)
		case DrawImageCommand:
			img := r.resources.GetImage(c.Image)
			backend.DrawImage(img, c.SrcRect, c.DstRect, c.Options)
		case DrawTextCommand:
			brush := r.resources.GetBrush(c.Brush)
			// Font face lookup would need additional handling
			backend.DrawText(c.Text, c.X, c.Y, nil, brush)
		case StrokeTextCommand:
			brush := r.resources.GetBrush(c.Brush)
			// StrokeText is recorded as a stroke text command.
			// Backends that support text stroking can use the stroke style;
			// others fall back to DrawText (fill) as an approximation.
			backend.DrawText(c.Text, c.X, c.Y, nil, brush)
		// Style commands are handled by the backend's internal state
		// during the actual drawing operations
		case SetFillStyleCommand, SetStrokeStyleCommand,
			SetLineWidthCommand, SetLineCapCommand, SetLineJoinCommand,
			SetMiterLimitCommand, SetDashCommand, SetFillRuleCommand:
			// These are state commands that were recorded but the actual
			// style is captured in the drawing commands themselves
		}
	}

	return backend.End()
}

func Pop

Pop is an alias for Restore, matching gg.Context API.

func (r *Recorder) Pop() {
	r.Restore()
}

func Push

Push is an alias for Save, matching gg.Context API.

func (r *Recorder) Push() {
	r.Save()
}

func QuadraticTo

QuadraticTo adds a quadratic Bezier curve to the current path.

func (r *Recorder) QuadraticTo(cx, cy, x, y float64) {
	cpx, cpy := r.transform.TransformPoint(cx, cy)
	px, py := r.transform.TransformPoint(x, y)
	r.currentPath.QuadraticTo(cpx, cpy, px, py)
}

func ResetClip

ResetClip removes all clipping regions.

func (r *Recorder) ResetClip() {
	r.commands = append(r.commands, ClearClipCommand{})
}

func Resources

Resources returns the resource pool.

func (r *Recording) Resources() *ResourcePool {
	return r.resources
}

func Restore

Restore restores the previously saved graphics state.

If the state stack is empty, this is a no-op.

func (r *Recorder) Restore() {
	if len(r.stateStack) == 0 {
		return
	}

	state := r.stateStack[len(r.stateStack)-1]
	r.stateStack = r.stateStack[:len(r.stateStack)-1]

	r.fillBrush = state.fillBrush
	r.strokeBrush = state.strokeBrush
	r.lineWidth = state.lineWidth
	r.lineCap = state.lineCap
	r.lineJoin = state.lineJoin
	r.miterLimit = state.miterLimit
	r.dashPattern = state.dashPattern
	r.dashOffset = state.dashOffset
	r.fillRule = state.fillRule
	r.antiAlias = state.antiAlias
	r.transform = state.transform
	r.fontFace = state.fontFace
	r.fontFamily = state.fontFamily
	r.fontSize = state.fontSize

	r.commands = append(r.commands, RestoreCommand{})
}

func Rotate

Rotate applies a rotation (angle in radians).

func (r *Recorder) Rotate(angle float64) {
	r.transform = r.transform.Multiply(Rotate(angle))
	r.commands = append(r.commands, SetTransformCommand{Matrix: r.transform})
}

func RotateAbout

RotateAbout rotates around a specific point.

func (r *Recorder) RotateAbout(angle, x, y float64) {
	r.Translate(x, y)
	r.Rotate(angle)
	r.Translate(-x, -y)
}

func Save

Save saves the current graphics state to the stack.

The state includes transform, fill style, stroke style, and line properties.

func (r *Recorder) Save() {
	// Clone dash pattern
	var dashCopy []float64
	if r.dashPattern != nil {
		dashCopy = make([]float64, len(r.dashPattern))
		copy(dashCopy, r.dashPattern)
	}

	r.stateStack = append(r.stateStack, recorderState{
		fillBrush:	r.fillBrush,
		strokeBrush:	r.strokeBrush,
		lineWidth:	r.lineWidth,
		lineCap:	r.lineCap,
		lineJoin:	r.lineJoin,
		miterLimit:	r.miterLimit,
		dashPattern:	dashCopy,
		dashOffset:	r.dashOffset,
		fillRule:	r.fillRule,
		antiAlias:	r.antiAlias,
		transform:	r.transform,
		fontFace:	r.fontFace,
		fontFamily:	r.fontFamily,
		fontSize:	r.fontSize,
	})

	r.commands = append(r.commands, SaveCommand{})
}

func Scale

Scale applies a scaling transformation.

func (r *Recorder) Scale(sx, sy float64) {
	r.transform = r.transform.Multiply(Scale(sx, sy))
	r.commands = append(r.commands, SetTransformCommand{Matrix: r.transform})
}

func SetAntiAlias

SetAntiAlias enables or disables anti-aliasing for geometry rendering.

When disabled, shapes are rendered with binary coverage (no gray edge pixels).

func (r *Recorder) SetAntiAlias(enabled bool) {
	r.antiAlias = enabled
	r.commands = append(r.commands, SetAntiAliasCommand{Enabled: enabled})
}

func SetColor

SetColor sets both fill and stroke color from a color.Color.

func (r *Recorder) SetColor(c gg.RGBA) {
	brush := NewSolidBrush(c)
	r.fillBrush = brush
	r.strokeBrush = brush
	brushRef := r.resources.AddBrush(brush)
	r.commands = append(r.commands,
		SetFillStyleCommand{Brush: brushRef},
		SetStrokeStyleCommand{Brush: brushRef})
}

func SetDash

SetDash sets the dash pattern for stroking.

Pass alternating dash and gap lengths.

Passing no arguments clears the dash pattern (returns to solid lines).

func (r *Recorder) SetDash(lengths ...float64) {
	if len(lengths) == 0 {
		r.ClearDash()
		return
	}

	r.dashPattern = make([]float64, len(lengths))
	copy(r.dashPattern, lengths)
	r.commands = append(r.commands, SetDashCommand{Pattern: r.dashPattern, Offset: r.dashOffset})
}

func SetDashOffset

SetDashOffset sets the starting offset into the dash pattern.

func (r *Recorder) SetDashOffset(offset float64) {
	r.dashOffset = offset
	if r.dashPattern != nil {
		r.commands = append(r.commands, SetDashCommand{Pattern: r.dashPattern, Offset: r.dashOffset})
	}
}

func SetFillBrush

SetFillBrush sets the fill brush from a gg.Brush.

func (r *Recorder) SetFillBrush(brush gg.Brush) {
	r.SetFillStyle(BrushFromGG(brush))
}

func SetFillRGB

SetFillRGB sets the fill color using RGB values (0-1).

func (r *Recorder) SetFillRGB(red, green, blue float64) {
	r.SetFillStyle(NewSolidBrush(gg.RGB(red, green, blue)))
}

func SetFillRGBA

SetFillRGBA sets the fill color using RGBA values (0-1).

func (r *Recorder) SetFillRGBA(red, green, blue, alpha float64) {
	r.SetFillStyle(NewSolidBrush(gg.RGBA2(red, green, blue, alpha)))
}

func SetFillRule

SetFillRule sets the fill rule.

func (r *Recorder) SetFillRule(rule FillRule) {
	r.fillRule = rule
	r.commands = append(r.commands, SetFillRuleCommand{Rule: rule})
}

func SetFillRuleGG

SetFillRuleGG sets the fill rule from gg.FillRule.

func (r *Recorder) SetFillRuleGG(rule gg.FillRule) {
	// #nosec G115 -- FillRule enum values are within uint8 range
	r.SetFillRule(FillRule(rule))
}

func SetFillStyle

SetFillStyle sets the fill brush.

func (r *Recorder) SetFillStyle(brush Brush) {
	r.fillBrush = brush
	brushRef := r.resources.AddBrush(brush)
	r.commands = append(r.commands, SetFillStyleCommand{Brush: brushRef})
}

func SetFont

SetFont sets the current font face for text drawing.

func (r *Recorder) SetFont(face text.Face) {
	r.fontFace = face
}

func SetFontFamily

SetFontFamily sets the current font family name.

func (r *Recorder) SetFontFamily(family string) {
	r.fontFamily = family
}

func SetFontSize

SetFontSize sets the current font size in points.

func (r *Recorder) SetFontSize(size float64) {
	r.fontSize = size
}

func SetHexColor

SetHexColor sets both fill and stroke color using a hex string.

func (r *Recorder) SetHexColor(hex string) {
	r.SetColor(gg.Hex(hex))
}

func SetLineCap

SetLineCap sets the line cap style.

func (r *Recorder) SetLineCap(lc LineCap) {
	r.lineCap = lc
	r.commands = append(r.commands, SetLineCapCommand{Cap: lc})
}

func SetLineCapGG

SetLineCapGG sets the line cap style from gg.LineCap.

func (r *Recorder) SetLineCapGG(lc gg.LineCap) {
	// #nosec G115 -- LineCap enum values are within uint8 range
	r.SetLineCap(LineCap(lc))
}

func SetLineJoin

SetLineJoin sets the line join style.

func (r *Recorder) SetLineJoin(join LineJoin) {
	r.lineJoin = join
	r.commands = append(r.commands, SetLineJoinCommand{Join: join})
}

func SetLineJoinGG

SetLineJoinGG sets the line join style from gg.LineJoin.

func (r *Recorder) SetLineJoinGG(join gg.LineJoin) {
	// #nosec G115 -- LineJoin enum values are within uint8 range
	r.SetLineJoin(LineJoin(join))
}

func SetLineWidth

SetLineWidth sets the line width for stroking.

func (r *Recorder) SetLineWidth(width float64) {
	r.lineWidth = width
	r.commands = append(r.commands, SetLineWidthCommand{Width: width})
}

func SetMiterLimit

SetMiterLimit sets the miter limit for line joins.

func (r *Recorder) SetMiterLimit(limit float64) {
	r.miterLimit = limit
	r.commands = append(r.commands, SetMiterLimitCommand{Limit: limit})
}

func SetRGB

SetRGB sets both fill and stroke color using RGB values (0-1).

func (r *Recorder) SetRGB(red, green, blue float64) {
	r.SetColor(gg.RGB(red, green, blue))
}

func SetRGBA

SetRGBA sets both fill and stroke color using RGBA values (0-1).

func (r *Recorder) SetRGBA(red, green, blue, alpha float64) {
	r.SetColor(gg.RGBA2(red, green, blue, alpha))
}

func SetStrokeBrush

SetStrokeBrush sets the stroke brush from a gg.Brush.

func (r *Recorder) SetStrokeBrush(brush gg.Brush) {
	r.SetStrokeStyle(BrushFromGG(brush))
}

func SetStrokeRGB

SetStrokeRGB sets the stroke color using RGB values (0-1).

func (r *Recorder) SetStrokeRGB(red, green, blue float64) {
	r.SetStrokeStyle(NewSolidBrush(gg.RGB(red, green, blue)))
}

func SetStrokeRGBA

SetStrokeRGBA sets the stroke color using RGBA values (0-1).

func (r *Recorder) SetStrokeRGBA(red, green, blue, alpha float64) {
	r.SetStrokeStyle(NewSolidBrush(gg.RGBA2(red, green, blue, alpha)))
}

func SetStrokeStyle

SetStrokeStyle sets the stroke brush.

func (r *Recorder) SetStrokeStyle(brush Brush) {
	r.strokeBrush = brush
	brushRef := r.resources.AddBrush(brush)
	r.commands = append(r.commands, SetStrokeStyleCommand{Brush: brushRef})
}

func SetTransform

SetTransform replaces the current transformation matrix.

func (r *Recorder) SetTransform(m Matrix) {
	r.transform = m
	r.commands = append(r.commands, SetTransformCommand{Matrix: r.transform})
}

func Shear

Shear applies a shear transformation.

func (r *Recorder) Shear(x, y float64) {
	r.transform = r.transform.Multiply(Shear(x, y))
	r.commands = append(r.commands, SetTransformCommand{Matrix: r.transform})
}

func Stroke

Stroke strokes the current path and clears it.

func (r *Recorder) Stroke() {
	if r.currentPath.NumVerbs() == 0 {
		return
	}

	pathRef := r.resources.AddPath(r.currentPath)
	brushRef := r.resources.AddBrush(r.strokeBrush)

	stroke := Stroke{
		Width:		r.lineWidth,
		Cap:		r.lineCap,
		Join:		r.lineJoin,
		MiterLimit:	r.miterLimit,
		DashPattern:	r.dashPattern,
		DashOffset:	r.dashOffset,
	}

	r.commands = append(r.commands, StrokePathCommand{
		Path:	pathRef,
		Brush:	brushRef,
		Stroke:	stroke,
	})

	r.currentPath = gg.NewPath()
}

func StrokePreserve

StrokePreserve strokes the current path without clearing it.

func (r *Recorder) StrokePreserve() {
	if r.currentPath.NumVerbs() == 0 {
		return
	}

	pathRef := r.resources.AddPath(r.currentPath)
	brushRef := r.resources.AddBrush(r.strokeBrush)

	stroke := Stroke{
		Width:		r.lineWidth,
		Cap:		r.lineCap,
		Join:		r.lineJoin,
		MiterLimit:	r.miterLimit,
		DashPattern:	r.dashPattern,
		DashOffset:	r.dashOffset,
	}

	r.commands = append(r.commands, StrokePathCommand{
		Path:	pathRef,
		Brush:	brushRef,
		Stroke:	stroke,
	})
}

func StrokeRectangle

StrokeRectangle strokes a rectangle without adding it to the path.

func (r *Recorder) StrokeRectangle(x, y, w, h float64) {
	// Transform corners
	x1, y1 := r.transform.TransformPoint(x, y)
	x2, y2 := r.transform.TransformPoint(x+w, y+h)

	rect := NewRectFromPoints(x1, y1, x2, y2)
	brushRef := r.resources.AddBrush(r.strokeBrush)

	stroke := Stroke{
		Width:		r.lineWidth,
		Cap:		r.lineCap,
		Join:		r.lineJoin,
		MiterLimit:	r.miterLimit,
		DashPattern:	r.dashPattern,
		DashOffset:	r.dashOffset,
	}

	r.commands = append(r.commands, StrokeRectCommand{
		Rect:	rect,
		Brush:	brushRef,
		Stroke:	stroke,
	})
}

func StrokeString

StrokeString strokes text outlines at position (x, y) where y is the baseline.

The stroke width, cap, join, and dash are captured from the current recorder state.

func (r *Recorder) StrokeString(s string, x, y float64) {
	px, py := r.transform.TransformPoint(x, y)

	brushRef := r.resources.AddBrush(r.strokeBrush)

	stroke := Stroke{
		Width:		r.lineWidth,
		Cap:		r.lineCap,
		Join:		r.lineJoin,
		MiterLimit:	r.miterLimit,
		DashPattern:	r.dashPattern,
		DashOffset:	r.dashOffset,
	}

	r.commands = append(r.commands, StrokeTextCommand{
		Text:		s,
		X:		px,
		Y:		py,
		FontSize:	r.fontSize,
		FontFamily:	r.fontFamily,
		Brush:		brushRef,
		Stroke:		stroke,
	})
}

func StrokeStringAnchored

StrokeStringAnchored strokes text outlines with an anchor point.

The anchor point is specified by ax and ay, which are in the range [0, 1].

func (r *Recorder) StrokeStringAnchored(s string, x, y, ax, ay float64) {
	// For recording, we store the base position and let the backend handle anchoring
	// This is a simplification; a full implementation would measure text
	r.StrokeString(s, x, y)
}

func Transform

Transform multiplies the current transformation matrix by the given matrix.

func (r *Recorder) Transform(m Matrix) {
	r.transform = r.transform.Multiply(m)
	r.commands = append(r.commands, SetTransformCommand{Matrix: r.transform})
}

func TransformPoint

TransformPoint transforms a point by the current matrix.

func (r *Recorder) TransformPoint(x, y float64) (float64, float64) {
	return r.transform.TransformPoint(x, y)
}

func Translate

Translate applies a translation to the transformation matrix.

func (r *Recorder) Translate(x, y float64) {
	r.transform = r.transform.Multiply(Translate(x, y))
	r.commands = append(r.commands, SetTransformCommand{Matrix: r.transform})
}

func Width

Width returns the width of the recording canvas.

func (r *Recorder) Width() int {
	return r.width
}

func Width

Width returns the width of the recording canvas.

func (r *Recording) Width() int {
	return r.width
}

func WordWrap

WordWrap wraps text to fit within the given width using word boundaries.

Uses the current font face for text measurement.

If no font face is set, returns the input string as a single-element slice.

func (r *Recorder) WordWrap(s string, w float64) []string {
	if r.fontFace == nil {
		return []string{s}
	}
	results := text.WrapText(s, r.fontFace, w, text.WrapWord)
	lines := make([]string, len(results))
	for i, res := range results {
		lines[i] = res.Text
	}
	return lines
}

Structs

type Recorder struct

Recorder captures drawing operations as commands.

It mirrors the gg.Context drawing API but generates commands

instead of rasterizing pixels. Use FinishRecording to obtain

an immutable Recording that can be replayed to different backends.

 

Example:

 

rec := recording.NewRecorder(800, 600)

rec.SetRGB(1, 0, 0)

rec.DrawCircle(100, 100, 50)

rec.Fill()

recording := rec.FinishRecording()

 

The Recorder is not safe for concurrent use.

type Recorder struct {
	width, height	int
	commands	[]Command
	resources	*ResourcePool

	// Current path being built
	currentPath	*gg.Path

	// Current state
	fillBrush	Brush
	strokeBrush	Brush
	lineWidth	float64
	lineCap		LineCap
	lineJoin	LineJoin
	miterLimit	float64
	dashPattern	[]float64
	dashOffset	float64
	fillRule	FillRule
	antiAlias	bool
	transform	Matrix

	// Current font
	fontFace	text.Face
	fontFamily	string
	fontSize	float64

	// State stack
	stateStack	[]recorderState
}

type Recording struct

Recording is an immutable container for recorded drawing commands.

It can be replayed to any Backend implementation.

type Recording struct {
	width, height	int
	commands	[]Command
	resources	*ResourcePool
}