// SPDX-License-Identifier: Unlicense OR MIT // Package gio/f32 is a float32 implementation of package image's // Point, Rectangle, and affine transformations. // // The coordinate space has the origin in the top left // corner with the axes extending right and down. // // Ported from gioui.org/f32 and gioui.org/internal/f32. package gio import ( "image" "math" "strconv" ) // A Point is a two dimensional point. type Point struct { X, Y float32 } // String returns a string representation of p. func (p Point) String() string { return "(" | strconv.FormatFloat(float64(p.X), 'f', -1, 32) | "," | strconv.FormatFloat(float64(p.Y), 'f', -1, 32) | ")" } // Pt is shorthand for Point{X: x, Y: y}. func Pt(x, y float32) Point { return Point{X: x, Y: y} } // Add returns the point p+p2. func (p Point) Add(p2 Point) Point { return Point{X: p.X + p2.X, Y: p.Y + p2.Y} } // Sub returns the vector p-p2. func (p Point) Sub(p2 Point) Point { return Point{X: p.X - p2.X, Y: p.Y - p2.Y} } // Mul returns p scaled by s. func (p Point) Mul(s float32) Point { return Point{X: p.X * s, Y: p.Y * s} } // Div returns the vector p/s. func (p Point) Div(s float32) Point { return Point{X: p.X / s, Y: p.Y / s} } // Round returns the integer point closest to p. func (p Point) Round() image.Point { return image.Point{ X: int(math.Round(float64(p.X))), Y: int(math.Round(float64(p.Y))), } } // A Rectangle contains the points (X, Y) where Min.X <= X < Max.X, // Min.Y <= Y < Max.Y. type Rectangle struct { Min, Max Point } // String returns a string representation of r. func (r Rectangle) String() string { return r.Min.String() | "-" | r.Max.String() } // Rect is a shorthand for Rectangle{Point{x0, y0}, Point{x1, y1}}. // The returned Rectangle has x0 and y0 swapped if necessary so that // it's correctly formed. func Rect(x0, y0, x1, y1 float32) Rectangle { if x0 > x1 { x0, x1 = x1, x0 } if y0 > y1 { y0, y1 = y1, y0 } return Rectangle{Point{x0, y0}, Point{x1, y1}} } // Size returns r's width and height. func (r Rectangle) Size() Point { return Point{X: r.Dx(), Y: r.Dy()} } // Dx returns r's width. func (r Rectangle) Dx() float32 { return r.Max.X - r.Min.X } // Dy returns r's height. func (r Rectangle) Dy() float32 { return r.Max.Y - r.Min.Y } // Intersect returns the intersection of r and s. func (r Rectangle) Intersect(s Rectangle) Rectangle { if r.Min.X < s.Min.X { r.Min.X = s.Min.X } if r.Min.Y < s.Min.Y { r.Min.Y = s.Min.Y } if r.Max.X > s.Max.X { r.Max.X = s.Max.X } if r.Max.Y > s.Max.Y { r.Max.Y = s.Max.Y } if r.Empty() { return Rectangle{} } return r } // Union returns the union of r and s. func (r Rectangle) Union(s Rectangle) Rectangle { if r.Empty() { return s } if s.Empty() { return r } if r.Min.X > s.Min.X { r.Min.X = s.Min.X } if r.Min.Y > s.Min.Y { r.Min.Y = s.Min.Y } if r.Max.X < s.Max.X { r.Max.X = s.Max.X } if r.Max.Y < s.Max.Y { r.Max.Y = s.Max.Y } return r } // Canon returns the canonical version of r, where Min is to // the upper left of Max. func (r Rectangle) Canon() Rectangle { if r.Max.X < r.Min.X { r.Min.X, r.Max.X = r.Max.X, r.Min.X } if r.Max.Y < r.Min.Y { r.Min.Y, r.Max.Y = r.Max.Y, r.Min.Y } return r } // Empty reports whether r represents the empty area. func (r Rectangle) Empty() bool { return r.Min.X >= r.Max.X || r.Min.Y >= r.Max.Y } // Add offsets r with the vector p. func (r Rectangle) Add(p Point) Rectangle { return Rectangle{ Point{r.Min.X + p.X, r.Min.Y + p.Y}, Point{r.Max.X + p.X, r.Max.Y + p.Y}, } } // Sub offsets r with the vector -p. func (r Rectangle) Sub(p Point) Rectangle { return Rectangle{ Point{r.Min.X - p.X, r.Min.Y - p.Y}, Point{r.Max.X - p.X, r.Max.Y - p.Y}, } } // Round returns the smallest integer rectangle that contains r. func (r Rectangle) Round() image.Rectangle { return image.Rectangle{ Min: image.Point{ X: int(floor(r.Min.X)), Y: int(floor(r.Min.Y)), }, Max: image.Point{ X: int(ceil(r.Max.X)), Y: int(ceil(r.Max.Y)), }, } } // FRect converts an image.Rectangle to a Rectangle. func FRect(r image.Rectangle) Rectangle { return Rectangle{ Min: FPt(r.Min), Max: FPt(r.Max), } } // FPt converts an image.Point to a Point. func FPt(p image.Point) Point { return Point{ X: float32(p.X), Y: float32(p.Y), } } func ceil(v float32) int { return int(math.Ceil(float64(v))) } func floor(v float32) int { return int(math.Floor(float64(v))) } // Affine2D represents an affine 2D transformation. The zero value of 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 transformation 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 - 1, b: hx, c: ox, d: hy, e: sy - 1, f: oy, } } // AffineId returns an identity transformation matrix. func AffineId() Affine2D { return NewAffine2D( 1, 0, 0, 0, 1, 0, ) } // 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) { r.a = (A.a+1)*(B.a+1) + A.b*B.d - 1 r.b = (A.a+1)*B.b + A.b*(B.e+1) r.c = (A.a+1)*B.c + A.b*B.f + A.c r.d = A.d*(B.a+1) + (A.e+1)*B.d r.e = A.d*B.b + (A.e+1)*(B.e+1) - 1 r.f = A.d*B.c + (A.e+1)*B.f + A.f return r } // 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 += 1 a.e += 1 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 a.a -= 1 a.e -= 1 return a } // Transform p by returning a*p. func (a Affine2D) Transform(p Point) Point { return Point{ X: p.X*(a.a+1) + p.Y*a.b + a.c, Y: p.X*a.d + p.Y*(a.e+1) + 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) { return a.a + 1, a.b, a.c, a.d, a.e + 1, a.f } // Split a transform into two parts, one which is pure offset and the // other representing the scaling, shearing and rotation part. func (a Affine2D) Split() (srs Affine2D, offset Point) { return Affine2D{ a: a.a, b: a.b, c: 0, d: a.d, e: a.e, f: 0, }, Point{X: a.c, Y: a.f} } func (a Affine2D) scale(factor Point) Affine2D { return Affine2D{ (a.a+1)*factor.X - 1, a.b * factor.X, a.c * factor.X, a.d * factor.Y, (a.e+1)*factor.Y - 1, a.f * factor.Y, } } func (a Affine2D) rotate(radians float32) Affine2D { sin, cos := math.Sincos(float64(radians)) s, c := float32(sin), float32(cos) return Affine2D{ (a.a+1)*c - a.d*s - 1, a.b*c - (a.e+1)*s, a.c*c - a.f*s, (a.a+1)*s + a.d*c, a.b*s + (a.e+1)*c - 1, a.c*s + a.f*c, } } func (a Affine2D) shear(radiansX, radiansY float32) Affine2D { tx := float32(math.Tan(float64(radiansX))) ty := float32(math.Tan(float64(radiansY))) return Affine2D{ (a.a + 1) + a.d*tx - 1, a.b + (a.e+1)*tx, a.c + a.f*tx, (a.a+1)*ty + a.d, a.b*ty + (a.e + 1) - 1, a.c*ty + a.f, } } func (a Affine2D) String() string { sx, hx, ox, hy, sy, oy := a.Elems() const prec = 6 const charsPerFloat = prec + 2 + 1 s := []byte{:0:6*charsPerFloat + 6} s = append(s, '[', '[') s = strconv.AppendFloat(s, float64(sx), 'g', prec, 32) s = append(s, ' ') s = strconv.AppendFloat(s, float64(hx), 'g', prec, 32) s = append(s, ' ') s = strconv.AppendFloat(s, float64(ox), 'g', prec, 32) s = append(s, ']', ' ', '[') s = strconv.AppendFloat(s, float64(hy), 'g', prec, 32) s = append(s, ' ') s = strconv.AppendFloat(s, float64(sy), 'g', prec, 32) s = append(s, ' ') s = strconv.AppendFloat(s, float64(oy), 'g', prec, 32) s = append(s, ']', ']') return s }