gpu, op, internal/ops: add affine transformations

Add support for affine transformations. The key changes are outlined
below.

- Painting/clipping with rectangles is handled by, for complex
  transforms, creating clipping paths representing the transformed
  rectangle and using a larger bounding box. Cover/Blit shaders updated
  correspondingly to correctly map texture cordinates from the new
  bounding boxes.
- Since path splitting must happen on CPU the transforms must happen CPU
  side as well - offsets removed from shaders.
- Complex transforms will lead to different path splitting which means
  that GPU arrays can no longer be cached if the transform has changed.
  Thus the current transform is added as a key to the cache.
- Add a public API to op for setting Affine transformations.

There are a number of optimizations that could be explored further but
which are left out now:
- Caching also of CPU operations (e.g path splitting & transforms) and
  not only caching the GPU arrays.
- Allow for re-use of cached GPU vertices if the transformation change
  is a pure offset / scaling since the splitting is then the same.

Signed-off-by: Viktor <viktor.ogeman@gmail.com>
This commit is contained in:
Viktor
2020-06-20 23:29:51 +02:00
committed by Elias Naur
parent b247395c62
commit 24951a7ee7
9 changed files with 270 additions and 145 deletions
+145 -49
View File
@@ -61,10 +61,11 @@ type drawOps struct {
// zimageOps are the rectangle clipped opaque images
// that can use fast front-to-back rendering with z-test
// and no blending.
zimageOps []imageOp
pathOps []*pathOp
pathOpCache []pathOp
qs quadSplitter
zimageOps []imageOp
pathOps []*pathOp
pathOpCache []pathOp
qs quadSplitter
uniqueKeyCounter int
}
type drawState struct {
@@ -82,7 +83,6 @@ type drawState struct {
}
type pathOp struct {
off f32.Point
// clip is the union of all
// later clip rectangles.
clip image.Rectangle
@@ -96,7 +96,6 @@ type pathOp struct {
type imageOp struct {
z float32
path *pathOp
off f32.Point
clip image.Rectangle
material material
clipType clipType
@@ -109,9 +108,8 @@ type material struct {
// For materialTypeColor.
color f32color.RGBA
// For materialTypeTexture.
texture *texture
uvScale f32.Point
uvOffset f32.Point
texture *texture
uvTrans f32.Affine2D
}
// clipOp is the shadow of clip.Op.
@@ -227,7 +225,7 @@ type blitter struct {
type blitColUniforms struct {
vert struct {
blitUniforms
_ [10]byte // Padding to a multiple of 16.
_ [12]byte // Padding to a multiple of 16.
}
frag struct {
colorUniforms
@@ -237,7 +235,7 @@ type blitColUniforms struct {
type blitTexUniforms struct {
vert struct {
blitUniforms
_ [10]byte // Padding to a multiple of 16.
_ [12]byte // Padding to a multiple of 16.
}
}
@@ -253,9 +251,10 @@ type program struct {
}
type blitUniforms struct {
transform [4]float32
uvTransform [4]float32
z float32
transform [4]float32
uvTransformR1 [4]float32
uvTransformR2 [4]float32
z float32
}
type colorUniforms struct {
@@ -507,7 +506,7 @@ func (r *renderer) stencilClips(pathCache *opCache, ops []*pathOp) {
r.ctx.Clear(0.0, 0.0, 0.0, 0.0)
}
data, _ := pathCache.get(p.pathKey)
r.pather.stencilPath(p.clip, p.off, p.place.Pos, data.(*pathData))
r.pather.stencilPath(p.clip, p.place.Pos, data.(*pathData))
}
}
@@ -682,6 +681,28 @@ func (d *drawOps) newPathOp() *pathOp {
return &d.pathOpCache[len(d.pathOpCache)-1]
}
func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key) {
npath := d.newPathOp()
*npath = pathOp{
parent: state.cpath,
}
state.cpath = npath
if len(aux) > 0 {
state.rect = false
state.cpath.pathKey = auxKey
state.cpath.path = true
state.cpath.pathVerts = aux
d.pathOps = append(d.pathOps, state.cpath)
}
}
// noCacheKey creates a new key for caches, but one that is unique and
// thus will never lead to re-use.
func (d *drawOps) noCacheKey() ops.Key {
d.uniqueKeyCounter--
return d.reader.NewKey(d.uniqueKeyCounter)
}
func (d *drawOps) collectOps(r *ops.Reader, state drawState) int {
var aux []byte
var auxKey ops.Key
@@ -699,32 +720,23 @@ loop:
case opconst.TypeClip:
var op clipOp
op.decode(encOp.Data)
off := state.t.Transform(f32.Point{})
bounds := op.bounds
if len(aux) > 0 {
// there is a clipping path, bounds is not filled before for performance
aux, bounds = d.buildVerts(aux)
// There is a clipping path, build the gpu data and update the
// cache key such that it will be equal only if the transform is the
// same also.
aux, op.bounds = d.buildVerts(aux, state.t)
auxKey = auxKey.SetTransform(state.t)
} else {
aux, op.bounds, _ = d.boundsForTransformedRect(bounds, state.t)
auxKey = d.noCacheKey()
}
state.clip = state.clip.Intersect(bounds.Add(off))
state.clip = state.clip.Intersect(op.bounds)
if state.clip.Empty() {
continue
}
npath := d.newPathOp()
*npath = pathOp{
parent: state.cpath,
off: off,
}
state.cpath = npath
if len(aux) > 0 {
state.rect = false
state.cpath.pathKey = auxKey
state.cpath.path = true
state.cpath.pathVerts = aux
d.pathOps = append(d.pathOps, state.cpath)
}
d.addClipPath(&state, aux, auxKey)
aux = nil
auxKey = ops.Key{}
case opconst.TypeColor:
@@ -735,13 +747,25 @@ loop:
state.image = decodeImageOp(encOp.Data, encOp.Refs)
case opconst.TypePaint:
op := decodePaintOp(encOp.Data)
off := state.t.Transform(f32.Point{})
clip := state.clip.Intersect(op.Rect.Add(off))
// Transform (if needed) the painting rectangle and if so generate a clip path,
// for those cases also compute a partialTrans that maps texture coordinates between
// the new bounding rectangle and the transformed original paint rectangle.
clipData, bnd, partialTrans := d.boundsForTransformedRect(op.Rect, state.t)
clip := state.clip.Intersect(bnd).Canon()
if clip.Empty() {
continue
}
wasrect := state.rect
if clipData != nil {
// The paint operation is sheared or rotated, add a clip path representing
// this transformed rectangle.
d.addClipPath(&state, clipData, d.noCacheKey())
}
bounds := boundRectF(clip)
mat := state.materialFor(d.cache, op.Rect, off, bounds)
mat := state.materialFor(d.cache, bnd, partialTrans, bounds)
if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && state.rect && mat.opaque && mat.material == materialColor {
// 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.
@@ -763,15 +787,20 @@ loop:
img := imageOp{
z: zf,
path: state.cpath,
off: off,
clip: bounds,
material: mat,
}
if state.rect && img.material.opaque {
d.zimageOps = append(d.zimageOps, img)
} else {
d.imageOps = append(d.imageOps, img)
}
if clipData != nil {
// we added a clip path that should not remain
state.cpath = state.cpath.parent
state.rect = wasrect
}
case opconst.TypePush:
state.z = d.collectOps(r, state)
case opconst.TypePop:
@@ -792,7 +821,7 @@ func expandPathOp(p *pathOp, clip image.Rectangle) {
}
}
func (d *drawState) materialFor(cache *resourceCache, rect f32.Rectangle, off f32.Point, clip image.Rectangle) material {
func (d *drawState) materialFor(cache *resourceCache, rect f32.Rectangle, trans f32.Affine2D, clip image.Rectangle) material {
var m material
switch d.matType {
case materialColor:
@@ -801,7 +830,7 @@ func (d *drawState) materialFor(cache *resourceCache, rect f32.Rectangle, off f3
m.opaque = m.color.A == 1.0
case materialTexture:
m.material = materialTexture
dr := boundRectF(rect.Add(off))
dr := boundRectF(rect)
sz := d.image.src.Bounds().Size()
sr := layout.FRect(d.image.rect)
if dx := float32(dr.Dx()); dx != 0 {
@@ -827,7 +856,8 @@ func (d *drawState) materialFor(cache *resourceCache, rect f32.Rectangle, off f3
tex = t
}
m.texture = tex.(*texture)
m.uvScale, m.uvOffset = texSpaceTransform(sr, sz)
uvScale, uvOffset := texSpaceTransform(sr, sz)
m.uvTrans = trans.Mul(f32.Affine2D{}.Scale(f32.Point{}, uvScale).Offset(uvOffset))
}
return m
}
@@ -846,7 +876,7 @@ func (r *renderer) drawZOps(ops []imageOp) {
}
drc := img.clip
scale, off := clipSpaceTransform(drc, r.blitter.viewport)
r.blitter.blit(img.z, m.material, m.color, scale, off, m.uvScale, m.uvOffset)
r.blitter.blit(img.z, m.material, m.color, scale, off, m.uvTrans)
}
r.ctx.SetDepthTest(false)
}
@@ -865,11 +895,12 @@ func (r *renderer) drawOps(ops []imageOp) {
r.ctx.BindTexture(0, r.texHandle(m.texture))
}
drc := img.clip
scale, off := clipSpaceTransform(drc, r.blitter.viewport)
var fbo stencilFBO
switch img.clipType {
case clipTypeNone:
r.blitter.blit(img.z, m.material, m.color, scale, off, m.uvScale, m.uvOffset)
r.blitter.blit(img.z, m.material, m.color, scale, off, m.uvTrans)
continue
case clipTypePath:
fbo = r.pather.stenciler.cover(img.place.Idx)
@@ -885,13 +916,13 @@ func (r *renderer) drawOps(ops []imageOp) {
Max: img.place.Pos.Add(drc.Size()),
}
coverScale, coverOff := texSpaceTransform(toRectF(uv), fbo.size)
r.pather.cover(img.z, m.material, m.color, scale, off, m.uvScale, m.uvOffset, coverScale, coverOff)
r.pather.cover(img.z, m.material, m.color, scale, off, m.uvTrans, coverScale, coverOff)
}
r.ctx.DepthMask(true)
r.ctx.SetDepthTest(false)
}
func (b *blitter) blit(z float32, mat materialType, col f32color.RGBA, scale, off, uvScale, uvOff f32.Point) {
func (b *blitter) blit(z float32, mat materialType, col f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D) {
p := b.prog[mat]
b.ctx.BindProgram(p.prog)
var uniforms *blitUniforms
@@ -900,7 +931,9 @@ func (b *blitter) blit(z float32, mat materialType, col f32color.RGBA, scale, of
b.colUniforms.frag.color = col
uniforms = &b.colUniforms.vert.blitUniforms
case materialTexture:
b.texUniforms.vert.uvTransform = [4]float32{uvScale.X, uvScale.Y, uvOff.X, uvOff.Y}
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
b.texUniforms.vert.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
b.texUniforms.vert.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
uniforms = &b.texUniforms.vert.blitUniforms
}
uniforms.z = z
@@ -994,6 +1027,7 @@ func clipSpaceTransform(r image.Rectangle, viewport image.Point) (f32.Point, f32
// the rectangle at (x, y) and dimensions (w, h).
scale := f32.Point{X: w * .5, Y: h * .5}
offset := f32.Point{X: x + w*.5, Y: y - h*.5}
return scale, offset
}
@@ -1043,9 +1077,8 @@ func (d *drawOps) writeVertCache(n int) []byte {
return d.vertCache[len(d.vertCache)-n:]
}
func (d *drawOps) buildVerts(aux []byte) (verts []byte, bounds f32.Rectangle) {
// split paths as needed, calculate maxY, bounds and create
// vertices that will be sent to GPU.
// transform, split paths as needed, calculate maxY, bounds and create GPU vertices.
func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D) (verts []byte, bounds f32.Rectangle) {
inf := float32(math.Inf(+1))
d.qs.bounds = f32.Rectangle{
Min: f32.Point{X: inf, Y: inf},
@@ -1057,6 +1090,7 @@ func (d *drawOps) buildVerts(aux []byte) (verts []byte, bounds f32.Rectangle) {
for qi := 0; len(aux) >= (ops.QuadSize + 4); qi++ {
d.qs.contour = bo.Uint32(aux)
quad := ops.DecodeQuad(aux[4:])
quad = quad.Transform(tr)
d.qs.splitAndEncode(quad)
@@ -1066,3 +1100,65 @@ func (d *drawOps) buildVerts(aux []byte) (verts []byte, bounds f32.Rectangle) {
fillMaxY(d.vertCache[startLength:])
return d.vertCache[startLength:], d.qs.bounds
}
// create GPU vertices for transformed r, find the bounds and establish texture transform.
func (d *drawOps) boundsForTransformedRect(r f32.Rectangle, tr f32.Affine2D) (aux []byte, bnd f32.Rectangle, ptr f32.Affine2D) {
if isPureOffset(tr) {
// fast-path to allow blitting of pure rectangles
_, _, ox, _, _, oy := tr.Elems()
off := f32.Pt(ox, oy)
bnd.Min = r.Min.Add(off)
bnd.Max = r.Max.Add(off)
return
}
// transform all corners, find new bounds
corners := [4]f32.Point{
tr.Transform(r.Min), tr.Transform(f32.Pt(r.Max.X, r.Min.Y)),
tr.Transform(r.Max), tr.Transform(f32.Pt(r.Min.X, r.Max.Y)),
}
bnd.Min = f32.Pt(math.MaxFloat32, math.MaxFloat32)
bnd.Max = f32.Pt(-math.MaxFloat32, -math.MaxFloat32)
for _, c := range corners {
if c.X < bnd.Min.X {
bnd.Min.X = c.X
}
if c.Y < bnd.Min.Y {
bnd.Min.Y = c.Y
}
if c.X > bnd.Max.X {
bnd.Max.X = c.X
}
if c.Y > bnd.Max.Y {
bnd.Max.Y = c.Y
}
}
// build the GPU vertices
l := len(d.vertCache)
d.vertCache = append(d.vertCache, make([]byte, vertStride*4*4)...)
aux = d.vertCache[l:]
encodeQuadTo(aux, 0, corners[0], corners[0].Add(corners[1]).Mul(0.5), corners[1])
encodeQuadTo(aux[vertStride*4:], 0, corners[1], corners[1].Add(corners[2]).Mul(0.5), corners[2])
encodeQuadTo(aux[vertStride*4*2:], 0, corners[2], corners[2].Add(corners[3]).Mul(0.5), corners[3])
encodeQuadTo(aux[vertStride*4*3:], 0, corners[3], corners[3].Add(corners[0]).Mul(0.5), corners[0])
fillMaxY(aux)
// establish the transform mapping from bounds rectangle to transformed corners
var P1, P2, P3 f32.Point
P1.X = (corners[1].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
P1.Y = (corners[1].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
P2.X = (corners[2].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
P2.Y = (corners[2].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
P3.X = (corners[3].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
P3.Y = (corners[3].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
sx, sy := P2.X-P3.X, P2.Y-P3.Y
ptr = f32.NewAffine2D(sx, P2.X-P1.X, P1.X-sx, sy, P2.Y-P1.Y, P1.Y-sy).Invert()
return
}
func isPureOffset(t f32.Affine2D) bool {
a, b, _, d, e, _ := t.Elems()
return a == 1 && b == 0 && d == 0 && e == 1
}
+12 -12
View File
@@ -54,7 +54,8 @@ type coverColUniforms struct {
type coverUniforms struct {
transform [4]float32
uvCoverTransform [4]float32
uvTransform [4]float32
uvTransformR1 [4]float32
uvTransformR2 [4]float32
z float32
}
@@ -77,9 +78,7 @@ type stenciler struct {
type stencilUniforms struct {
vert struct {
transform [4]float32
pathOffset [2]float32
_ [8]byte // Padding to multiple of 16.
transform [4]float32
}
}
@@ -307,8 +306,8 @@ func (p *pather) begin(sizes []image.Point) {
p.stenciler.begin(sizes)
}
func (p *pather) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data *pathData) {
p.stenciler.stencilPath(bounds, offset, uv, data)
func (p *pather) stencilPath(bounds image.Rectangle, uv image.Point, data *pathData) {
p.stenciler.stencilPath(bounds, uv, data)
}
func (s *stenciler) beginIntersect(sizes []image.Point) {
@@ -337,14 +336,13 @@ func (s *stenciler) begin(sizes []image.Point) {
s.ctx.BindIndexBuffer(s.indexBuf)
}
func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data *pathData) {
func (s *stenciler) stencilPath(bounds image.Rectangle, uv image.Point, data *pathData) {
s.ctx.Viewport(uv.X, uv.Y, bounds.Dx(), bounds.Dy())
// Transform UI coordinates to OpenGL coordinates.
texSize := f32.Point{X: float32(bounds.Dx()), Y: float32(bounds.Dy())}
scale := f32.Point{X: 2 / texSize.X, Y: 2 / texSize.Y}
orig := f32.Point{X: -1 - float32(bounds.Min.X)*2/texSize.X, Y: -1 - float32(bounds.Min.Y)*2/texSize.Y}
s.prog.uniforms.vert.transform = [4]float32{scale.X, scale.Y, orig.X, orig.Y}
s.prog.uniforms.vert.pathOffset = [2]float32{offset.X, offset.Y}
s.prog.prog.UploadUniforms()
// Draw in batches that fit in uint16 indices.
start := 0
@@ -361,11 +359,11 @@ func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv ima
}
}
func (p *pather) cover(z float32, mat materialType, col f32color.RGBA, scale, off, uvScale, uvOff, coverScale, coverOff f32.Point) {
p.coverer.cover(z, mat, col, scale, off, uvScale, uvOff, coverScale, coverOff)
func (p *pather) cover(z float32, mat materialType, col f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
p.coverer.cover(z, mat, col, scale, off, uvTrans, coverScale, coverOff)
}
func (c *coverer) cover(z float32, mat materialType, col f32color.RGBA, scale, off, uvScale, uvOff, coverScale, coverOff f32.Point) {
func (c *coverer) cover(z float32, mat materialType, col f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
p := c.prog[mat]
c.ctx.BindProgram(p.prog)
var uniforms *coverUniforms
@@ -374,7 +372,9 @@ func (c *coverer) cover(z float32, mat materialType, col f32color.RGBA, scale, o
c.colUniforms.frag.color = col
uniforms = &c.colUniforms.vert.coverUniforms
case materialTexture:
c.texUniforms.vert.uvTransform = [4]float32{uvScale.X, uvScale.Y, uvOff.X, uvOff.Y}
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
c.texUniforms.vert.uvTransformR1 = [4]float32{t1, t2, t3, 0}
c.texUniforms.vert.uvTransformR2 = [4]float32{t4, t5, t6, 0}
uniforms = &c.texUniforms.vert.coverUniforms
}
uniforms.z = z
+55 -43
View File
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -8,7 +8,8 @@ precision highp float;
layout(binding = 0) uniform Block {
vec4 transform;
vec4 uvTransform;
vec4 uvTransformR1;
vec4 uvTransformR2;
float z;
};
@@ -21,5 +22,5 @@ layout(location = 0) out vec2 vUV;
void main() {
vec2 p = pos*transform.xy + transform.zw;
gl_Position = toClipSpace(vec4(p, z, 1));
vUV = uv*uvTransform.xy + uvTransform.zw;
vUV = transform3x2(m3x2(uvTransformR1.xyz, uvTransformR2.xyz), vec3(uv,1)).xy;
}
+3 -2
View File
@@ -9,7 +9,8 @@ precision highp float;
layout(binding = 0) uniform Block {
vec4 transform;
vec4 uvCoverTransform;
vec4 uvTransform;
vec4 uvTransformR1;
vec4 uvTransformR2;
float z;
};
@@ -22,7 +23,7 @@ layout(location = 1) out vec2 vUV;
void main() {
gl_Position = toClipSpace(vec4(pos*transform.xy + transform.zw, z, 1));
vUV = uv*uvTransform.xy + uvTransform.zw;
vUV = transform3x2(m3x2(uvTransformR1.xyz, uvTransformR2.xyz), vec3(uv,1)).xy;
vec3 uv3 = transform3x2(fboTextureTransform, vec3(uv, 1.0));
vCoverUV = (uv3*vec3(uvCoverTransform.xy, 1.0)+vec3(uvCoverTransform.zw, 0.0)).xy;
}
+4 -5
View File
@@ -6,7 +6,6 @@ precision highp float;
layout(binding = 0) uniform Block {
vec4 transform;
vec2 pathOffset;
};
layout(location=0) in float corner;
@@ -23,10 +22,10 @@ void main() {
// Add a one pixel overlap so curve quads cover their
// entire curves. Could use conservative rasterization
// if available.
vec2 from = from + pathOffset;
vec2 ctrl = ctrl + pathOffset;
vec2 to = to + pathOffset;
float maxy = maxy + pathOffset.y;
vec2 from = from;
vec2 ctrl = ctrl;
vec2 to = to;
float maxy = maxy;
vec2 pos;
float c = corner;
if (c >= 0.375) {
+7
View File
@@ -16,6 +16,13 @@ type Quad struct {
From, Ctrl, To f32.Point
}
func (q Quad) Transform(t f32.Affine2D) Quad {
q.From = t.Transform(q.From)
q.Ctrl = t.Transform(q.Ctrl)
q.To = t.Transform(q.To)
return q
}
func EncodeQuad(d []byte, q Quad) {
bo := binary.LittleEndian
bo.PutUint32(d[0:], math.Float32bits(q.From.X))
+18 -3
View File
@@ -5,6 +5,7 @@ package ops
import (
"encoding/binary"
"gioui.org/f32"
"gioui.org/internal/opconst"
"gioui.org/op"
)
@@ -26,9 +27,10 @@ type EncodedOp struct {
// Key is a unique key for a given op.
type Key struct {
ops *op.Ops
pc int
version int
ops *op.Ops
pc int
version int
sx, hx, sy, hy float32
}
// Shadow of op.MacroOp.
@@ -52,6 +54,10 @@ type opMacroDef struct {
endpc pc
}
func (r *Reader) NewKey(pc int) Key {
return Key{ops: r.ops, pc: pc, version: r.ops.Version()}
}
// Reset start reading from the op list.
func (r *Reader) Reset(ops *op.Ops) {
r.stack = r.stack[:0]
@@ -59,6 +65,15 @@ func (r *Reader) Reset(ops *op.Ops) {
r.ops = ops
}
func (k Key) SetTransform(t f32.Affine2D) Key {
sx, hx, sy, hy, _, _ := t.Elems()
k.sx = sx
k.hx = hx
k.sy = sy
k.hy = hy
return k
}
func (r *Reader) Decode() (EncodedOp, bool) {
if r.ops == nil {
return EncodedOp{}, false
+23 -29
View File
@@ -117,10 +117,10 @@ type InvalidateOp struct {
At time.Time
}
// TransformOp applies a transform to the current transform.
// TransformOp applies a transform to the current transform. The zero value
// for TransformOp represents the identity transform.
type TransformOp struct {
// TODO: general transformations.
offset f32.Point
t f32.Affine2D
}
// stack tracks the integer identities of StackOp and MacroOp
@@ -264,39 +264,33 @@ func (r InvalidateOp) Add(o *Ops) {
}
}
// Offset the transformation.
// Offset creates a TransformOp with the offset o.
func Offset(o f32.Point) TransformOp {
return TransformOp{t: f32.Affine2D{}.Offset(o)}
}
// Affine creates a TransformOp representing the transformation a.
func Affine(a f32.Affine2D) TransformOp {
return TransformOp{t: a}
}
// Offset the transfomraiton.
func (t TransformOp) Offset(o f32.Point) TransformOp {
return t.Multiply(TransformOp{o})
}
// Invert the transformation.
func (t TransformOp) Invert() TransformOp {
return TransformOp{offset: t.offset.Mul(-1)}
}
// Transform a point.
func (t TransformOp) Transform(p f32.Point) f32.Point {
return p.Add(t.offset)
}
// Multiply by a transformation.
func (t TransformOp) Multiply(t2 TransformOp) TransformOp {
return TransformOp{
offset: t.offset.Add(t2.offset),
}
t.t = t.t.Offset(o)
return t
}
func (t TransformOp) Add(o *Ops) {
data := o.Write(opconst.TypeTransformLen)
data[0] = byte(opconst.TypeTransform)
bo := binary.LittleEndian
// write it out as an affine matrix although we only support offset yet
bo.PutUint32(data[1:], math.Float32bits(1.0))
bo.PutUint32(data[1+4*1:], math.Float32bits(0))
bo.PutUint32(data[1+4*2:], math.Float32bits(t.offset.X))
bo.PutUint32(data[1+4*3:], math.Float32bits(0))
bo.PutUint32(data[1+4*4:], math.Float32bits(1))
bo.PutUint32(data[1+4*5:], math.Float32bits(t.offset.Y))
a, b, c, d, e, f := t.t.Elems()
bo.PutUint32(data[1:], math.Float32bits(a))
bo.PutUint32(data[1+4*1:], math.Float32bits(b))
bo.PutUint32(data[1+4*2:], math.Float32bits(c))
bo.PutUint32(data[1+4*3:], math.Float32bits(d))
bo.PutUint32(data[1+4*4:], math.Float32bits(e))
bo.PutUint32(data[1+4*5:], math.Float32bits(f))
}
func (s *stack) push() stackID {