scene/encoding.go
Functions
func AffineFromMatrix
func AffineFromMatrix(m gg.Matrix) Affine {
return Affine{
A: float32(m.A),
B: float32(m.B),
C: float32(m.C),
D: float32(m.D),
E: float32(m.E),
F: float32(m.F),
}
}
func Append
Append merges another encoding into this one.
The other encoding's content is appended after the current content.
Append merges another Encoding into this one, adjusting brush indices.
For scene-level merging with image registry offset, use AppendWithImages.
func (e *Encoding) Append(other *Encoding) {
e.AppendWithImages(other, 0)
}
func AppendWithImages
AppendWithImages merges another Encoding into this one, adjusting both
brush indices and image indices by their respective offsets.
imageOffset is the number of images already registered in the target scene;
it shifts TagImage drawData entries so they reference the correct images
after merging two image registries.
func (e *Encoding) AppendWithImages(other *Encoding, imageOffset uint32) {
if other == nil || len(other.tags) == 0 {
return
}
// Calculate brush index offset for the appended encoding
//nolint:gosec // brush slice length is bounded, overflow not possible in practice
brushOffset := uint32(len(e.brushes))
// Append tags directly
e.tags = append(e.tags, other.tags...)
// Append path data directly
e.pathData = append(e.pathData, other.pathData...)
// Append draw data with adjusted brush indices
// We need to adjust brush references in fill/stroke commands
drawDataStart := len(e.drawData)
e.drawData = append(e.drawData, other.drawData...)
// Adjust brush indices in the appended draw data.
// Also adjust brush indices embedded in textData for TagText commands.
drawIdx := 0
textDataStart := len(e.textData)
e.textData = append(e.textData, other.textData...)
textOff := 0
for _, tag := range other.tags {
switch tag {
case TagFill:
if drawIdx < len(other.drawData) {
e.drawData[drawDataStart+drawIdx] += brushOffset
}
drawIdx += 2
case TagFillRoundRect:
if drawIdx < len(other.drawData) {
e.drawData[drawDataStart+drawIdx] += brushOffset
}
drawIdx += 2
case TagStroke:
if drawIdx < len(other.drawData) {
e.drawData[drawDataStart+drawIdx] += brushOffset
}
drawIdx += 5
case TagPushLayer:
drawIdx += 2
case TagSetAntiAlias:
drawIdx++ // 1 uint32: 0 or 1
case TagImage:
if imageOffset > 0 && drawIdx < len(other.drawData) {
e.drawData[drawDataStart+drawIdx] += imageOffset
}
drawIdx++
case TagText:
if textOff+glyphRunDataSize <= len(other.textData) {
glyphCount := int(binary.LittleEndian.Uint16(other.textData[textOff+12:]))
textLen := int(binary.LittleEndian.Uint16(other.textData[textOff+28:]))
// Adjust brush index (at offset 24 in GlyphRunData)
brushIdxOff := textDataStart + textOff + 24
oldBrush := binary.LittleEndian.Uint32(e.textData[brushIdxOff:])
binary.LittleEndian.PutUint32(e.textData[brushIdxOff:], oldBrush+brushOffset)
textOff += glyphRunDataSize + glyphCount*glyphEntrySize + textLen
}
}
}
// Append transforms
e.transforms = append(e.transforms, other.transforms...)
// Append brushes
e.brushes = append(e.brushes, other.brushes...)
// Union bounds
e.bounds = e.bounds.Union(other.bounds)
// Update statistics
e.pathCount += other.pathCount
e.shapeCount += other.shapeCount
}
func AppendWithTranslation
AppendWithTranslation merges another encoding with a translation offset
applied to all path coordinates.
Architecture: pathData coordinate offset approach.
Our SceneCanvas pre-bakes absolute coordinates via applyTransform() and
records Identity transforms in the encoding. This differs from Vello where
paths stay in local coordinates and the transform stream carries the full
transformation (Vello composes transforms at append: parent * child).
Because our pathData already contains absolute coordinates with Identity
transforms, the correct offset strategy is:
- pathData: offset all coordinate float32 values by (dx, dy)
- transforms: copy VERBATIM (they are Identity; composing translation
would cause double-offset since the renderer applies transforms to
already-offset pathData coordinates)
Alternative approaches considered:
- Vello pattern (transform composition only): multiply each child
transform by TranslateAffine(dx, dy). Does NOT work with our
pre-baked coordinate architecture — coordinates would stay at (0,0)
since transforms are Identity.
- Skia/Flutter pattern (replay-time canvas transform): wrap replay in
Push/Translate/Pop on the target canvas. Works with render.Canvas
(used by current desktop compositor) but NOT with SceneCanvas
(Scene.Append has no canvas context).
- Migrate to Vello architecture: stop pre-baking coordinates, record
transforms in encoding, compose at append. Correct long-term but
requires rewriting SceneCanvas coordinate handling.
Tag exhaustiveness: every Tag that consumes pathData floats MUST have a
case in the switch below. Adding a new tag with pathData without updating
this switch will cause silent coordinate corruption. The default case
handles tags with zero pathData/drawData (markers, clip, pop).
func (e *Encoding) AppendWithTranslation(other *Encoding, dx, dy float32, imageOffset uint32) {
if other == nil || len(other.tags) == 0 {
return
}
if dx == 0 && dy == 0 {
e.AppendWithImages(other, imageOffset)
return
}
//nolint:gosec // brush slice length is bounded
brushOffset := uint32(len(e.brushes))
e.tags = append(e.tags, other.tags...)
pathStart := len(e.pathData)
e.pathData = append(e.pathData, other.pathData...)
drawDataStart := len(e.drawData)
e.drawData = append(e.drawData, other.drawData...)
pathIdx := 0
drawIdx := 0
for _, tag := range other.tags {
switch tag {
// --- Coordinate tags: offset pathData by (dx, dy) ---
case TagMoveTo, TagLineTo:
e.pathData[pathStart+pathIdx] += dx
e.pathData[pathStart+pathIdx+1] += dy
pathIdx += 2
case TagQuadTo:
for i := 0; i < 4; i += 2 {
e.pathData[pathStart+pathIdx+i] += dx
e.pathData[pathStart+pathIdx+i+1] += dy
}
pathIdx += 4
case TagCubicTo:
for i := 0; i < 6; i += 2 {
e.pathData[pathStart+pathIdx+i] += dx
e.pathData[pathStart+pathIdx+i+1] += dy
}
pathIdx += 6
case TagFillRoundRect:
// 6 floats: minX, minY, maxX, maxY (offset), radiusX, radiusY (no offset).
e.pathData[pathStart+pathIdx] += dx
e.pathData[pathStart+pathIdx+1] += dy
e.pathData[pathStart+pathIdx+2] += dx
e.pathData[pathStart+pathIdx+3] += dy
pathIdx += 6
if drawIdx < len(other.drawData) {
e.drawData[drawDataStart+drawIdx] += brushOffset
}
drawIdx += 2
// --- Non-coordinate pathData: skip without offset ---
case TagBrush:
pathIdx += 4 // 4 float32: R, G, B, A — not coordinates
// --- Draw data only (no pathData) ---
case TagFill:
if drawIdx < len(other.drawData) {
e.drawData[drawDataStart+drawIdx] += brushOffset
}
drawIdx += 2
case TagStroke:
if drawIdx < len(other.drawData) {
e.drawData[drawDataStart+drawIdx] += brushOffset
}
drawIdx += 5 // brush + width + miterLimit + cap + join
case TagPushLayer:
drawIdx += 2 // blend mode + alpha
case TagSetAntiAlias:
drawIdx++ // 1 uint32: 0 or 1, no coordinate data
case TagImage:
if imageOffset > 0 && drawIdx < len(other.drawData) {
e.drawData[drawDataStart+drawIdx] += imageOffset
}
drawIdx++ // image index only; transform is in transforms stream
case TagText:
// Text data is in the textData stream. Handled in a second pass below.
// --- Marker/structural tags: zero pathData, zero drawData ---
case TagTransform:
// Transform data is in the separate transforms stream, not pathData.
// Handled below (copied verbatim).
case TagBeginPath, TagEndPath, TagClosePath,
TagPopLayer, TagBeginClip, TagEndClip:
// Pure markers — no data in any stream.
default:
panic(fmt.Sprintf("scene.AppendWithTranslation: unhandled tag 0x%02X (%s) — update switch to handle pathData/drawData layout for this tag", byte(tag), tag))
}
}
e.appendTextDataWithTranslation(other, brushOffset, dx, dy)
// Transforms copied verbatim — see architecture note above.
e.transforms = append(e.transforms, other.transforms...)
e.brushes = append(e.brushes, other.brushes...)
ob := other.bounds
ob.MinX += dx
ob.MinY += dy
ob.MaxX += dx
ob.MaxY += dy
e.bounds = e.bounds.Union(ob)
e.pathCount += other.pathCount
e.shapeCount += other.shapeCount
}
func Bounds
Bounds returns the cumulative bounding box of all encoded content.
func (e *Encoding) Bounds() Rect {
return e.bounds
}
func Brushes
Brushes returns the brush definitions.
func (e *Encoding) Brushes() []Brush {
return e.brushes
}
func Capacity
Capacity returns the total allocated capacity in bytes.
func (e *Encoding) Capacity() int {
return cap(e.tags) +
cap(e.pathData)*4 +
cap(e.drawData)*4 +
cap(e.textData) +
cap(e.transforms)*24 +
cap(e.brushes)*20
}
func Clone
Clone creates a deep copy of the encoding.
func (e *Encoding) Clone() *Encoding {
clone := NewEncoding()
clone.tags = make([]Tag, len(e.tags))
copy(clone.tags, e.tags)
clone.pathData = make([]float32, len(e.pathData))
copy(clone.pathData, e.pathData)
clone.drawData = make([]uint32, len(e.drawData))
copy(clone.drawData, e.drawData)
clone.transforms = make([]Affine, len(e.transforms))
copy(clone.transforms, e.transforms)
clone.textData = make([]byte, len(e.textData))
copy(clone.textData, e.textData)
clone.brushes = make([]Brush, len(e.brushes))
copy(clone.brushes, e.brushes)
clone.bounds = e.bounds
clone.pathBounds = e.pathBounds
clone.pathCount = e.pathCount
clone.shapeCount = e.shapeCount
return clone
}
func CommandBounds
CommandBounds returns per-draw-command bounding boxes as TaggedBounds.
The command index serves as a stable ID — valid when scene is built
in the same order each frame (standard immediate-mode pattern).
func (e *Encoding) CommandBounds() []TaggedBounds {
result := make([]TaggedBounds, len(e.commandBounds))
for i, b := range e.commandBounds {
result[i] = TaggedBounds{
ID: uint64(i),
Rect: b.ImageRect(),
}
}
return result
}
func DefaultStrokeStyle
DefaultStrokeStyle returns default stroke parameters.
func DefaultStrokeStyle() *StrokeStyle {
return &StrokeStyle{
Width: 1.0,
MiterLimit: 10.0,
Cap: LineCapButt,
Join: LineJoinMiter,
}
}
func DrawData
DrawData returns the draw data stream.
func (e *Encoding) DrawData() []uint32 {
return e.drawData
}
func EmptyRect
EmptyRect returns an empty rectangle (inverted bounds for union operations).
func EmptyRect() Rect {
return Rect{
MinX: math.MaxFloat32,
MinY: math.MaxFloat32,
MaxX: -math.MaxFloat32,
MaxY: -math.MaxFloat32,
}
}
func EncodeAntiAlias
EncodeAntiAlias adds an anti-aliasing state change command.
The value is stored as 1 uint32 in drawData (0 = disabled, 1 = enabled).
func (e *Encoding) EncodeAntiAlias(enabled bool) {
e.tags = append(e.tags, TagSetAntiAlias)
var val uint32
if enabled {
val = 1
}
e.drawData = append(e.drawData, val)
}
func EncodeBeginClip
EncodeBeginClip begins a clipping region.
func (e *Encoding) EncodeBeginClip() {
e.tags = append(e.tags, TagBeginClip)
}
func EncodeBrush
EncodeBrush encodes a brush definition.
func (e *Encoding) EncodeBrush(brush Brush) int {
idx := len(e.brushes)
e.brushes = append(e.brushes, brush)
e.tags = append(e.tags, TagBrush)
// For solid brush, encode RGBA
e.pathData = append(e.pathData,
float32(brush.Color.R),
float32(brush.Color.G),
float32(brush.Color.B),
float32(brush.Color.A),
)
return idx
}
func EncodeEndClip
EncodeEndClip ends the current clipping region.
func (e *Encoding) EncodeEndClip() {
e.tags = append(e.tags, TagEndClip)
}
func EncodeFill
EncodeFill adds a fill command with the given brush and fill style.
func (e *Encoding) EncodeFill(brush Brush, style FillStyle) {
brushIdx := len(e.brushes)
e.brushes = append(e.brushes, brush)
e.tags = append(e.tags, TagFill)
//nolint:gosec // brush index is bounded by slice length, overflow not possible in practice
e.drawData = append(e.drawData, uint32(brushIdx), uint32(style))
e.shapeCount++
}
func EncodeFillRoundRect
EncodeFillRoundRect adds a rounded rectangle fill command using SDF rendering.
This bypasses path encoding entirely, storing the rectangle geometry directly
in the data streams for dedicated SDF per-pixel rendering in the tile renderer.
func (e *Encoding) EncodeFillRoundRect(brush Brush, style FillStyle, rect Rect, rx, ry float32) {
brushIdx := len(e.brushes)
e.brushes = append(e.brushes, brush)
e.tags = append(e.tags, TagFillRoundRect)
//nolint:gosec // brush index is bounded by slice length, overflow not possible in practice
e.drawData = append(e.drawData, uint32(brushIdx), uint32(style))
e.pathData = append(e.pathData, rect.MinX, rect.MinY, rect.MaxX, rect.MaxY, rx, ry)
e.bounds = e.bounds.Union(rect)
e.shapeCount++
}
func EncodeImage
EncodeImage encodes an image reference.
func (e *Encoding) EncodeImage(imageIndex uint32, transform Affine) {
e.tags = append(e.tags, TagImage)
e.drawData = append(e.drawData, imageIndex)
e.transforms = append(e.transforms, transform)
}
func EncodePath
EncodePath encodes a complete path from a gg.Path.
func (e *Encoding) EncodePath(p *gg.Path) {
if p == nil {
return
}
if p.NumVerbs() == 0 {
return
}
e.tags = append(e.tags, TagBeginPath)
e.pathBounds = EmptyRect()
e.pathCount++
p.Iterate(func(verb gg.PathVerb, coords []float64) {
switch verb {
case gg.MoveTo:
e.encodeMoveTo(float32(coords[0]), float32(coords[1]))
case gg.LineTo:
e.encodeLineTo(float32(coords[0]), float32(coords[1]))
case gg.QuadTo:
e.encodeQuadTo(
float32(coords[0]), float32(coords[1]),
float32(coords[2]), float32(coords[3]),
)
case gg.CubicTo:
e.encodeCubicTo(
float32(coords[0]), float32(coords[1]),
float32(coords[2]), float32(coords[3]),
float32(coords[4]), float32(coords[5]),
)
case gg.Close:
e.tags = append(e.tags, TagClosePath)
}
})
e.tags = append(e.tags, TagEndPath)
e.bounds = e.bounds.Union(e.pathBounds)
}
func EncodePopLayer
EncodePopLayer pops the current compositing layer.
func (e *Encoding) EncodePopLayer() {
e.tags = append(e.tags, TagPopLayer)
}
func EncodePushLayer
EncodePushLayer pushes a new compositing layer.
func (e *Encoding) EncodePushLayer(blend BlendMode, alpha float32) {
e.tags = append(e.tags, TagPushLayer)
e.drawData = append(e.drawData, uint32(blend))
e.drawData = append(e.drawData, math.Float32bits(alpha))
}
func EncodeStroke
EncodeStroke adds a stroke command with the given brush and stroke style.
func (e *Encoding) EncodeStroke(brush Brush, style *StrokeStyle) {
if style == nil {
style = DefaultStrokeStyle()
}
brushIdx := len(e.brushes)
e.brushes = append(e.brushes, brush)
e.tags = append(e.tags, TagStroke)
//nolint:gosec // brush index is bounded by slice length, overflow not possible in practice
e.drawData = append(e.drawData,
uint32(brushIdx),
math.Float32bits(style.Width),
math.Float32bits(style.MiterLimit),
uint32(style.Cap),
uint32(style.Join),
)
e.shapeCount++
}
func EncodeText
EncodeText encodes a pre-shaped text run as a TagText command.
The glyph run header, glyph entries, and original text are serialized into the textData stream.
func (e *Encoding) EncodeText(run GlyphRunData, glyphs []GlyphEntry, str string) {
e.tags = append(e.tags, TagText)
totalSize := glyphRunDataSize + len(glyphs)*glyphEntrySize + len(str)
// Grow textData in one allocation, then write directly into the tail.
base := len(e.textData)
e.textData = append(e.textData, make([]byte, totalSize)...)
buf := e.textData[base:]
off := 0
// Header
binary.LittleEndian.PutUint64(buf[off:], run.FontSourceID)
off += 8
binary.LittleEndian.PutUint32(buf[off:], math.Float32bits(run.FontSize))
off += 4
binary.LittleEndian.PutUint16(buf[off:], run.GlyphCount)
off += 2
binary.LittleEndian.PutUint16(buf[off:], uint16(run.Flags))
off += 2
binary.LittleEndian.PutUint32(buf[off:], math.Float32bits(run.OriginX))
off += 4
binary.LittleEndian.PutUint32(buf[off:], math.Float32bits(run.OriginY))
off += 4
binary.LittleEndian.PutUint32(buf[off:], run.BrushIndex)
off += 4
binary.LittleEndian.PutUint16(buf[off:], run.TextLen)
off += 2
// Glyph entries
for _, g := range glyphs {
binary.LittleEndian.PutUint16(buf[off:], uint16(g.GlyphID))
off += 2
binary.LittleEndian.PutUint32(buf[off:], math.Float32bits(g.X))
off += 4
binary.LittleEndian.PutUint32(buf[off:], math.Float32bits(g.Y))
off += 4
}
// Text bytes
copy(buf[off:], str)
e.shapeCount++
}
func EncodeTransform
EncodeTransform adds a transform command.
func (e *Encoding) EncodeTransform(t Affine) {
e.tags = append(e.tags, TagTransform)
e.transforms = append(e.transforms, t)
}
func EncodeTransformFromMatrix
EncodeTransformFromMatrix adds a transform from a gg.Matrix.
func (e *Encoding) EncodeTransformFromMatrix(m gg.Matrix) {
e.EncodeTransform(AffineFromMatrix(m))
}
func GetBrush
GetBrush returns the brush at the given index.
func (it *Iterator) GetBrush(idx uint32) (Brush, bool) {
if int(idx) >= len(it.enc.brushes) {
return Brush{}, false
}
return it.enc.brushes[idx], true
}
func Hash
Hash computes a 64-bit FNV-1a hash of the encoding for cache keys.
The hash includes all stream data to ensure uniqueness.
func (e *Encoding) Hash() uint64 {
const (
fnvOffset = 14695981039346656037
fnvPrime = 1099511628211
)
hash := uint64(fnvOffset)
// Hash tags
for _, t := range e.tags {
hash ^= uint64(t)
hash *= fnvPrime
}
// Hash path data
for _, v := range e.pathData {
bits := math.Float32bits(v)
hash ^= uint64(bits)
hash *= fnvPrime
}
// Hash draw data
for _, v := range e.drawData {
hash ^= uint64(v)
hash *= fnvPrime
}
// Hash text data
for _, b := range e.textData {
hash ^= uint64(b)
hash *= fnvPrime
}
// Hash transforms
for _, t := range e.transforms {
hash ^= uint64(math.Float32bits(t.A))
hash *= fnvPrime
hash ^= uint64(math.Float32bits(t.B))
hash *= fnvPrime
hash ^= uint64(math.Float32bits(t.C))
hash *= fnvPrime
hash ^= uint64(math.Float32bits(t.D))
hash *= fnvPrime
hash ^= uint64(math.Float32bits(t.E))
hash *= fnvPrime
hash ^= uint64(math.Float32bits(t.F))
hash *= fnvPrime
}
return hash
}
func Height
Height returns the height of the rectangle.
func (r Rect) Height() float32 {
if r.IsEmpty() {
return 0
}
return r.MaxY - r.MinY
}
func IdentityAffine
IdentityAffine returns the identity transformation.
func IdentityAffine() Affine {
return Affine{A: 1, B: 0, C: 0, D: 0, E: 1, F: 0}
}
func ImageRect
ImageRect converts to image.Rectangle (floor min, ceil max for pixel coverage).
func (r Rect) ImageRect() image.Rectangle {
if r.IsEmpty() {
return image.Rectangle{}
}
return image.Rect(
int(math.Floor(float64(r.MinX))),
int(math.Floor(float64(r.MinY))),
int(math.Ceil(float64(r.MaxX))),
int(math.Ceil(float64(r.MaxY))),
)
}
func IsAdvanced
IsAdvanced returns true if this is an advanced separable blend mode.
func (mode BlendMode) IsAdvanced() bool {
return (mode >= BlendMultiply && mode <= BlendExclusion) ||
mode == BlendNormal
}
func IsEmpty
IsEmpty returns true if the rectangle has no area.
func (r Rect) IsEmpty() bool {
return r.MinX >= r.MaxX || r.MinY >= r.MaxY
}
func IsEmpty
IsEmpty returns true if the encoding contains no commands.
func (e *Encoding) IsEmpty() bool {
return len(e.tags) == 0
}
func IsHSL
IsHSL returns true if this is an HSL-based non-separable blend mode.
func (mode BlendMode) IsHSL() bool {
return mode >= BlendHue && mode <= BlendLuminosity
}
func IsIdentity
IsIdentity returns true if this is the identity transformation.
func (a Affine) IsIdentity() bool {
return a.A == 1 && a.B == 0 && a.C == 0 &&
a.D == 0 && a.E == 1 && a.F == 0
}
func IsPorterDuff
IsPorterDuff returns true if this is a Porter-Duff compositing mode.
func (mode BlendMode) IsPorterDuff() bool {
return mode >= BlendClear && mode <= BlendPlus
}
func Multiply
Multiply returns the product of two affine transformations.
func (a Affine) Multiply(b Affine) Affine {
return Affine{
A: a.A*b.A + a.B*b.D,
B: a.A*b.B + a.B*b.E,
C: a.A*b.C + a.B*b.F + a.C,
D: a.D*b.A + a.E*b.D,
E: a.D*b.B + a.E*b.E,
F: a.D*b.C + a.E*b.F + a.F,
}
}
func NewAffine
NewAffine creates an affine transformation from individual matrix components.
| a b c |
| d e f |
func NewAffine(a, b, c, d, e, f float32) Affine {
return Affine{A: a, B: b, C: c, D: d, E: e, F: f}
}
func NewEncoding
NewEncoding creates a new empty encoding.
func NewEncoding() *Encoding {
return &Encoding{
tags: make([]Tag, 0, 64),
pathData: make([]float32, 0, 256),
drawData: make([]uint32, 0, 32),
transforms: make([]Affine, 0, 8),
textData: make([]byte, 0, 256),
brushes: make([]Brush, 0, 16),
commandBounds: make([]Rect, 0, 32),
bounds: EmptyRect(),
pathBounds: EmptyRect(),
}
}
func NewIterator
NewIterator creates an iterator for the encoding.
func (e *Encoding) NewIterator() *Iterator {
return &Iterator{enc: e}
}
func Next
Next advances to the next command and returns its tag.
Returns false when iteration is complete.
func (it *Iterator) Next() (Tag, bool) {
if it.tagIdx >= len(it.enc.tags) {
return 0, false
}
tag := it.enc.tags[it.tagIdx]
it.tagIdx++
return tag, true
}
func PathCount
PathCount returns the number of paths encoded.
func (e *Encoding) PathCount() int {
return e.pathCount
}
func PathData
PathData returns the path data stream.
func (e *Encoding) PathData() []float32 {
return e.pathData
}
func ReadDrawData
ReadDrawData reads n uint32 values from the draw data stream.
func (it *Iterator) ReadDrawData(n int) []uint32 {
if it.drawIdx+n > len(it.enc.drawData) {
return nil
}
data := it.enc.drawData[it.drawIdx : it.drawIdx+n]
it.drawIdx += n
return data
}
func ReadPathData
ReadPathData reads n float32 values from the path data stream.
func (it *Iterator) ReadPathData(n int) []float32 {
if it.pathIdx+n > len(it.enc.pathData) {
return nil
}
data := it.enc.pathData[it.pathIdx : it.pathIdx+n]
it.pathIdx += n
return data
}
func ReadTransform
ReadTransform reads the next transform from the stream.
func (it *Iterator) ReadTransform() (Affine, bool) {
if it.transIdx >= len(it.enc.transforms) {
return Affine{}, false
}
t := it.enc.transforms[it.transIdx]
it.transIdx++
return t, true
}
func RecordCommandBounds
RecordCommandBounds stores the bounding box for the current draw command.
Called by Scene after each Fill/Stroke/DrawImage with the transformed shape bounds.
Used by DamageTracker (ADR-021) for frame-to-frame object diff.
func (e *Encoding) RecordCommandBounds(bounds Rect) {
e.commandBounds = append(e.commandBounds, bounds)
}
func Reset
Reset resets the iterator to the beginning.
func (it *Iterator) Reset() {
it.tagIdx = 0
it.pathIdx = 0
it.drawIdx = 0
it.transIdx = 0
it.brushIdx = 0
}
func Reset
Reset clears the encoding for reuse without deallocating memory.
This is the key method for zero-allocation pooling.
func (e *Encoding) Reset() {
e.tags = e.tags[:0]
e.pathData = e.pathData[:0]
e.drawData = e.drawData[:0]
e.transforms = e.transforms[:0]
e.textData = e.textData[:0]
e.brushes = e.brushes[:0]
e.commandBounds = e.commandBounds[:0]
e.bounds = EmptyRect()
e.pathBounds = EmptyRect()
e.pathCount = 0
e.shapeCount = 0
}
func RotateAffine
RotateAffine creates a rotation transformation (angle in radians).
func RotateAffine(angle float32) Affine {
cos := float32(math.Cos(float64(angle)))
sin := float32(math.Sin(float64(angle)))
return Affine{A: cos, B: -sin, C: 0, D: sin, E: cos, F: 0}
}
func ScaleAffine
ScaleAffine creates a scaling transformation.
func ScaleAffine(x, y float32) Affine {
return Affine{A: x, B: 0, C: 0, D: 0, E: y, F: 0}
}
func ShapeCount
ShapeCount returns the number of shapes (fills + strokes) encoded.
func (e *Encoding) ShapeCount() int {
return e.shapeCount
}
func Size
Size returns the approximate memory size in bytes.
func (e *Encoding) Size() int {
return len(e.tags) +
len(e.pathData)*4 +
len(e.drawData)*4 +
len(e.textData) +
len(e.transforms)*24 + // 6 float32 per Affine
len(e.brushes)*20 // approximate brush size
}
func SolidBrush
SolidBrush creates a solid color brush.
func SolidBrush(c gg.RGBA) Brush {
return Brush{
Kind: BrushSolid,
Color: c,
}
}
func String
String returns a human-readable name for the blend mode.
func (mode BlendMode) String() string {
switch mode {
// Standard blend modes
case BlendNormal:
return "Normal"
case BlendMultiply:
return "Multiply"
case BlendScreen:
return "Screen"
case BlendOverlay:
return "Overlay"
case BlendDarken:
return "Darken"
case BlendLighten:
return "Lighten"
case BlendColorDodge:
return "ColorDodge"
case BlendColorBurn:
return "ColorBurn"
case BlendHardLight:
return "HardLight"
case BlendSoftLight:
return "SoftLight"
case BlendDifference:
return "Difference"
case BlendExclusion:
return "Exclusion"
// HSL blend modes
case BlendHue:
return "Hue"
case BlendSaturation:
return "Saturation"
case BlendColor:
return "Color"
case BlendLuminosity:
return "Luminosity"
// Porter-Duff modes
case BlendClear:
return "Clear"
case BlendCopy:
return "Copy"
case BlendDestination:
return "Destination"
case BlendSourceOver:
return "SourceOver"
case BlendDestinationOver:
return "DestinationOver"
case BlendSourceIn:
return "SourceIn"
case BlendDestinationIn:
return "DestinationIn"
case BlendSourceOut:
return "SourceOut"
case BlendDestinationOut:
return "DestinationOut"
case BlendSourceAtop:
return "SourceAtop"
case BlendDestinationAtop:
return "DestinationAtop"
case BlendXor:
return "Xor"
case BlendPlus:
return "Plus"
default:
return unknownStr
}
}
func Tags
Tags returns the tag stream (read-only access for iteration).
func (e *Encoding) Tags() []Tag {
return e.tags
}
func TextData
TextData returns the text data stream.
func (e *Encoding) TextData() []byte {
return e.textData
}
func TransformPoint
TransformPoint transforms a point by the affine matrix.
func (a Affine) TransformPoint(x, y float32) (float32, float32) {
return a.A*x + a.B*y + a.C, a.D*x + a.E*y + a.F
}
func Transforms
Transforms returns the transform stream.
func (e *Encoding) Transforms() []Affine {
return e.transforms
}
func TranslateAffine
TranslateAffine creates a translation transformation.
func TranslateAffine(x, y float32) Affine {
return Affine{A: 1, B: 0, C: x, D: 0, E: 1, F: y}
}
func Union
Union returns the smallest rectangle containing both r and other.
func (r Rect) Union(other Rect) Rect {
return Rect{
MinX: min32(r.MinX, other.MinX),
MinY: min32(r.MinY, other.MinY),
MaxX: max32(r.MaxX, other.MaxX),
MaxY: max32(r.MaxY, other.MaxY),
}
}
func UnionPoint
UnionPoint expands the rectangle to include the point.
func (r Rect) UnionPoint(x, y float32) Rect {
return Rect{
MinX: min32(r.MinX, x),
MinY: min32(r.MinY, y),
MaxX: max32(r.MaxX, x),
MaxY: max32(r.MaxY, y),
}
}
func UpdateBounds
UpdateBounds expands the encoding's bounding box to include the given rect.
This is used to propagate transformed bounds from Scene to Encoding,
ensuring that the tile-based renderer's early-out intersection test
uses correct post-transform coordinates.
func (e *Encoding) UpdateBounds(bounds Rect) {
e.bounds = e.bounds.Union(bounds)
}
func Width
Width returns the width of the rectangle.
func (r Rect) Width() float32 {
if r.IsEmpty() {
return 0
}
return r.MaxX - r.MinX
}
Structs
type StrokeStyle struct
StrokeStyle contains stroke parameters.
type StrokeStyle struct {
Width float32
MiterLimit float32
Cap LineCap
Join LineJoin
}
type Brush struct
Brush represents a paint source for fill/stroke operations.
type Brush struct {
Kind BrushKind
Color gg.RGBA // For solid brushes
// Additional fields for gradients/images would go here
}
type Rect struct
Rect represents a bounding rectangle.
type Rect struct {
MinX, MinY float32
MaxX, MaxY float32
}
type Affine struct
Affine represents a 2D affine transformation matrix.
The matrix is stored in row-major order as:
| A B C |
| D E F |
Where a point (x, y) is transformed to:
x' = A*x + B*y + C
y' = D*x + E*y + F
type Affine struct {
A, B, C float32
D, E, F float32
}
type GlyphRunData struct
GlyphRunData is the header for a TagText scene encoding element.
Followed by GlyphCount × GlyphEntry, then TextLen bytes of UTF-8 text.
type GlyphRunData struct {
FontSourceID uint64
FontSize float32
GlyphCount uint16
Flags TextFlags
OriginX float32
OriginY float32
BrushIndex uint32
TextLen uint16
}
type GlyphEntry struct
GlyphEntry is a single positioned glyph in a text run (10 bytes).
type GlyphEntry struct {
GlyphID text.GlyphID // uint16
X float32
Y float32
}
type Encoding struct
Encoding holds the dual-stream encoded representation of drawing commands.
It uses separate streams for tags (1 byte each), path data, draw data,
and transforms to maximize cache efficiency and GPU compatibility.
type Encoding struct {
// tags is the command stream (1 byte per command)
tags []Tag
// pathData holds coordinate data for path commands (float32)
// MoveTo: 2, LineTo: 2, QuadTo: 4, CubicTo: 6
pathData []float32
// drawData holds draw command parameters (uint32)
// Fill: brush index, fill style
// Stroke: brush index, then style params
drawData []uint32
// transforms holds affine transformation matrices
transforms []Affine
// textData holds serialized GlyphRunData + GlyphEntry arrays for TagText commands.
// Separate stream from pathData — text data has different layout and lifetime.
textData []byte
// brushes holds brush definitions referenced by draw commands
brushes []Brush
// commandBounds tracks per-draw-command bounding boxes (ADR-021).
// Parallel to shapeCount: commandBounds[i] = bounds of i-th shape command.
// Used by DamageTracker for frame-to-frame object diff.
commandBounds []Rect
// bounds tracks cumulative bounding box
bounds Rect
// pathBounds tracks current path's bounding box
pathBounds Rect
// statistics for debugging/profiling
pathCount int
shapeCount int
}
type Iterator struct
Iterator provides sequential access to encoded commands.
type Iterator struct {
enc *Encoding
tagIdx int
pathIdx int
drawIdx int
transIdx int
brushIdx int
}
AffineFromMatrix converts a gg.Matrix to an Affine.