diff --git a/go.mod b/go.mod index 545a90ca9..d1e4f5fed 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 - gioui.org/shader v1.0.6 + gioui.org/shader v1.0.7 github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 diff --git a/go.sum b/go.sum index 1407f1848..dea79ffe8 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8v gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc= gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= -gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y= -gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= +gioui.org/shader v1.0.7 h1:fDoor1Id/tRxoIpzBSAr5TBo6QfSkMTOmdbMEyWDgGE= +gioui.org/shader v1.0.7/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo= github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI= diff --git a/gpu/gpu.go b/gpu/gpu.go index ab850fa41..3b7198f34 100644 --- a/gpu/gpu.go +++ b/gpu/gpu.go @@ -68,22 +68,40 @@ type renderer struct { pather *pather packer packer intersections packer + layers packer + layerFBOs fboSet } type drawOps struct { - profile bool - reader ops.Reader - states []f32.Affine2D - transStack []f32.Affine2D - vertCache []byte - viewport image.Point - clear bool - clearColor f32color.RGBA - imageOps []imageOp - pathOps []*pathOp - pathOpCache []pathOp - qs quadSplitter - pathCache *opCache + profile bool + reader ops.Reader + states []f32.Affine2D + transStack []f32.Affine2D + layers []opacityLayer + opacityStack []int + vertCache []byte + viewport image.Point + clear bool + clearColor f32color.RGBA + imageOps []imageOp + pathOps []*pathOp + pathOpCache []pathOp + qs quadSplitter + pathCache *opCache +} + +type opacityLayer struct { + opacity float32 + parent int + // depth of the opacity stack. Layers of equal depth are + // independent and may be packed into one atlas. + depth int + // opStart and opEnd denote the range of drawOps.imageOps + // that belong to the layer. + opStart, opEnd int + // clip of the layer operations. + clip image.Rectangle + place placement } type drawState struct { @@ -127,7 +145,12 @@ type imageOp struct { clip image.Rectangle material material clipType clipType - place placement + // place is either a placement in the path fbos or intersection fbos, + // depending on clipType. + place placement + // layerOps is the number of operations this + // operation replaces. + layerOps int } func decodeStrokeOp(data []byte) float32 { @@ -154,10 +177,12 @@ type material struct { // For materialTypeColor. color f32color.RGBA // For materialTypeLinearGradient. - color1 f32color.RGBA - color2 f32color.RGBA + color1 f32color.RGBA + color2 f32color.RGBA + opacity float32 // For materialTypeTexture. data imageOpData + tex driver.Texture uvTrans f32.Affine2D } @@ -222,8 +247,6 @@ func decodeLinearGradientOp(data []byte) linearGradientOpData { } } -type clipType uint8 - type resource interface { release() } @@ -273,6 +296,8 @@ type blitUniforms struct { transform [4]float32 uvTransformR1 [4]float32 uvTransformR2 [4]float32 + opacity float32 + _ [3]float32 } type colorUniforms struct { @@ -284,7 +309,7 @@ type gradientUniforms struct { color2 f32color.RGBA } -type materialType uint8 +type clipType uint8 const ( clipTypeNone clipType = iota @@ -292,6 +317,8 @@ const ( clipTypeIntersection ) +type materialType uint8 + const ( materialColor materialType = iota materialLinearGradient @@ -391,6 +418,8 @@ func (g *gpu) frame(target RenderTarget) error { g.coverTimer.begin() g.renderer.uploadImages(g.cache, g.drawOps.imageOps) g.renderer.prepareDrawOps(g.cache, g.drawOps.imageOps) + g.drawOps.layers = g.renderer.packLayers(g.drawOps.layers) + g.renderer.drawLayers(g.cache, g.drawOps.layers, g.drawOps.imageOps) d := driver.LoadDesc{ ClearColor: g.drawOps.clearColor, } @@ -400,7 +429,7 @@ func (g *gpu) frame(target RenderTarget) error { } g.ctx.BeginRenderPass(defFBO, d) g.ctx.Viewport(0, 0, viewport.X, viewport.Y) - g.renderer.drawOps(g.cache, g.drawOps.imageOps) + g.renderer.drawOps(g.cache, image.Point{}, g.renderer.blitter.viewport, g.drawOps.imageOps) g.coverTimer.end() g.ctx.EndRenderPass() g.cleanupTimer.begin() @@ -464,15 +493,18 @@ func newRenderer(ctx driver.Device) *renderer { if cap := 8192; maxDim > cap { maxDim = cap } + d := image.Pt(maxDim, maxDim) - r.packer.maxDims = image.Pt(maxDim, maxDim) - r.intersections.maxDims = image.Pt(maxDim, maxDim) + r.packer.maxDims = d + r.intersections.maxDims = d + r.layers.maxDims = d return r } func (r *renderer) release() { r.pather.release() r.blitter.release() + r.layerFBOs.delete(r.ctx, 0) } func newBlitter(ctx driver.Device) *blitter { @@ -747,8 +779,7 @@ func (r *renderer) packStencils(pops *[]*pathOp) { ops = ops[:len(ops)-1] continue } - sz := image.Point{X: p.clip.Dx(), Y: p.clip.Dy()} - place, ok := r.packer.add(sz) + place, ok := r.packer.add(p.clip.Size()) if !ok { // The clip area is at most the entire screen. Hopefully no // screen is larger than GL_MAX_TEXTURE_SIZE. @@ -760,6 +791,83 @@ func (r *renderer) packStencils(pops *[]*pathOp) { *pops = ops } +func (r *renderer) packLayers(layers []opacityLayer) []opacityLayer { + // Make every layer bounds contain nested layers; cull empty layers. + for i := len(layers) - 1; i >= 0; i-- { + l := layers[i] + if l.parent != -1 { + b := layers[l.parent].clip + layers[l.parent].clip = b.Union(l.clip) + } + if l.clip.Empty() { + layers = append(layers[:i], layers[i+1:]...) + } + } + // Pack layers. + r.layers.clear() + depth := 0 + for i := range layers { + l := &layers[i] + // Only layers of the same depth may be packed together. + if l.depth != depth { + r.layers.newPage() + } + place, ok := r.layers.add(l.clip.Size()) + if !ok { + // The layer area is at most the entire screen. Hopefully no + // screen is larger than GL_MAX_TEXTURE_SIZE. + panic(fmt.Errorf("layer size %v is larger than maximum texture size %v", l.clip.Size(), r.layers.maxDims)) + } + l.place = place + } + return layers +} + +func (r *renderer) drawLayers(cache *resourceCache, layers []opacityLayer, ops []imageOp) { + if len(r.layers.sizes) == 0 { + return + } + fbo := -1 + r.layerFBOs.resize(r.ctx, driver.TextureFormatSRGBA, r.layers.sizes) + for i := len(layers) - 1; i >= 0; i-- { + l := layers[i] + if fbo != l.place.Idx { + if fbo != -1 { + r.ctx.EndRenderPass() + r.ctx.PrepareTexture(r.layerFBOs.fbos[fbo].tex) + } + fbo = l.place.Idx + f := r.layerFBOs.fbos[fbo] + r.ctx.BeginRenderPass(f.tex, driver.LoadDesc{Action: driver.LoadActionClear}) + } + v := image.Rectangle{ + Min: l.place.Pos, + Max: l.place.Pos.Add(l.clip.Size()), + } + r.ctx.Viewport(v.Min.X, v.Min.Y, v.Max.X, v.Max.Y) + f := r.layerFBOs.fbos[fbo] + r.drawOps(cache, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd]) + sr := f32.FRect(v) + uvScale, uvOffset := texSpaceTransform(sr, f.size) + uvTrans := f32.Affine2D{}.Scale(f32.Point{}, uvScale).Offset(uvOffset) + // Replace layer ops with one textured op. + ops[l.opStart] = imageOp{ + clip: l.clip, + material: material{ + material: materialTexture, + tex: f.tex, + uvTrans: uvTrans, + opacity: l.opacity, + }, + layerOps: l.opEnd - l.opStart - 1, + } + } + if fbo != -1 { + r.ctx.EndRenderPass() + r.ctx.PrepareTexture(r.layerFBOs.fbos[fbo].tex) + } +} + func (d *drawOps) reset(viewport image.Point) { d.profile = false d.viewport = viewport @@ -768,6 +876,8 @@ func (d *drawOps) reset(viewport image.Point) { d.pathOpCache = d.pathOpCache[:0] d.vertCache = d.vertCache[:0] d.transStack = d.transStack[:0] + d.layers = d.layers[:0] + d.opacityStack = d.opacityStack[:0] } func (d *drawOps) collect(root *op.Ops, viewport image.Point) { @@ -866,6 +976,27 @@ loop: state.t = d.transStack[n-1] d.transStack = d.transStack[:n-1] + case ops.TypePushOpacity: + opacity := ops.DecodeOpacity(encOp.Data) + parent := -1 + depth := len(d.opacityStack) + if depth > 0 { + parent = d.opacityStack[depth-1] + } + lidx := len(d.layers) + d.layers = append(d.layers, opacityLayer{ + opacity: opacity, + parent: parent, + depth: depth, + opStart: len(d.imageOps), + }) + d.opacityStack = append(d.opacityStack, lidx) + case ops.TypePopOpacity: + n := len(d.opacityStack) + idx := d.opacityStack[n-1] + d.layers[idx].opEnd = len(d.imageOps) + d.opacityStack = d.opacityStack[:n-1] + case ops.TypeStroke: quads.key.strokeWidth = decodeStrokeOp(encOp.Data) @@ -958,7 +1089,7 @@ loop: mat := state.materialFor(bnd, off, partialTrans, bounds) rect := state.cpath == nil || state.cpath.rect - if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && rect && mat.opaque && (mat.material == materialColor) { + if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && rect && mat.opaque && (mat.material == materialColor) && len(d.opacityStack) == 0 { // The image is a uniform opaque color and takes up the whole screen. // Scrap images up to and including this image and set clear color. d.imageOps = d.imageOps[:0] @@ -971,6 +1102,15 @@ loop: clip: bounds, material: mat, } + if n := len(d.opacityStack); n > 0 { + idx := d.opacityStack[n-1] + lb := d.layers[idx].clip + if lb.Empty() { + d.layers[idx].clip = img.clip + } else { + d.layers[idx].clip = lb.Union(img.clip) + } + } d.imageOps = append(d.imageOps, img) if clipData != nil { @@ -1000,7 +1140,9 @@ func expandPathOp(p *pathOp, clip image.Rectangle) { } func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32.Affine2D, clip image.Rectangle) material { - var m material + m := material{ + opacity: 1., + } switch d.matType { case materialColor: m.material = materialColor @@ -1040,10 +1182,11 @@ func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32 } func (r *renderer) uploadImages(cache *resourceCache, ops []imageOp) { - for _, img := range ops { + for i := range ops { + img := &ops[i] m := img.material if m.material == materialTexture { - r.texHandle(cache, m.data) + img.material.tex = r.texHandle(cache, m.data) } } } @@ -1053,10 +1196,10 @@ func (r *renderer) prepareDrawOps(cache *resourceCache, ops []imageOp) { m := img.material switch m.material { case materialTexture: - r.ctx.PrepareTexture(r.texHandle(cache, m.data)) + r.ctx.PrepareTexture(m.tex) } - var fbo stencilFBO + var fbo FBO switch img.clipType { case clipTypeNone: continue @@ -1069,24 +1212,26 @@ func (r *renderer) prepareDrawOps(cache *resourceCache, ops []imageOp) { } } -func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) { +func (r *renderer) drawOps(cache *resourceCache, opOff image.Point, viewport image.Point, ops []imageOp) { var coverTex driver.Texture - for _, img := range ops { + for i := 0; i < len(ops); i++ { + img := ops[i] + i += img.layerOps m := img.material switch m.material { case materialTexture: - r.ctx.BindTexture(0, r.texHandle(cache, m.data)) + r.ctx.BindTexture(0, m.tex) } - drc := img.clip + drc := img.clip.Add(opOff) - scale, off := clipSpaceTransform(drc, r.blitter.viewport) - var fbo stencilFBO + scale, off := clipSpaceTransform(drc, viewport) + var fbo FBO switch img.clipType { case clipTypeNone: p := r.blitter.pipelines[m.material] r.ctx.BindPipeline(p.pipeline) r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0) - r.blitter.blit(m.material, m.color, m.color1, m.color2, scale, off, m.uvTrans) + r.blitter.blit(m.material, m.color, m.color1, m.color2, scale, off, m.opacity, m.uvTrans) continue case clipTypePath: fbo = r.pather.stenciler.cover(img.place.Idx) @@ -1109,7 +1254,7 @@ func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) { } } -func (b *blitter) blit(mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D) { +func (b *blitter) blit(mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, opacity float32, uvTrans f32.Affine2D) { p := b.pipelines[mat] b.ctx.BindPipeline(p.pipeline) var uniforms *blitUniforms @@ -1119,18 +1264,19 @@ func (b *blitter) blit(mat materialType, col f32color.RGBA, col1, col2 f32color. uniforms = &b.colUniforms.blitUniforms case materialTexture: t1, t2, t3, t4, t5, t6 := uvTrans.Elems() - b.texUniforms.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0} - b.texUniforms.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0} uniforms = &b.texUniforms.blitUniforms + uniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0} + uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0} case materialLinearGradient: b.linearGradientUniforms.color1 = col1 b.linearGradientUniforms.color2 = col2 t1, t2, t3, t4, t5, t6 := uvTrans.Elems() - b.linearGradientUniforms.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0} - b.linearGradientUniforms.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0} uniforms = &b.linearGradientUniforms.blitUniforms + uniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0} + uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0} } + uniforms.opacity = opacity uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y} p.UploadUniforms(b.ctx) b.ctx.DrawArrays(0, 4) diff --git a/gpu/internal/rendertest/refs/TestOpacity.png b/gpu/internal/rendertest/refs/TestOpacity.png new file mode 100644 index 000000000..7ddf84ea5 Binary files /dev/null and b/gpu/internal/rendertest/refs/TestOpacity.png differ diff --git a/gpu/internal/rendertest/render_test.go b/gpu/internal/rendertest/render_test.go index f3de925e3..009002045 100644 --- a/gpu/internal/rendertest/render_test.go +++ b/gpu/internal/rendertest/render_test.go @@ -413,6 +413,22 @@ func TestGapsInPath(t *testing.T) { }) } +func TestOpacity(t *testing.T) { + run(t, func(ops *op.Ops) { + opc1 := paint.PushOpacity(ops, .3) + // Fill screen to exercize the glClear optimization. + paint.FillShape(ops, color.NRGBA{R: 255, A: 255}, clip.Rect{Max: image.Pt(1024, 1024)}.Op()) + opc2 := paint.PushOpacity(ops, .6) + paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(20, 10), Max: image.Pt(64, 128)}.Op()) + opc2.Pop() + opc1.Pop() + opc3 := paint.PushOpacity(ops, .6) + paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(50+20, 10), Max: image.Pt(50+64, 128)}.Op()) + opc3.Pop() + }, func(r result) { + }) +} + // lerp calculates linear interpolation with color b and p. func lerp(a, b f32color.RGBA, p float32) f32color.RGBA { return f32color.RGBA{ diff --git a/gpu/path.go b/gpu/path.go index 79248fd64..b92c4163c 100644 --- a/gpu/path.go +++ b/gpu/path.go @@ -90,10 +90,10 @@ type intersectUniforms struct { } type fboSet struct { - fbos []stencilFBO + fbos []FBO } -type stencilFBO struct { +type FBO struct { size image.Point tex driver.Texture } @@ -247,10 +247,10 @@ func newStenciler(ctx driver.Device) *stenciler { return st } -func (s *fboSet) resize(ctx driver.Device, sizes []image.Point) { +func (s *fboSet) resize(ctx driver.Device, format driver.TextureFormat, sizes []image.Point) { // Add fbos. for i := len(s.fbos); i < len(sizes); i++ { - s.fbos = append(s.fbos, stencilFBO{}) + s.fbos = append(s.fbos, FBO{}) } // Resize fbos. for i, sz := range sizes { @@ -273,7 +273,7 @@ func (s *fboSet) resize(ctx driver.Device, sizes []image.Point) { if sz.X > max { sz.X = max } - tex, err := ctx.NewTexture(driver.TextureFormatFloat, sz.X, sz.Y, driver.FilterNearest, driver.FilterNearest, + tex, err := ctx.NewTexture(format, sz.X, sz.Y, driver.FilterNearest, driver.FilterNearest, driver.BufferBindingTexture|driver.BufferBindingFramebuffer) if err != nil { panic(err) @@ -340,15 +340,15 @@ func (s *stenciler) beginIntersect(sizes []image.Point) { // 8 bit coverage is enough, but OpenGL ES only supports single channel // floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if // no floating point support is available. - s.intersections.resize(s.ctx, sizes) + s.intersections.resize(s.ctx, driver.TextureFormatFloat, sizes) } -func (s *stenciler) cover(idx int) stencilFBO { +func (s *stenciler) cover(idx int) FBO { return s.fbos.fbos[idx] } func (s *stenciler) begin(sizes []image.Point) { - s.fbos.resize(s.ctx, sizes) + s.fbos.resize(s.ctx, driver.TextureFormatFloat, sizes) } func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data pathData) { diff --git a/internal/ops/ops.go b/internal/ops/ops.go index e5349aeb6..2d69bca84 100644 --- a/internal/ops/ops.go +++ b/internal/ops/ops.go @@ -53,6 +53,8 @@ const ( TypeDefer TypeTransform TypePopTransform + TypePushOpacity + TypePopOpacity TypeInvalidate TypeImage TypePaint @@ -121,6 +123,7 @@ const ( ClipStack StackKind = iota TransStack PassStack + OpacityStack _StackKind ) @@ -136,6 +139,8 @@ const ( TypeDeferLen = 1 TypeTransformLen = 1 + 1 + 4*6 TypePopTransformLen = 1 + TypePushOpacityLen = 1 + 4 + TypePopOpacityLen = 1 TypeRedrawLen = 1 + 8 TypeImageLen = 1 TypePaintLen = 1 @@ -381,6 +386,14 @@ func DecodeTransform(data []byte) (t f32.Affine2D, push bool) { return f32.NewAffine2D(a, b, c, d, e, f), push } +func DecodeOpacity(data []byte) float32 { + if OpType(data[0]) != TypePushOpacity { + panic("invalid op") + } + bo := binary.LittleEndian + return math.Float32frombits(bo.Uint32(data[1:])) +} + // DecodeSave decodes the state id of a save op. func DecodeSave(data []byte) int { if OpType(data[0]) != TypeSave { @@ -410,6 +423,8 @@ var opProps = [0x100]opProp{ TypeDefer: {Size: TypeDeferLen, NumRefs: 0}, TypeTransform: {Size: TypeTransformLen, NumRefs: 0}, TypePopTransform: {Size: TypePopTransformLen, NumRefs: 0}, + TypePushOpacity: {Size: TypePushOpacityLen, NumRefs: 0}, + TypePopOpacity: {Size: TypePopOpacityLen, NumRefs: 0}, TypeInvalidate: {Size: TypeRedrawLen, NumRefs: 0}, TypeImage: {Size: TypeImageLen, NumRefs: 2}, TypePaint: {Size: TypePaintLen, NumRefs: 0}, @@ -470,6 +485,10 @@ func (t OpType) String() string { return "Transform" case TypePopTransform: return "PopTransform" + case TypePushOpacity: + return "PushOpacity" + case TypePopOpacity: + return "PopOpacity" case TypeInvalidate: return "Invalidate" case TypeImage: diff --git a/op/paint/paint.go b/op/paint/paint.go index 1c992973a..34ed974ba 100644 --- a/op/paint/paint.go +++ b/op/paint/paint.go @@ -44,6 +44,14 @@ type LinearGradientOp struct { type PaintOp struct { } +// OpacityStack represents an opacity applied to all painting operations +// until Pop is called. +type OpacityStack struct { + id ops.StackID + macroID int + ops *ops.Ops +} + // NewImageOp creates an ImageOp backed by src. // // NewImageOp assumes the backing image is immutable, and may cache a @@ -145,3 +153,25 @@ func Fill(ops *op.Ops, c color.NRGBA) { ColorOp{Color: c}.Add(ops) PaintOp{}.Add(ops) } + +// PushOpacity creates a drawing layer with an opacity in the range [0;1]. +// The layer includes every subsequent drawing operation until [OpacityStack.Pop] +// is called. +// +// The layer is drawn in two steps. First, the layer operations are +// drawn to a separate image. Then, the image is blended on top of +// the frame, with the opacity used as the blending factor. +func PushOpacity(o *op.Ops, opacity float32) OpacityStack { + id, macroID := ops.PushOp(&o.Internal, ops.OpacityStack) + data := ops.Write(&o.Internal, ops.TypePushOpacityLen) + bo := binary.LittleEndian + data[0] = byte(ops.TypePushOpacity) + bo.PutUint32(data[1:], math.Float32bits(opacity)) + return OpacityStack{ops: &o.Internal, id: id, macroID: macroID} +} + +func (t OpacityStack) Pop() { + ops.PopOp(t.ops, ops.OpacityStack, t.id, t.macroID) + data := ops.Write(t.ops, ops.TypePopOpacityLen) + data[0] = byte(ops.TypePopOpacity) +}