scene/scene.go
Functions
func AntiAlias
func (s *Scene) AntiAlias() bool {
return s.antiAlias
}
func Append
Append merges another Scene into this one, combining encodings, image
registries, and bounds. Image indices in the appended scene's encoding
are adjusted to account for images already registered in this scene.
This is the Scene-level equivalent of Flutter's SceneBuilder.addPicture() —
composing cached display lists from multiple RepaintBoundary nodes into a
single scene for rendering.
The other scene is flattened (layers collapsed) before appending.
func (s *Scene) Append(other *Scene) {
if other == nil || other.IsEmpty() {
return
}
otherEnc := other.Encoding()
//nolint:gosec // image registry length is bounded
imageOffset := uint32(len(s.imageRegistry))
s.currentEncoding().AppendWithImages(otherEnc, imageOffset)
s.imageRegistry = append(s.imageRegistry, other.imageRegistry...)
for id, src := range other.fontRegistry {
s.fontRegistry[id] = src
}
s.bounds = s.bounds.Union(other.Bounds())
s.version++
}
func AppendWithTranslation
AppendWithTranslation merges another scene into this one, offsetting all
coordinates by (dx, dy). Image indices are adjusted to avoid conflicts.
This is the Vello-derived pattern for composing child scenes recorded at
local (0,0) into a parent at a specific position. Used by RepaintBoundary
scene composition (ADR-007 Phase 5).
func (s *Scene) AppendWithTranslation(other *Scene, dx, dy float32) {
if other == nil || other.IsEmpty() {
return
}
otherEnc := other.Encoding()
//nolint:gosec // image registry length is bounded
imageOffset := uint32(len(s.imageRegistry))
s.currentEncoding().AppendWithTranslation(otherEnc, dx, dy, imageOffset)
s.imageRegistry = append(s.imageRegistry, other.imageRegistry...)
for id, src := range other.fontRegistry {
s.fontRegistry[id] = src
}
ob := other.Bounds()
ob.MinX += dx
ob.MinY += dy
ob.MaxX += dx
ob.MaxY += dy
s.bounds = s.bounds.Union(ob)
s.version++
}
func Bounds
Bounds returns the image bounds as a Rect.
func (img *Image) Bounds() Rect {
return Rect{
MinX: 0,
MinY: 0,
MaxX: float32(img.Width),
MaxY: float32(img.Height),
}
}
func Bounds
Bounds returns the cumulative bounding box of all scene content.
func (s *Scene) Bounds() Rect {
return s.bounds
}
func ClipBounds
ClipBounds returns the intersection of all active clip regions.
func (s *Scene) ClipBounds() Rect {
return s.clipStack.CombinedBounds()
}
func ClipDepth
ClipDepth returns the current clip stack depth.
func (s *Scene) ClipDepth() int {
return s.clipStack.Depth()
}
func DrawImage
DrawImage draws an image at the given transform.
func (s *Scene) DrawImage(img *Image, transform Affine) {
if img == nil {
return
}
// Register image and get index
imageIdx := s.registerImage(img)
// Combine with current transform
combinedTransform := s.currentTransform.Multiply(transform)
// Get the current layer's encoding
enc := s.currentEncoding()
// Encode image command
//nolint:gosec // image index is bounded by slice length
enc.EncodeImage(uint32(imageIdx), combinedTransform)
// Update bounds
imgBounds := Rect{
MinX: 0,
MinY: 0,
MaxX: float32(img.Width),
MaxY: float32(img.Height),
}
if !combinedTransform.IsIdentity() {
imgBounds = transformBounds(imgBounds, combinedTransform)
}
s.bounds = s.bounds.Union(imgBounds)
enc.UpdateBounds(imgBounds)
enc.RecordCommandBounds(imgBounds)
// Update layer bounds
s.layerStack.Top().UpdateBounds(imgBounds)
s.version++
}
func Encoding
Encoding returns the root encoding containing all scene commands.
This encoding can be used for rendering or caching.
func (s *Scene) Encoding() *Encoding {
// Flatten all layers into root encoding
s.flattenLayers()
return s.encoding
}
func Fill
Fill fills a shape with the given style, transform, and brush.
func (s *Scene) Fill(style FillStyle, transform Affine, brush Brush, shape Shape) {
if shape == nil {
return
}
// Combine with current transform
combinedTransform := s.currentTransform.Multiply(transform)
// Get the current layer's encoding
enc := s.currentEncoding()
// Emit AA state change if needed (delta encoding).
s.emitAntiAliasIfNeeded(enc)
// Optimization: RoundRectShape uses dedicated SDF encoding
// which bypasses path construction entirely.
if rr, ok := shape.(*RoundRectShape); ok {
if !combinedTransform.IsIdentity() {
enc.EncodeTransform(combinedTransform)
}
enc.EncodeFillRoundRect(brush, style, rr.Rect, rr.RadiusX, rr.RadiusY)
shapeBounds := rr.Rect
if !combinedTransform.IsIdentity() {
shapeBounds = transformBounds(shapeBounds, combinedTransform)
}
s.bounds = s.bounds.Union(shapeBounds)
enc.UpdateBounds(shapeBounds)
enc.RecordCommandBounds(shapeBounds)
s.layerStack.Top().UpdateBounds(shapeBounds)
s.version++
return
}
// Encode transform if not identity
if !combinedTransform.IsIdentity() {
enc.EncodeTransform(combinedTransform)
}
// Convert shape to path and encode — use pooled path when possible.
path := s.shapeToPath(shape)
if path == nil || path.IsEmpty() {
return
}
s.encodeScenePath(enc, path)
s.pathPool.Put(path)
// Encode fill command
enc.EncodeFill(brush, style)
// Update bounds
shapeBounds := shape.Bounds()
if !combinedTransform.IsIdentity() {
shapeBounds = transformBounds(shapeBounds, combinedTransform)
}
s.bounds = s.bounds.Union(shapeBounds)
enc.UpdateBounds(shapeBounds)
enc.RecordCommandBounds(shapeBounds)
// Update layer bounds
s.layerStack.Top().UpdateBounds(shapeBounds)
s.version++
}
func Flatten
Flatten composites all layers into a single encoding.
This is useful for rendering the complete scene as a single
flattened representation.
Unlike Encoding(), this method returns a new cloned encoding
that is independent of the scene's internal state. This allows
the scene to be modified after calling Flatten() without
affecting the returned encoding.
The flattening process:
1. All open layers are popped and composited
2. Layer blend modes and alpha values are applied
3. The result is a single encoding with all content merged
Example:
scene := NewScene()
scene.Fill(FillNonZero, IdentityAffine(), SolidBrush(gg.Red), rect)
scene.PushLayer(BlendMultiply, 0.5, nil)
scene.Fill(FillNonZero, IdentityAffine(), SolidBrush(gg.Blue), circle)
flat := scene.Flatten()
// flat contains both shapes with layer compositing applied
func (s *Scene) Flatten() *Encoding {
// First ensure all layers are flattened
s.flattenLayers()
// Return a clone so caller can't affect internal state
return s.encoding.Clone()
}
func FontRegistry
FontRegistry returns the scene's font registry for render-time font lookup.
func (s *Scene) FontRegistry() map[uint64]*text.FontSource {
return s.fontRegistry
}
func Get
Get retrieves a scene from the pool or creates a new one.
func (sp *ScenePool) Get() *Scene {
if len(sp.scenes) > 0 {
scene := sp.scenes[len(sp.scenes)-1]
sp.scenes = sp.scenes[:len(sp.scenes)-1]
scene.Reset()
return scene
}
return NewScene()
}
func Images
Images returns all registered images.
func (s *Scene) Images() []*Image {
return s.imageRegistry
}
func IsEmpty
IsEmpty returns true if the image has no dimensions.
func (img *Image) IsEmpty() bool {
return img.Width <= 0 || img.Height <= 0
}
func IsEmpty
IsEmpty returns true if the scene has no content.
func (s *Scene) IsEmpty() bool {
return s.encoding.IsEmpty() && s.layerStack.Root().IsEmpty()
}
func LayerDepth
LayerDepth returns the current layer stack depth.
func (s *Scene) LayerDepth() int {
return s.layerStack.Depth()
}
func NewImage
NewImage creates a new image with the given dimensions.
func NewImage(width, height int) *Image {
return &Image{
Width: width,
Height: height,
}
}
func NewScene
NewScene creates a new empty scene.
func NewScene() *Scene {
return &Scene{
encoding: NewEncoding(),
layerStack: NewLayerStack(),
transformStack: make([]Affine, 0, 8),
clipStack: NewClipStack(),
version: 0,
bounds: EmptyRect(),
currentTransform: IdentityAffine(),
antiAlias: true,
lastEncodedAA: true,
imageRegistry: make([]*Image, 0, 8),
fontRegistry: make(map[uint64]*text.FontSource, 4),
}
}
func NewScenePool
NewScenePool creates a new scene pool.
func NewScenePool() *ScenePool {
return &ScenePool{
scenes: make([]*Scene, 0, 4),
}
}
func PopClip
PopClip pops the current clip region.
Returns false if there's no clip to pop.
func (s *Scene) PopClip() bool {
if s.clipStack.IsEmpty() {
return false
}
s.clipStack.Pop()
// Encode end clip
enc := s.currentEncoding()
enc.EncodeEndClip()
s.version++
return true
}
func PopLayer
PopLayer pops the current layer and composites it with the content below.
Returns false if there's no layer to pop (only root layer remains).
func (s *Scene) PopLayer() bool {
if s.layerStack.IsRoot() {
return false
}
layer := s.layerStack.Pop()
if layer == nil {
return false
}
// Get parent encoding
parentEnc := s.currentEncoding()
// If layer had content, append it to parent
if layer.Encoding != nil && !layer.Encoding.IsEmpty() {
parentEnc.Append(layer.Encoding)
}
// If layer had a clip, end it
if layer.Clip != nil {
parentEnc.EncodeEndClip()
}
// Encode pop layer
parentEnc.EncodePopLayer()
// Update parent bounds
s.layerStack.Top().UpdateBounds(layer.Bounds)
// Return layer to pool
s.layerStack.ReleaseLayer(layer)
s.version++
return true
}
func PopTransform
PopTransform pops the current transform from the stack.
Returns false if there's no transform to pop.
func (s *Scene) PopTransform() bool {
if len(s.transformStack) == 0 {
return false
}
s.currentTransform = s.transformStack[len(s.transformStack)-1]
s.transformStack = s.transformStack[:len(s.transformStack)-1]
s.version++
return true
}
func PushClip
PushClip pushes a clip region. All subsequent drawing operations
will be clipped to this shape until PopClip is called.
func (s *Scene) PushClip(shape Shape) {
if shape == nil {
return
}
clipState := NewClipState(shape, s.currentTransform)
s.clipStack.Push(clipState)
// Encode clip in current layer
enc := s.currentEncoding()
path := s.shapeToPath(shape)
if path != nil && !path.IsEmpty() {
// Encode transform if not identity
if !s.currentTransform.IsIdentity() {
enc.EncodeTransform(s.currentTransform)
}
s.encodeScenePath(enc, path)
s.pathPool.Put(path)
enc.EncodeBeginClip()
}
s.version++
}
func PushLayer
PushLayer pushes a new compositing layer.
All subsequent drawing operations will be rendered to this layer.
Call PopLayer to composite the layer with the content below.
func (s *Scene) PushLayer(blend BlendMode, alpha float32, clip Shape) {
clampedAlpha := clampAlpha(alpha)
layer := s.layerStack.AcquireLayer()
layer.Kind = LayerRegular
layer.BlendMode = blend
layer.Alpha = clampedAlpha
layer.Clip = clip
layer.Transform = s.currentTransform
layer.ClipStackDepth = s.clipStack.Depth()
// Initialize layer encoding
if layer.Encoding == nil {
layer.Encoding = NewEncoding()
} else {
layer.Encoding.Reset()
}
// Encode push layer in parent
parentEnc := s.currentEncoding()
parentEnc.EncodePushLayer(blend, clampedAlpha)
// If there's a clip, encode it
if clip != nil {
path := s.shapeToPath(clip)
if path != nil && !path.IsEmpty() {
s.encodeScenePath(parentEnc, path)
s.pathPool.Put(path)
parentEnc.EncodeBeginClip()
}
}
s.layerStack.Push(layer)
s.version++
}
func PushTransform
PushTransform pushes a transform onto the transform stack.
The transform is concatenated with the current transform.
func (s *Scene) PushTransform(t Affine) {
s.transformStack = append(s.transformStack, s.currentTransform)
s.currentTransform = s.currentTransform.Multiply(t)
s.version++
}
func Put
Put returns a scene to the pool for reuse.
func (sp *ScenePool) Put(scene *Scene) {
if scene == nil {
return
}
sp.scenes = append(sp.scenes, scene)
}
func RegisterFont
RegisterFont registers a FontSource in the scene's font registry.
Returns the FontSourceID used to reference this font in TagText commands.
func (s *Scene) RegisterFont(source *text.FontSource) uint64 {
id := computeSceneTextFontID(source)
s.fontRegistry[id] = source
return id
}
func Reset
Reset clears the scene for reuse without deallocating memory.
func (s *Scene) Reset() {
s.encoding.Reset()
s.layerStack.Reset()
s.transformStack = s.transformStack[:0]
s.clipStack.Reset()
s.version++
s.bounds = EmptyRect()
s.currentTransform = IdentityAffine()
s.antiAlias = true
s.lastEncodedAA = true
s.imageRegistry = s.imageRegistry[:0]
clear(s.fontRegistry)
}
func Rotate
Rotate applies a rotation to the current transform (angle in radians).
func (s *Scene) Rotate(angle float32) {
s.currentTransform = s.currentTransform.Multiply(RotateAffine(angle))
s.version++
}
func Scale
Scale applies a scale to the current transform.
func (s *Scene) Scale(x, y float32) {
s.currentTransform = s.currentTransform.Multiply(ScaleAffine(x, y))
s.version++
}
func SetAntiAlias
SetAntiAlias enables or disables anti-aliasing for subsequent fill and stroke
operations. When disabled, shapes are rendered with binary (pixel-perfect)
coverage, producing crisp edges ideal for UI elements at exact pixel boundaries.
The state change is delta-encoded: TagSetAntiAlias is emitted into the encoding
only when the value differs from the previously encoded state. Default is true.
Reference: Skia SkPaint::setAntiAlias, Cairo cairo_set_antialias.
func (s *Scene) SetAntiAlias(enabled bool) {
s.antiAlias = enabled
s.version++
}
func SetTransform
SetTransform sets the current transform, replacing the existing one.
This does not affect the transform stack.
func (s *Scene) SetTransform(t Affine) {
s.currentTransform = t
s.version++
}
func Stroke
Stroke strokes a shape with the given style, transform, and brush.
func (s *Scene) Stroke(style *StrokeStyle, transform Affine, brush Brush, shape Shape) {
if shape == nil {
return
}
if style == nil {
style = DefaultStrokeStyle()
}
// Combine with current transform
combinedTransform := s.currentTransform.Multiply(transform)
// Get the current layer's encoding
enc := s.currentEncoding()
// Emit AA state change if needed (delta encoding).
s.emitAntiAliasIfNeeded(enc)
// Encode transform if not identity
if !combinedTransform.IsIdentity() {
enc.EncodeTransform(combinedTransform)
}
// Convert shape to path and encode — use pooled path when possible.
path := s.shapeToPath(shape)
if path == nil || path.IsEmpty() {
return
}
s.encodeScenePath(enc, path)
s.pathPool.Put(path)
// Encode stroke command
enc.EncodeStroke(brush, style)
// Update bounds (expand by stroke width)
shapeBounds := shape.Bounds()
halfWidth := style.Width / 2
shapeBounds.MinX -= halfWidth
shapeBounds.MinY -= halfWidth
shapeBounds.MaxX += halfWidth
shapeBounds.MaxY += halfWidth
if !combinedTransform.IsIdentity() {
shapeBounds = transformBounds(shapeBounds, combinedTransform)
}
s.bounds = s.bounds.Union(shapeBounds)
enc.UpdateBounds(shapeBounds)
enc.RecordCommandBounds(shapeBounds)
// Update layer bounds
s.layerStack.Top().UpdateBounds(shapeBounds)
s.version++
}
func TaggedBounds
TaggedBounds returns per-draw-command bounding boxes with stable IDs.
Used by DamageTracker (ADR-021) for frame-to-frame object diff.
func (s *Scene) TaggedBounds() []TaggedBounds {
return s.encoding.CommandBounds()
}
func Transform
Transform returns the current transform.
func (s *Scene) Transform() Affine {
return s.currentTransform
}
func TransformDepth
TransformDepth returns the current transform stack depth.
func (s *Scene) TransformDepth() int {
return len(s.transformStack)
}
func Translate
Translate applies a translation to the current transform.
func (s *Scene) Translate(x, y float32) {
s.currentTransform = s.currentTransform.Multiply(TranslateAffine(x, y))
s.version++
}
func Version
Version returns the scene version number.
This is incremented on each modification and can be used for cache invalidation.
func (s *Scene) Version() uint64 {
return s.version
}
func Warmup
Warmup pre-allocates scenes to avoid allocation during critical paths.
func (sp *ScenePool) Warmup(count int) {
scenes := make([]*Scene, count)
for i := 0; i < count; i++ {
scenes[i] = sp.Get()
}
for i := 0; i < count; i++ {
sp.Put(scenes[i])
}
}
Structs
type Scene struct
Scene is the main retained mode container for accumulating drawing operations.
It builds an Encoding that can be efficiently rendered or cached.
Scene provides a familiar drawing API with Fill, Stroke, layers, clips,
and transforms. All operations are recorded into an internal Encoding
for later playback or GPU submission.
Example:
scene := NewScene()
scene.Fill(FillNonZero, IdentityAffine(), SolidBrush(gg.Red), circle)
scene.PushLayer(BlendMultiply, 0.5, nil)
scene.Stroke(DefaultStrokeStyle(), IdentityAffine(), SolidBrush(gg.Blue), rect)
scene.PopLayer()
enc := scene.Encoding()
type Scene struct {
// encoding is the root encoding that accumulates all commands
encoding *Encoding
// layerStack manages the hierarchy of compositing layers
layerStack *LayerStack
// transformStack holds the transform state stack
transformStack []Affine
// clipStack holds the clip state stack
clipStack *ClipStack
// version is incremented on each modification for cache invalidation
version uint64
// bounds tracks the cumulative bounding box of all content
bounds Rect
// currentTransform is the current combined transform
currentTransform Affine
// antiAlias controls whether subsequent draws use anti-aliasing.
// Default: true. State changes are delta-encoded (TagSetAntiAlias emitted
// only when the value differs from lastEncodedAA).
antiAlias bool
// lastEncodedAA tracks the last AA state emitted into the encoding.
// Used for dirty tracking to avoid redundant TagSetAntiAlias tags.
lastEncodedAA bool
// imageRegistry maps image handles to indices
imageRegistry []*Image
// fontRegistry maps FontSourceID → *text.FontSource for TagText resolution.
// Populated at scene recording time, consumed at render/playback time.
fontRegistry map[uint64]*text.FontSource
// pathPool reuses Path objects to avoid per-shape allocations in Fill/Stroke.
// Each shape.ToPath() creates a new Path; pooling eliminates this overhead.
pathPool PathPool
}
type Image struct
Image represents an image resource for drawing.
This is a placeholder that will be expanded during integration.
type Image struct {
// Width is the image width in pixels
Width int
// Height is the image height in pixels
Height int
// Data holds the pixel data (RGBA format)
// This will be populated during integration phase
Data []byte
}
type ScenePool struct
ScenePool manages a pool of reusable Scene objects.
type ScenePool struct {
scenes []*Scene
}
AntiAlias returns whether anti-aliasing is enabled for subsequent draws.