forked from joejulian/gio
f32: implement 2D affine transformations
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 <viktor.ogeman@gmail.com>
This commit is contained in:
+154
@@ -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()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user