~eliasnaur/gio-patches

various: implement 2D affine transforms v3 PROPOSED

Péter Szilágyi: 1
 various: implement 2D affine transforms

 15 files changed, 358 insertions(+), 97 deletions(-)
Hey Elias,

Thanks for the review comments. I kind of knew they are coming, just
didn't have a good place to rationalize/discuss them (would be nice if
sourcehut provided a means to add comment / questions outside of the
patch commit description). I replied below, but the crux of my
question revolves around making all the helper methods internal. I do
understand the desire, but that entails the internal fields being
converted to public. I can do that id that's what you want, but I'm
not sure. Please advise.
Next
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~eliasnaur/gio-patches/patches/9212/mbox | git am -3
Learn more about email & git

[PATCH v3] various: implement 2D affine transforms Export this patch

Currently Gio has a simplistic transformation operation implemented
that's essentially an offset vector. This is enough for many layout
tasks, but falls short when animations come into the picture (e.g.
spinner).

This CL replaces the current transform op with one base on affine
transformation matrices, thus enabling advanced operations such as
scaling, rotation and shearing.

Since it's expected that most UIs will not use (or use in very limited
portions) the advanced transforms, the CL also special cases the offset
use case, so both calculations and serializations using only translation
will take optimized coda paths. This is handled under the hood.

Whilst the CL does implement all the affine transforms, they cannot be
readily used yet for the widgets, as the `gpu` package does not use the
transforms correctly. Instead of transforming the input coordinates, it
transforms an offset vector and uses that to only offset the inputs.
This needs to be fixed but I considered it out of scope of the affine
calculations and left it for a followup CL.

Signed-off-by: Péter Szilágyi <peterke@gmail.com>
---
 app/internal/gpu/gpu.go       |   5 +-
 app/internal/input/pointer.go |   5 +-
 f32/affine.go                 |  99 ++++++++++++++++++++++++
 f32/affine_test.go            |  86 +++++++++++++++++++++
 internal/opconst/ops.go       |  41 +++++-----
 internal/ops/ops.go           |  23 ------
 layout/flex.go                |   2 +-
 layout/layout.go              |   4 +-
 layout/list.go                |   2 +-
 layout/stack.go               |   2 +-
 op/op.go                      |  38 ----------
 op/transform.go               | 139 ++++++++++++++++++++++++++++++++++
 widget/editor.go              |   2 +-
 widget/label.go               |   2 +-
 widget/material/button.go     |   5 +-
 15 files changed, 358 insertions(+), 97 deletions(-)
 create mode 100644 f32/affine.go
 create mode 100644 f32/affine_test.go
 delete mode 100644 internal/ops/ops.go
 create mode 100644 op/transform.go

diff --git a/app/internal/gpu/gpu.go b/app/internal/gpu/gpu.go
index b9b3f43..3efe4c0 100644
--- a/app/internal/gpu/gpu.go
+++ b/app/internal/gpu/gpu.go
@@ -722,9 +722,8 @@ func (d *drawOps) collectOps(r *ops.Reader, state drawState) int {
 loop:
 	for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
 		switch opconst.OpType(encOp.Data[0]) {
-		case opconst.TypeTransform:
-			dop := ops.DecodeTransformOp(encOp.Data)
-			state.t = state.t.Multiply(op.TransformOp(dop))
+		case opconst.TypeTransformOffset, opconst.TypeTransform:
+			state.t = state.t.Apply(op.DecodeTransformOp(encOp.Data))
 		case opconst.TypeAux:
 			aux = encOp.Data[opconst.TypeAuxLen:]
 			// The first data byte stores whether the MaxY
diff --git a/app/internal/input/pointer.go b/app/internal/input/pointer.go
index d2fffd6..430b4c2 100644
--- a/app/internal/input/pointer.go
+++ b/app/internal/input/pointer.go
@@ -85,9 +85,8 @@ func (q *pointerQueue) collectHandlers(r *ops.Reader, events *handlerEvents, t o
 				pass: pass,
 			})
 			node = len(q.hitTree) - 1
-		case opconst.TypeTransform:
-			dop := ops.DecodeTransformOp(encOp.Data)
-			t = t.Multiply(op.TransformOp(dop))
+		case opconst.TypeTransformOffset, opconst.TypeTransform:
+			t = t.Apply(op.DecodeTransformOp(encOp.Data))
 		case opconst.TypePointerInput:
 			op := decodePointerInputOp(encOp.Data, encOp.Refs)
 			q.hitTree = append(q.hitTree, hitNode{
diff --git a/f32/affine.go b/f32/affine.go
new file mode 100644
index 0000000..1bff3d3
--- /dev/null
+++ b/f32/affine.go
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package f32
+
+import (
+	"math"
+)
+
+// Affine2D represents a 2D affine transformation (translation, rotation, scaling
+// and shearing). The type represents the top two works of the 3x3 transformation
+// matrix.
+type Affine2D [2][3]float32
+
+// Identity2D creates a noop affine transformation that can be modified.
+func Identity2D() Affine2D {
+	return Affine2D{
+		{1, 0, 0},
+		{0, 1, 0},
+	}
+}
+
+// Offset the transformation with the given vector.
+func (a Affine2D) Offset(amount Point) Affine2D {
+	return Affine2D{
+		{a[0][0], a[0][1], a[0][2] + amount.X},
+		{a[1][0], a[1][1], a[1][2] + amount.Y},
+	}
+}
+
+// Scale the transformation by the given factors. If either of the components is
+// zero, inversion will panic.
+func (a Affine2D) Scale(factor Point) Affine2D {
+	return Affine2D{
+		{a[0][0] * factor.X, a[0][1] * factor.X, a[0][2] * factor.X},
+		{a[1][0] * factor.Y, a[1][1] * factor.Y, a[1][2] * factor.Y},
+	}
+}
+
+// Rotate the transformation by the given angle (in radians) counter clockwise.
+func (a Affine2D) Rotate(angle float32) Affine2D {
+	var (
+		sin, cos   = math.Sincos(float64(angle))
+		fsin, fcos = float32(sin), float32(cos)
+	)
+	return Affine2D{
+		{a[0][0]*fcos - a[1][0]*fsin, a[0][1]*fcos - a[1][1]*fsin, a[0][2]*fcos - a[1][2]*fsin},
+		{a[0][0]*fsin + a[1][0]*fcos, a[0][1]*fsin + a[1][1]*fcos, a[0][2]*fsin + a[1][2]*fcos},
+	}
+}
+
+// Shear the transformation by the given angles (in radians). If either of the
+// components is a multiple of PI/2, inversion will panic.
+func (a Affine2D) Shear(angles Point) Affine2D {
+	var (
+		tx = float32(math.Tan(float64(angles.X)))
+		ty = float32(math.Tan(float64(angles.Y)))
+	)
+	return Affine2D{
+		{a[0][0] + a[1][0]*tx, a[0][1] + a[1][1]*tx, a[0][2] + a[1][2]*tx},
+		{a[0][0]*ty + a[1][0], a[0][1]*ty + a[1][1], a[1][2]*ty + a[1][2]},
+	}
+}
+
+// Apply appends a transformation on top of the current stack. Note, internally
+// this method multiplies the matrices in reverse (T2 x T) to keep the user's
+// expected order (final 2D transform product goes on the right side of the eq).
+func (a Affine2D) Apply(op Affine2D) Affine2D {
+	return Affine2D{
+		{
+			op[0][0]*a[0][0] + op[0][1]*a[1][0],
+			op[0][0]*a[0][1] + op[0][1]*a[1][1],
+			op[0][0]*a[0][2] + op[0][1]*a[1][2] + op[0][2],
+		},
+		{
+			op[1][0]*a[0][0] + op[1][1]*a[1][0],
+			op[1][0]*a[0][1] + op[1][1]*a[1][1],
+			op[1][0]*a[0][2] + op[1][1]*a[1][2] + op[1][2],
+		},
+	}
+}
+
+// Invert the transformation.
+func (a Affine2D) Invert() Affine2D {
+	var (
+		invdet = 1 / (a[0][0]*a[1][1] - a[0][1]*a[1][0])
+	)
+	return Affine2D{
+		{a[1][1] * invdet, -a[0][1] * invdet, (a[0][1]*a[1][2] - a[1][1]*a[0][2]) * invdet},
+		{-a[1][0] * invdet, a[0][0] * invdet, -(a[0][0]*a[1][2] - a[1][0]*a[0][2]) * invdet},
+	}
+}
+
+// Transform a point in the 2D space according to the applied transform stack.
+func (a Affine2D) Transform(p Point) Point {
+	return Point{
+		X: p.X*a[0][0] + p.Y*a[0][1] + a[0][2],
+		Y: p.X*a[1][0] + p.Y*a[1][1] + a[1][2],
+	}
+}
diff --git a/f32/affine_test.go b/f32/affine_test.go
new file mode 100644
index 0000000..466f160
--- /dev/null
+++ b/f32/affine_test.go
@@ -0,0 +1,86 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package f32
+
+import (
+	"math"
+	"testing"
+)
+
+// eq checks floating point equality at some epsilon precision.
+func eq(a, b float32) bool {
+	return math.Abs(float64(a)-float64(b)) < 0.00001
+}
+
+func TestTransformOffset(t *testing.T) {
+	p := Point{X: 1, Y: 2}
+	o := Point{X: 2, Y: -3}
+
+	r := Identity2D().Offset(o).Transform(p)
+	if !eq(r.X, 3) || !eq(r.Y, -1) {
+		t.Errorf("offset transformation mismatch: have %v, want {3 -1}", r)
+	}
+	i := Identity2D().Offset(o).Invert().Transform(r)
+	if !eq(i.X, p.X) || !eq(i.Y, p.Y) {
+		t.Errorf("offset transformation inverse mismatch: have %v, want %v", i, p)
+	}
+}
+
+func TestTransformScale(t *testing.T) {
+	p := Point{X: 1, Y: 2}
+	s := Point{X: -1, Y: 2}
+
+	r := Identity2D().Scale(s).Transform(p)
+	if !eq(r.X, -1) || !eq(r.Y, 4) {
+		t.Errorf("scale transformation mismatch: have %v, want {-1 4}", r)
+	}
+	i := Identity2D().Scale(s).Invert().Transform(r)
+	if !eq(i.X, p.X) || !eq(i.Y, p.Y) {
+		t.Errorf("scale transformation inverse mismatch: have %v, want %v", i, p)
+	}
+}
+
+func TestTransformRotate(t *testing.T) {
+	p := Point{X: 1, Y: 0}
+	a := float32(math.Pi / 2)
+
+	r := Identity2D().Rotate(a).Transform(p)
+	if !eq(r.X, 0) || !eq(r.Y, 1) {
+		t.Errorf("rotate transformation mismatch: have %v, want {0 1}", r)
+	}
+	i := Identity2D().Rotate(a).Invert().Transform(r)
+	if !eq(i.X, p.X) || !eq(i.Y, p.Y) {
+		t.Errorf("rotate transformation inverse mismatch: have %v, want %v", i, p)
+	}
+}
+
+func TestTransformShear(t *testing.T) {
+	p := Point{X: 1, Y: 1}
+	a := Point{X: math.Pi / 4, Y: 0}
+
+	r := Identity2D().Shear(a).Transform(p)
+	if !eq(r.X, 2) || !eq(r.Y, 1) {
+		t.Errorf("shear transformation mismatch: have %v, want {2 1}", r)
+	}
+	i := Identity2D().Shear(a).Invert().Transform(r)
+	if !eq(i.X, p.X) || !eq(i.Y, p.Y) {
+		t.Errorf("shear transformation inverse mismatch: have %v, want %v", i, p)
+	}
+}
+
+func TestTransformMultiply(t *testing.T) {
+	p := Point{X: 1, Y: 2}
+	o := Point{X: 2, Y: -3}
+	s := Point{X: -1, Y: 2}
+	a := float32(-math.Pi / 2)
+	h := Point{X: math.Pi / 4, Y: 0}
+
+	r := Identity2D().Offset(o).Scale(s).Rotate(a).Shear(h).Transform(p)
+	if !eq(r.X, 1) || !eq(r.Y, 3) {
+		t.Errorf("complex transformation mismatch: have %v, want {1 3}", r)
+	}
+	i := Identity2D().Offset(o).Scale(s).Rotate(a).Shear(h).Invert().Transform(r)
+	if !eq(i.X, p.X) || !eq(i.Y, p.Y) {
+		t.Errorf("complex transformation inverse mismatch: have %v, want %v", i, p)
+	}
+}
diff --git a/internal/opconst/ops.go b/internal/opconst/ops.go
index 6bca3d0..c767e5c 100644
--- a/internal/opconst/ops.go
+++ b/internal/opconst/ops.go
@@ -10,7 +10,8 @@ const firstOpIndex = 200
 const (
 	TypeMacroDef OpType = iota + firstOpIndex
 	TypeMacro
-	TypeTransform
+	TypeTransform       // Generic affine transformation
+	TypeTransformOffset // Offset only affine transformation
 	TypeLayer
 	TypeInvalidate
 	TypeImage
@@ -29,24 +30,25 @@ const (
 )
 
 const (
-	TypeMacroDefLen     = 1 + 4 + 4
-	TypeMacroLen        = 1 + 4 + 4 + 4
-	TypeTransformLen    = 1 + 4*2
-	TypeLayerLen        = 1
-	TypeRedrawLen       = 1 + 8
-	TypeImageLen        = 1
-	TypePaintLen        = 1 + 4*4
-	TypeColorLen        = 1 + 4
-	TypeAreaLen         = 1 + 1 + 4*4
-	TypePointerInputLen = 1 + 1
-	TypePassLen         = 1 + 1
-	TypeKeyInputLen     = 1 + 1
-	TypeHideInputLen    = 1
-	TypePushLen         = 1
-	TypePopLen          = 1
-	TypeAuxLen          = 1
-	TypeClipLen         = 1 + 4*4
-	TypeProfileLen      = 1
+	TypeMacroDefLen        = 1 + 4 + 4
+	TypeMacroLen           = 1 + 4 + 4 + 4
+	TypeTransformLen       = 1 + 4*6 // Affine transform matrix's top rows
+	TypeTransformOffsetLen = 1 + 4*2 // X and Y offset
+	TypeLayerLen           = 1
+	TypeRedrawLen          = 1 + 8
+	TypeImageLen           = 1
+	TypePaintLen           = 1 + 4*4
+	TypeColorLen           = 1 + 4
+	TypeAreaLen            = 1 + 1 + 4*4
+	TypePointerInputLen    = 1 + 1
+	TypePassLen            = 1 + 1
+	TypeKeyInputLen        = 1 + 1
+	TypeHideInputLen       = 1
+	TypePushLen            = 1
+	TypePopLen             = 1
+	TypeAuxLen             = 1
+	TypeClipLen            = 1 + 4*4
+	TypeProfileLen         = 1
 )
 
 func (t OpType) Size() int {
@@ -54,6 +56,7 @@ func (t OpType) Size() int {
 		TypeMacroDefLen,
 		TypeMacroLen,
 		TypeTransformLen,
+		TypeTransformOffsetLen,
 		TypeLayerLen,
 		TypeRedrawLen,
 		TypeImageLen,
diff --git a/internal/ops/ops.go b/internal/ops/ops.go
deleted file mode 100644
index c8a9f89..0000000
--- a/internal/ops/ops.go
@@ -1,23 +0,0 @@
-// SPDX-License-Identifier: Unlicense OR MIT
-
-package ops
-
-import (
-	"encoding/binary"
-	"math"
-
-	"gioui.org/f32"
-	"gioui.org/internal/opconst"
-	"gioui.org/op"
-)
-
-func DecodeTransformOp(d []byte) op.TransformOp {
-	bo := binary.LittleEndian
-	if opconst.OpType(d[0]) != opconst.TypeTransform {
-		panic("invalid op")
-	}
-	return op.TransformOp{}.Offset(f32.Point{
-		X: math.Float32frombits(bo.Uint32(d[1:])),
-		Y: math.Float32frombits(bo.Uint32(d[5:])),
-	})
-}
diff --git a/layout/flex.go b/layout/flex.go
index 942b94b..65a68c9 100644
--- a/layout/flex.go
+++ b/layout/flex.go
@@ -154,7 +154,7 @@ func (f *Flex) Layout(gtx *Context, children ...FlexChild) {
 		}
 		var stack op.StackOp
 		stack.Push(gtx.Ops)
-		op.TransformOp{}.Offset(toPointF(axisPoint(f.Axis, mainSize, cross))).Add(gtx.Ops)
+		op.Offset(toPointF(axisPoint(f.Axis, mainSize, cross))).Add(gtx.Ops)
 		child.macro.Add(gtx.Ops)
 		stack.Pop()
 		mainSize += axisMain(f.Axis, dims.Size)
diff --git a/layout/layout.go b/layout/layout.go
index 715bcff..d4b26ab 100644
--- a/layout/layout.go
+++ b/layout/layout.go
@@ -166,7 +166,7 @@ func (in Inset) Layout(gtx *Context, w Widget) {
 	}
 	var stack op.StackOp
 	stack.Push(gtx.Ops)
-	op.TransformOp{}.Offset(toPointF(image.Point{X: left, Y: top})).Add(gtx.Ops)
+	op.Offset(toPointF(image.Point{X: left, Y: top})).Add(gtx.Ops)
 	dims := ctxLayout(gtx, mcs, w)
 	stack.Pop()
 	gtx.Dimensions = Dimensions{
@@ -213,7 +213,7 @@ func (a Align) Layout(gtx *Context, w Widget) {
 	}
 	var stack op.StackOp
 	stack.Push(gtx.Ops)
-	op.TransformOp{}.Offset(toPointF(p)).Add(gtx.Ops)
+	op.Offset(toPointF(p)).Add(gtx.Ops)
 	macro.Add(gtx.Ops)
 	stack.Pop()
 	gtx.Dimensions = Dimensions{
diff --git a/layout/list.go b/layout/list.go
index 6af3da0..525ff65 100644
--- a/layout/list.go
+++ b/layout/list.go
@@ -261,7 +261,7 @@ func (l *List) layout() Dimensions {
 		var stack op.StackOp
 		stack.Push(ops)
 		clip.Rect{Rect: toRectF(r)}.Op(ops).Add(ops)
-		op.TransformOp{}.Offset(toPointF(axisPoint(l.Axis, pos, cross))).Add(ops)
+		op.Offset(toPointF(axisPoint(l.Axis, pos, cross))).Add(ops)
 		child.macro.Add(ops)
 		stack.Pop()
 		pos += childSize
diff --git a/layout/stack.go b/layout/stack.go
index 560d7aa..f27f776 100644
--- a/layout/stack.go
+++ b/layout/stack.go
@@ -84,7 +84,7 @@ func (s *Stack) Layout(gtx *Context, children ...StackChild) {
 		}
 		var stack op.StackOp
 		stack.Push(gtx.Ops)
-		op.TransformOp{}.Offset(toPointF(p)).Add(gtx.Ops)
+		op.Offset(toPointF(p)).Add(gtx.Ops)
 		ch.macro.Add(gtx.Ops)
 		stack.Pop()
 		if baseline == 0 {
diff --git a/op/op.go b/op/op.go
index 9cc67e3..98888fe 100644
--- a/op/op.go
+++ b/op/op.go
@@ -64,10 +64,8 @@ package op
 
 import (
 	"encoding/binary"
-	"math"
 	"time"
 
-	"gioui.org/f32"
 	"gioui.org/internal/opconst"
 )
 
@@ -110,12 +108,6 @@ type InvalidateOp struct {
 	At time.Time
 }
 
-// TransformOp applies a transform to the current transform.
-type TransformOp struct {
-	// TODO: general transformations.
-	offset f32.Point
-}
-
 type pc struct {
 	data int
 	refs int
@@ -256,33 +248,3 @@ func (r InvalidateOp) Add(o *Ops) {
 		}
 	}
 }
-
-// Offset the transformation.
-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),
-	}
-}
-
-func (t TransformOp) Add(o *Ops) {
-	data := o.Write(opconst.TypeTransformLen)
-	data[0] = byte(opconst.TypeTransform)
-	bo := binary.LittleEndian
-	bo.PutUint32(data[1:], math.Float32bits(t.offset.X))
-	bo.PutUint32(data[5:], math.Float32bits(t.offset.Y))
-}
diff --git a/op/transform.go b/op/transform.go
new file mode 100644
index 0000000..164224a
--- /dev/null
+++ b/op/transform.go
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package op
+
+import (
+	"encoding/binary"
+	"math"
+
+	"gioui.org/f32"
+	"gioui.org/internal/opconst"
+)
+
+// TransformOp applies a transformation (translate, rotate, scale, shear).
+type TransformOp struct {
+	complex bool         // Set if the transform is not only offset
+	matrix  f32.Affine2D // Full 2D affine transformation matrix
+}
+
+// Offset offsets by a given 2D vector.
+func Offset(off f32.Point) TransformOp {
+	return TransformOp{
+		matrix: f32.Affine2D{}.Offset(off),
+	}
+}
+
+// Affine2D applies a generic 2D transformation.
+func Affine2D(aff f32.Affine2D) TransformOp {
+	return TransformOp{
+		complex: true,
+		matrix:  aff,
+	}
+}
+
+// Apply appends a transformation on top of the current stack.
+func (t TransformOp) Apply(op TransformOp) TransformOp {
+	switch {
+	case !op.complex:
+		return TransformOp{
+			complex: t.complex,
+			matrix:  t.matrix.Offset(f32.Point{X: op.matrix[0][2], Y: op.matrix[1][2]}),
+		}
+
+	case !t.complex:
+		return TransformOp{
+			complex: op.complex,
+			matrix:  op.matrix.Offset(f32.Point{X: t.matrix[0][2], Y: t.matrix[1][2]}),
+		}
+
+	default:
+		return TransformOp{
+			complex: true,
+			matrix:  t.matrix.Apply(op.matrix),
+		}
+	}
+}
+
+// Invert the transformation.
+func (t TransformOp) Invert() TransformOp {
+	if !t.complex {
+		return Offset(f32.Point{X: -t.matrix[0][2], Y: -t.matrix[1][2]})
+	}
+	return TransformOp{
+		complex: true,
+		matrix:  t.matrix.Invert(),
+	}
+}
+
+// Transform a point in the 2D space according to the applied transform stack.
+func (t TransformOp) Transform(p f32.Point) f32.Point {
+	if !t.complex {
+		return p.Add(f32.Point{X: t.matrix[0][2], Y: t.matrix[1][2]})
+	}
+	return t.matrix.Transform(p)
+}
+
+// Add inserts this transformation op onto the end of the ops stack.
+func (t TransformOp) Add(o *Ops) {
+	bo := binary.LittleEndian
+
+	// If the transform is offset only (usual case), we can cheat by serializing
+	// only the offset vector instead of the entire transformation matrix
+	if !t.complex {
+		data := o.Write(opconst.TypeTransformOffsetLen)
+		data[0] = byte(opconst.TypeTransformOffset)
+
+		bo.PutUint32(data[1:], math.Float32bits(t.matrix[0][2]))
+		bo.PutUint32(data[5:], math.Float32bits(t.matrix[1][2]))
+
+		return
+	}
+	// Seems we have a full affine transformation, serialize the entire matrix
+	data := o.Write(opconst.TypeTransformLen)
+	data[0] = byte(opconst.TypeTransform)
+
+	m := t.matrix // Surely inited, because uninited is handled as offset-only
+
+	bo.PutUint32(data[1:], math.Float32bits(m[0][0]))
+	bo.PutUint32(data[5:], math.Float32bits(m[0][1]))
+	bo.PutUint32(data[9:], math.Float32bits(m[0][2]))
+	bo.PutUint32(data[13:], math.Float32bits(m[1][0]))
+	bo.PutUint32(data[17:], math.Float32bits(m[1][1]))
+	bo.PutUint32(data[21:], math.Float32bits(m[1][2]))
+}
+
+// DecodeTransformOp parses a transform matrix from a binary format.
+func DecodeTransformOp(d []byte) TransformOp {
+	bo := binary.LittleEndian
+
+	// Ensure we have a valid decoding call
+	if opconst.OpType(d[0]) != opconst.TypeTransformOffset && opconst.OpType(d[0]) != opconst.TypeTransform {
+		panic("invalid op")
+	}
+	// If the transform is offset only (usual case), we can cheat by deserializing
+	// only the offset vector instead of the entire transformation matrix
+	if opconst.OpType(d[0]) == opconst.TypeTransformOffset {
+		return TransformOp{
+			matrix: [2][3]float32{
+				{1, 0, math.Float32frombits(bo.Uint32(d[1:]))},
+				{0, 1, math.Float32frombits(bo.Uint32(d[5:]))},
+			},
+		}
+	}
+	// Seems we have a full affine transformation, deserialize the entire matrix
+	return TransformOp{
+		complex: true,
+		matrix: [2][3]float32{
+			{
+				math.Float32frombits(bo.Uint32(d[1:])),
+				math.Float32frombits(bo.Uint32(d[5:])),
+				math.Float32frombits(bo.Uint32(d[9:])),
+			},
+			{
+				math.Float32frombits(bo.Uint32(d[13:])),
+				math.Float32frombits(bo.Uint32(d[17:])),
+				math.Float32frombits(bo.Uint32(d[21:])),
+			},
+		},
+	}
+}
diff --git a/widget/editor.go b/widget/editor.go
index 8ae14ae..bf175bd 100644
--- a/widget/editor.go
+++ b/widget/editor.go
@@ -284,7 +284,7 @@ func (e *Editor) PaintText(gtx *layout.Context) {
 	for _, shape := range e.shapes {
 		var stack op.StackOp
 		stack.Push(gtx.Ops)
-		op.TransformOp{}.Offset(shape.offset).Add(gtx.Ops)
+		op.Offset(shape.offset).Add(gtx.Ops)
 		shape.clip.Add(gtx.Ops)
 		paint.PaintOp{Rect: toRectF(clip).Sub(shape.offset)}.Add(gtx.Ops)
 		stack.Pop()
diff --git a/widget/label.go b/widget/label.go
index 16927b0..7f9c557 100644
--- a/widget/label.go
+++ b/widget/label.go
@@ -107,7 +107,7 @@ func (l Label) Layout(gtx *layout.Context, s *text.Shaper, font text.Font, txt s
 		lclip := toRectF(clip).Sub(off)
 		var stack op.StackOp
 		stack.Push(gtx.Ops)
-		op.TransformOp{}.Offset(off).Add(gtx.Ops)
+		op.Offset(off).Add(gtx.Ops)
 		s.Shape(gtx, font, str).Add(gtx.Ops)
 		paint.PaintOp{Rect: lclip}.Add(gtx.Ops)
 		stack.Pop()
diff --git a/widget/material/button.go b/widget/material/button.go
index 17c9810..8462403 100644
--- a/widget/material/button.go
+++ b/widget/material/button.go
@@ -150,10 +150,7 @@ func drawInk(gtx *layout.Context, c widget.Click) {
 	col := byte(0xaa * (1 - t*t))
 	ink := paint.ColorOp{Color: color.RGBA{A: col, R: col, G: col, B: col}}
 	ink.Add(gtx.Ops)
-	op.TransformOp{}.Offset(c.Position).Offset(f32.Point{
-		X: -rr,
-		Y: -rr,
-	}).Add(gtx.Ops)
+	op.Offset(c.Position.Add(f32.Point{X: -rr, Y: -rr})).Add(gtx.Ops)
 	clip.Rect{
 		Rect: f32.Rectangle{Max: f32.Point{
 			X: float32(size),
-- 
2.20.1
View this thread in the archives