From e7bc1a45533b01655d5b7d6a8aca7aeb226d1cef Mon Sep 17 00:00:00 2001 From: Viktor Date: Sat, 20 Jun 2020 23:29:49 +0200 Subject: [PATCH] f32: implement 2D affine transformations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements 2D affine transformations. This commit is a step towards full affine transformations for drawing operations. Heavily based on the work by Péter Szilágyi in patch 9212 Signed-off-by: Viktor --- f32/affine.go | 154 +++++++++++++++++++++++++++++++++++++++++++++ f32/affine_test.go | 121 +++++++++++++++++++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 f32/affine.go create mode 100644 f32/affine_test.go diff --git a/f32/affine.go b/f32/affine.go new file mode 100644 index 00000000..44e914bb --- /dev/null +++ b/f32/affine.go @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package f32 + +import ( + "math" +) + +// Affine2D represents an affine 2D transformation. The zero value if Affine2D +// represents the identity transform. +type Affine2D struct { + // in order to make the zero value of Affine2D represent the identity + // transform we store it with the identity matrix subtracted, that is + // if the actual transformaiton matrix is: + // [sx, hx, ox] + // [hy, sy, oy] + // [ 0, 0, 1] + // we store a = sx-1 and e = sy-1 + a, b, c float32 + d, e, f float32 +} + +// NewAffine2D creates a new Affine2D transform from the matrix elements +// in row major order. The rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1]. +func NewAffine2D(sx, hx, ox, hy, sy, oy float32) Affine2D { + return Affine2D{ + a: sx, b: hx, c: ox, + d: hy, e: sy, f: oy, + }.encode() +} + +// Offset the transformation. +func (a Affine2D) Offset(offset Point) Affine2D { + return Affine2D{ + a.a, a.b, a.c + offset.X, + a.d, a.e, a.f + offset.Y, + } +} + +// Scale the transformation around the given origin. +func (a Affine2D) Scale(origin, factor Point) Affine2D { + if origin == (Point{}) { + return a.scale(factor) + } + a = a.Offset(origin.Mul(-1)) + a = a.scale(factor) + return a.Offset(origin) +} + +// Rotate the transformation by the given angle (in radians) counter clockwise around the given origin. +func (a Affine2D) Rotate(origin Point, radians float32) Affine2D { + if origin == (Point{}) { + return a.rotate(radians) + } + a = a.Offset(origin.Mul(-1)) + a = a.rotate(radians) + return a.Offset(origin) +} + +// Shear the transformation by the given angle (in radians) around the given origin. +func (a Affine2D) Shear(origin Point, radiansX, radiansY float32) Affine2D { + if origin == (Point{}) { + return a.shear(radiansX, radiansY) + } + a = a.Offset(origin.Mul(-1)) + a = a.shear(radiansX, radiansY) + return a.Offset(origin) +} + +// Mul returns A*B. +func (A Affine2D) Mul(B Affine2D) (r Affine2D) { + A, B = A.decode(), B.decode() + r.a = A.a*B.a + A.b*B.d + r.b = A.a*B.b + A.b*B.e + r.c = A.a*B.c + A.b*B.f + A.c + r.d = A.d*B.a + A.e*B.d + r.e = A.d*B.b + A.e*B.e + r.f = A.d*B.c + A.e*B.f + A.f + return r.encode() +} + +// Invert the transformation. Note that if the matrix is close to singular +// numerical errors may become large or infinity. +func (a Affine2D) Invert() Affine2D { + if a.a == 0 && a.b == 0 && a.d == 0 && a.e == 0 { + return Affine2D{a: 0, b: 0, c: -a.c, d: 0, e: 0, f: -a.f} + } + a = a.decode() + det := a.a*a.e - a.b*a.d + a.a, a.e = a.e/det, a.a/det + a.b, a.d = -a.b/det, -a.d/det + temp := a.c + a.c = -a.a*a.c - a.b*a.f + a.f = -a.d*temp - a.e*a.f + return a.encode() +} + +// Transform p by returning a*p. +func (a Affine2D) Transform(p Point) Point { + a = a.decode() + return Point{ + X: p.X*a.a + p.Y*a.b + a.c, + Y: p.X*a.d + p.Y*a.e + a.f, + } +} + +// Elems returns the matrix elements of the transform in row-major order. The +// rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1]. +func (a Affine2D) Elems() (sx, hx, ox, hy, sy, oy float32) { + a = a.decode() + return a.a, a.b, a.c, a.d, a.e, a.f +} + +func (a Affine2D) encode() Affine2D { + // since we store with identity matrix subtracted + a.a -= 1 + a.e -= 1 + return a +} + +func (a Affine2D) decode() Affine2D { + // since we store with identity matrix subtracted + a.a += 1 + a.e += 1 + return a +} + +func (a Affine2D) scale(factor Point) Affine2D { + a = a.decode() + return Affine2D{ + a.a * factor.X, a.b * factor.X, a.c * factor.X, + a.d * factor.Y, a.e * factor.Y, a.f * factor.Y, + }.encode() +} + +func (a Affine2D) rotate(radians float32) Affine2D { + sin, cos := math.Sincos(float64(radians)) + s, c := float32(sin), float32(cos) + a = a.decode() + return Affine2D{ + a.a*c - a.d*s, a.b*c - a.e*s, a.c*c - a.f*s, + a.a*s + a.d*c, a.b*s + a.e*c, a.c*s + a.f*c, + }.encode() +} + +func (a Affine2D) shear(radiansX, radiansY float32) Affine2D { + tx := float32(math.Tan(float64(radiansX))) + ty := float32(math.Tan(float64(radiansY))) + a = a.decode() + return Affine2D{ + a.a + a.d*tx, a.b + a.e*tx, a.c + a.f*tx, + a.a*ty + a.d, a.b*ty + a.e, a.f*ty + a.f, + }.encode() +} diff --git a/f32/affine_test.go b/f32/affine_test.go new file mode 100644 index 00000000..69da14b5 --- /dev/null +++ b/f32/affine_test.go @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package f32 + +import ( + "math" + "testing" +) + +func eq(p1, p2 Point) bool { + tol := 1e-5 + dx, dy := p2.X-p1.X, p2.Y-p1.Y + return math.Abs(math.Sqrt(float64(dx*dx+dy*dy))) < tol +} + +func TestTransformOffset(t *testing.T) { + p := Point{X: 1, Y: 2} + o := Point{X: 2, Y: -3} + + r := Affine2D{}.Offset(o).Transform(p) + if !eq(r, Pt(3, -1)) { + t.Errorf("offset transformation mismatch: have %v, want {3 -1}", r) + } + i := Affine2D{}.Offset(o).Invert().Transform(r) + if !eq(i, p) { + 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 := Affine2D{}.Scale(Point{}, s).Transform(p) + if !eq(r, Pt(-1, 4)) { + t.Errorf("scale transformation mismatch: have %v, want {-1 4}", r) + } + i := Affine2D{}.Scale(Point{}, s).Invert().Transform(r) + if !eq(i, p) { + 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 := Affine2D{}.Rotate(Point{}, a).Transform(p) + if !eq(r, Pt(0, 1)) { + t.Errorf("rotate transformation mismatch: have %v, want {0 1}", r) + } + i := Affine2D{}.Rotate(Point{}, a).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("rotate transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestTransformShear(t *testing.T) { + p := Point{X: 1, Y: 1} + + r := Affine2D{}.Shear(Point{}, math.Pi/4, 0).Transform(p) + if !eq(r, Pt(2, 1)) { + t.Errorf("shear transformation mismatch: have %v, want {2 1}", r) + } + i := Affine2D{}.Shear(Point{}, math.Pi/4, 0).Invert().Transform(r) + if !eq(i, p) { + 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) + + r := Affine2D{}.Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Transform(p) + if !eq(r, Pt(1, 3)) { + t.Errorf("complex transformation mismatch: have %v, want {1 3}", r) + } + i := Affine2D{}.Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("complex transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestTransformScaleAround(t *testing.T) { + p := Pt(-1, -1) + target := Pt(-6, -13) + pt := Affine2D{}.Scale(Pt(4, 5), Pt(2, 3)).Transform(p) + if !eq(pt, target) { + t.Log(pt, "!=", target) + t.Error("Scale not as expected") + } +} + +func TestTransformRotateAround(t *testing.T) { + p := Pt(-1, -1) + pt := Affine2D{}.Rotate(Pt(1, 1), -math.Pi/2).Transform(p) + target := Pt(-1, 3) + if !eq(pt, target) { + t.Log(pt, "!=", target) + t.Error("Rotate not as expected") + } +} + +func TestMulOrder(t *testing.T) { + A := Affine2D{}.Offset(Pt(100, 100)) + B := Affine2D{}.Scale(Point{}, Pt(2, 2)) + _ = A + _ = B + + T1 := Affine2D{}.Offset(Pt(100, 100)).Scale(Point{}, Pt(2, 2)) + T2 := B.Mul(A) + + if T1 != T2 { + t.Log(T1) + t.Log(T2) + t.Error("multiplication / transform order not as expected") + } +}