vector: make args float32 pairs instead of f32.Vec2.

The f32.Vec2 type doesn't seem worth it.

Change-Id: I021c7e13d7e2dd261334f4aa7e867df4fd8f1c3e
Reviewed-on: https://go-review.googlesource.com/32772
Reviewed-by: David Crawshaw <crawshaw@golang.org>
This commit is contained in:
Nigel Tao 2016-11-06 16:38:22 +11:00
parent c78039e8ce
commit 98f3e4e74d
5 changed files with 153 additions and 168 deletions

View File

@ -481,11 +481,11 @@ func makeInXxx(height int, useFloatingPointMath bool) *Rasterizer {
for _, d := range data {
switch d.n {
case 0:
z.MoveTo(d.p)
z.MoveTo(d.px, d.py)
case 1:
z.LineTo(d.p)
z.LineTo(d.px, d.py)
case 2:
z.QuadTo(d.p, d.q)
z.QuadTo(d.px, d.py, d.qx, d.qy)
}
}
return z

View File

@ -7,10 +7,6 @@ package vector
// This file contains a fixed point math implementation of the vector
// graphics rasterizer.
import (
"golang.org/x/image/math/f32"
)
const (
// ϕ is the number of binary digits after the fixed point.
//
@ -58,35 +54,35 @@ func fixedMin(x, y int1ϕ) int1ϕ {
func fixedFloor(x int1ϕ) int32 { return int32(x >> ϕ) }
func fixedCeil(x int1ϕ) int32 { return int32((x + fxOneMinusIota) >> ϕ) }
func (z *Rasterizer) fixedLineTo(b f32.Vec2) {
a := z.pen
z.pen = b
func (z *Rasterizer) fixedLineTo(bx, by float32) {
ax, ay := z.penX, z.penY
z.penX, z.penY = bx, by
dir := int1ϕ(1)
if a[1] > b[1] {
dir, a, b = -1, b, a
if ay > by {
dir, ax, ay, bx, by = -1, bx, by, ax, ay
}
// Horizontal line segments yield no change in coverage. Almost horizontal
// segments would yield some change, in ideal math, but the computation
// further below, involving 1 / (b[1] - a[1]), is unstable in fixed point
// math, so we treat the segment as if it was perfectly horizontal.
if b[1]-a[1] <= 0.000001 {
// further below, involving 1 / (by - ay), is unstable in fixed point math,
// so we treat the segment as if it was perfectly horizontal.
if by-ay <= 0.000001 {
return
}
dxdy := (b[0] - a[0]) / (b[1] - a[1])
dxdy := (bx - ax) / (by - ay)
ay := int1ϕ(a[1] * float32(fxOne))
by := int1ϕ(b[1] * float32(fxOne))
ayϕ := int1ϕ(ay * float32(fxOne))
byϕ := int1ϕ(by * float32(fxOne))
x := int1ϕ(a[0] * float32(fxOne))
y := fixedFloor(ay)
yMax := fixedCeil(by)
x := int1ϕ(ax * float32(fxOne))
y := fixedFloor(ayϕ)
yMax := fixedCeil(byϕ)
if yMax > int32(z.size.Y) {
yMax = int32(z.size.Y)
}
width := int32(z.size.X)
for ; y < yMax; y++ {
dy := fixedMin(int1ϕ(y+1)<<ϕ, by) - fixedMax(int1ϕ(y)<<ϕ, ay)
dy := fixedMin(int1ϕ(y+1)<<ϕ, byϕ) - fixedMax(int1ϕ(y)<<ϕ, ayϕ)
xNext := x + int1ϕ(float32(dy)*dxdy)
if y < 0 {
x = xNext

View File

@ -9,8 +9,6 @@ package vector
import (
"math"
"golang.org/x/image/math/f32"
)
func floatingMax(x, y float32) float32 {
@ -30,32 +28,32 @@ func floatingMin(x, y float32) float32 {
func floatingFloor(x float32) int32 { return int32(math.Floor(float64(x))) }
func floatingCeil(x float32) int32 { return int32(math.Ceil(float64(x))) }
func (z *Rasterizer) floatingLineTo(b f32.Vec2) {
a := z.pen
z.pen = b
func (z *Rasterizer) floatingLineTo(bx, by float32) {
ax, ay := z.penX, z.penY
z.penX, z.penY = bx, by
dir := float32(1)
if a[1] > b[1] {
dir, a, b = -1, b, a
if ay > by {
dir, ax, ay, bx, by = -1, bx, by, ax, ay
}
// Horizontal line segments yield no change in coverage. Almost horizontal
// segments would yield some change, in ideal math, but the computation
// further below, involving 1 / (b[1] - a[1]), is unstable in floating
// point math, so we treat the segment as if it was perfectly horizontal.
if b[1]-a[1] <= 0.000001 {
// further below, involving 1 / (by - ay), is unstable in floating point
// math, so we treat the segment as if it was perfectly horizontal.
if by-ay <= 0.000001 {
return
}
dxdy := (b[0] - a[0]) / (b[1] - a[1])
dxdy := (bx - ax) / (by - ay)
x := a[0]
y := floatingFloor(a[1])
yMax := floatingCeil(b[1])
x := ax
y := floatingFloor(ay)
yMax := floatingCeil(by)
if yMax > int32(z.size.Y) {
yMax = int32(z.size.Y)
}
width := int32(z.size.X)
for ; y < yMax; y++ {
dy := floatingMin(float32(y+1), b[1]) - floatingMax(float32(y), a[1])
dy := floatingMin(float32(y+1), by) - floatingMax(float32(y), ay)
xNext := x + dy*dxdy
if y < 0 {
x = xNext

View File

@ -26,8 +26,6 @@ import (
"image/color"
"image/draw"
"math"
"golang.org/x/image/math/f32"
)
// floatingPointMathThreshold is the width or height above which the rasterizer
@ -50,18 +48,8 @@ import (
// would still produce acceptable quality, but 512 seems to work.
const floatingPointMathThreshold = 512
func midPoint(p, q f32.Vec2) f32.Vec2 {
return f32.Vec2{
(p[0] + q[0]) * 0.5,
(p[1] + q[1]) * 0.5,
}
}
func lerp(t float32, p, q f32.Vec2) f32.Vec2 {
return f32.Vec2{
p[0] + t*(q[0]-p[0]),
p[1] + t*(q[1]-p[1]),
}
func lerp(t, px, py, qx, qy float32) (x, y float32) {
return px + t*(qx-px), py + t*(qy-py)
}
func clamp(i, width int32) uint {
@ -107,8 +95,10 @@ type Rasterizer struct {
useFloatingPointMath bool
size image.Point
first f32.Vec2
pen f32.Vec2
firstX float32
firstY float32
penX float32
penY float32
// DrawOp is the operator used for the Draw method.
//
@ -124,8 +114,10 @@ type Rasterizer struct {
// This includes setting z.DrawOp to draw.Over.
func (z *Rasterizer) Reset(w, h int) {
z.size = image.Point{w, h}
z.first = f32.Vec2{}
z.pen = f32.Vec2{}
z.firstX = 0
z.firstY = 0
z.penX = 0
z.penY = 0
z.DrawOp = draw.Over
z.setUseFloatingPointMath(w > floatingPointMathThreshold || h > floatingPointMathThreshold)
@ -169,63 +161,66 @@ func (z *Rasterizer) Bounds() image.Rectangle {
// Pen returns the location of the path-drawing pen: the last argument to the
// most recent XxxTo call.
func (z *Rasterizer) Pen() f32.Vec2 {
return z.pen
func (z *Rasterizer) Pen() (x, y float32) {
return z.penX, z.penY
}
// ClosePath closes the current path.
func (z *Rasterizer) ClosePath() {
z.LineTo(z.first)
z.LineTo(z.firstX, z.firstY)
}
// MoveTo starts a new path and moves the pen to a.
// MoveTo starts a new path and moves the pen to (ax, ay).
//
// The coordinates are allowed to be out of the Rasterizer's bounds.
func (z *Rasterizer) MoveTo(a f32.Vec2) {
z.first = a
z.pen = a
func (z *Rasterizer) MoveTo(ax, ay float32) {
z.firstX = ax
z.firstY = ay
z.penX = ax
z.penY = ay
}
// LineTo adds a line segment, from the pen to b, and moves the pen to b.
// LineTo adds a line segment, from the pen to (bx, by), and moves the pen to
// (bx, by).
//
// The coordinates are allowed to be out of the Rasterizer's bounds.
func (z *Rasterizer) LineTo(b f32.Vec2) {
func (z *Rasterizer) LineTo(bx, by float32) {
if z.useFloatingPointMath {
z.floatingLineTo(b)
z.floatingLineTo(bx, by)
} else {
z.fixedLineTo(b)
z.fixedLineTo(bx, by)
}
}
// QuadTo adds a quadratic Bézier segment, from the pen via b to c, and moves
// the pen to c.
// QuadTo adds a quadratic Bézier segment, from the pen via (bx, by) to (cx,
// cy), and moves the pen to (cx, cy).
//
// The coordinates are allowed to be out of the Rasterizer's bounds.
func (z *Rasterizer) QuadTo(b, c f32.Vec2) {
a := z.pen
devsq := devSquared(a, b, c)
func (z *Rasterizer) QuadTo(bx, by, cx, cy float32) {
ax, ay := z.penX, z.penY
devsq := devSquared(ax, ay, bx, by, cx, cy)
if devsq >= 0.333 {
const tol = 3
n := 1 + int(math.Sqrt(math.Sqrt(tol*float64(devsq))))
t, nInv := float32(0), 1/float32(n)
for i := 0; i < n-1; i++ {
t += nInv
ab := lerp(t, a, b)
bc := lerp(t, b, c)
z.LineTo(lerp(t, ab, bc))
abx, aby := lerp(t, ax, ay, bx, by)
bcx, bcy := lerp(t, bx, by, cx, cy)
z.LineTo(lerp(t, abx, aby, bcx, bcy))
}
}
z.LineTo(c)
z.LineTo(cx, cy)
}
// CubeTo adds a cubic Bézier segment, from the pen via b and c to d, and moves
// the pen to d.
// CubeTo adds a cubic Bézier segment, from the pen via (bx, by) and (cx, cy)
// to (dx, dy), and moves the pen to (dx, dy).
//
// The coordinates are allowed to be out of the Rasterizer's bounds.
func (z *Rasterizer) CubeTo(b, c, d f32.Vec2) {
a := z.pen
devsq := devSquared(a, b, d)
if devsqAlt := devSquared(a, c, d); devsq < devsqAlt {
func (z *Rasterizer) CubeTo(bx, by, cx, cy, dx, dy float32) {
ax, ay := z.penX, z.penY
devsq := devSquared(ax, ay, bx, by, dx, dy)
if devsqAlt := devSquared(ax, ay, cx, cy, dx, dy); devsq < devsqAlt {
devsq = devsqAlt
}
if devsq >= 0.333 {
@ -234,19 +229,20 @@ func (z *Rasterizer) CubeTo(b, c, d f32.Vec2) {
t, nInv := float32(0), 1/float32(n)
for i := 0; i < n-1; i++ {
t += nInv
ab := lerp(t, a, b)
bc := lerp(t, b, c)
cd := lerp(t, c, d)
abc := lerp(t, ab, bc)
bcd := lerp(t, bc, cd)
z.LineTo(lerp(t, abc, bcd))
abx, aby := lerp(t, ax, ay, bx, by)
bcx, bcy := lerp(t, bx, by, cx, cy)
cdx, cdy := lerp(t, cx, cy, dx, dy)
abcx, abcy := lerp(t, abx, aby, bcx, bcy)
bcdx, bcdy := lerp(t, bcx, bcy, cdx, cdy)
z.LineTo(lerp(t, abcx, abcy, bcdx, bcdy))
}
}
z.LineTo(d)
z.LineTo(dx, dy)
}
// devSquared returns a measure of how curvy the sequnce a to b to c is. It
// determines how many line segments will approximate a Bézier curve segment.
// devSquared returns a measure of how curvy the sequence (ax, ay) to (bx, by)
// to (cx, cy) is. It determines how many line segments will approximate a
// Bézier curve segment.
//
// http://lists.nongnu.org/archive/html/freetype-devel/2016-08/msg00080.html
// gives the rationale for this evenly spaced heuristic instead of a recursive
@ -258,9 +254,9 @@ func (z *Rasterizer) CubeTo(b, c, d f32.Vec2) {
// Taking a circular arc as a simplifying assumption (ie a spherical cow),
// where I get n, a recursive approach would get 2^⌈lg n⌉, which, if I haven't
// made any horrible mistakes, is expected to be 33% more in the limit.
func devSquared(a, b, c f32.Vec2) float32 {
devx := a[0] - 2*b[0] + c[0]
devy := a[1] - 2*b[1] + c[1]
func devSquared(ax, ay, bx, by, cx, cy float32) float32 {
devx := ax - 2*bx + cx
devy := ay - 2*by + cy
return devx*devx + devy*devy
}

View File

@ -17,8 +17,6 @@ import (
"os"
"path/filepath"
"testing"
"golang.org/x/image/math/f32"
)
// encodePNG is useful for manually debugging the tests.
@ -35,15 +33,13 @@ func encodePNG(dstFilename string, src image.Image) error {
return closeErr
}
func pointOnCircle(center, radius, index, number int) f32.Vec2 {
func pointOnCircle(center, radius, index, number int) (x, y float32) {
c := float64(center)
r := float64(radius)
i := float64(index)
n := float64(number)
return f32.Vec2{
float32(c + r*(math.Cos(2*math.Pi*i/n))),
float32(c + r*(math.Sin(2*math.Pi*i/n))),
}
return float32(c + r*(math.Cos(2*math.Pi*i/n))),
float32(c + r*(math.Sin(2*math.Pi*i/n)))
}
func TestRasterizeOutOfBounds(t *testing.T) {
@ -59,15 +55,15 @@ func TestRasterizeOutOfBounds(t *testing.T) {
for i := 0; i < n; i++ {
for j := 1; j < n/2; j++ {
z.Reset(2*center, 2*center)
z.MoveTo(f32.Vec2{1 * center, 1 * center})
z.MoveTo(1*center, 1*center)
z.LineTo(pointOnCircle(center, radius, i+0, n))
z.LineTo(pointOnCircle(center, radius, i+j, n))
z.ClosePath()
z.MoveTo(f32.Vec2{0 * center, 0 * center})
z.LineTo(f32.Vec2{0 * center, 2 * center})
z.LineTo(f32.Vec2{2 * center, 2 * center})
z.LineTo(f32.Vec2{2 * center, 0 * center})
z.MoveTo(0*center, 0*center)
z.LineTo(0*center, 2*center)
z.LineTo(2*center, 2*center)
z.LineTo(2*center, 0*center)
z.ClosePath()
dst := image.NewAlpha(z.Bounds())
@ -91,10 +87,7 @@ func TestRasterizePolygon(t *testing.T) {
for radius := 4; radius <= 256; radius *= 2 {
for n := 3; n <= 19; n += 4 {
z.Reset(2*radius, 2*radius)
z.MoveTo(f32.Vec2{
float32(2 * radius),
float32(1 * radius),
})
z.MoveTo(float32(2*radius), float32(1*radius))
for i := 1; i < n; i++ {
z.LineTo(pointOnCircle(radius, radius, i, n))
}
@ -112,10 +105,10 @@ func TestRasterizePolygon(t *testing.T) {
func TestRasterizeAlmostAxisAligned(t *testing.T) {
z := NewRasterizer(8, 8)
z.MoveTo(f32.Vec2{2, 2})
z.LineTo(f32.Vec2{6, math.Nextafter32(2, 0)})
z.LineTo(f32.Vec2{6, 6})
z.LineTo(f32.Vec2{math.Nextafter32(2, 0), 6})
z.MoveTo(2, 2)
z.LineTo(6, math.Nextafter32(2, 0))
z.LineTo(6, 6)
z.LineTo(math.Nextafter32(2, 0), 6)
z.ClosePath()
dst := image.NewAlpha(z.Bounds())
@ -132,10 +125,10 @@ func TestRasterizeWideAlmostHorizontalLines(t *testing.T) {
x := float32(int(1 << i))
z.Reset(8, 8)
z.MoveTo(f32.Vec2{-x, 3})
z.LineTo(f32.Vec2{+x, 4})
z.LineTo(f32.Vec2{+x, 6})
z.LineTo(f32.Vec2{-x, 6})
z.MoveTo(-x, 3)
z.LineTo(+x, 4)
z.LineTo(+x, 6)
z.LineTo(-x, 6)
z.ClosePath()
dst := image.NewAlpha(z.Bounds())
@ -149,9 +142,9 @@ func TestRasterizeWideAlmostHorizontalLines(t *testing.T) {
func TestRasterize30Degrees(t *testing.T) {
z := NewRasterizer(8, 8)
z.MoveTo(f32.Vec2{4, 4})
z.LineTo(f32.Vec2{8, 4})
z.LineTo(f32.Vec2{4, 6})
z.MoveTo(4, 4)
z.LineTo(8, 4)
z.LineTo(4, 6)
z.ClosePath()
dst := image.NewAlpha(z.Bounds())
@ -168,11 +161,11 @@ func TestRasterizeRandomLineTos(t *testing.T) {
n, rng := 0, rand.New(rand.NewSource(int64(i)))
z.Reset(i+2, i+2)
z.MoveTo(f32.Vec2{float32(i / 2), float32(i / 2)})
z.MoveTo(float32(i/2), float32(i/2))
for ; rng.Intn(16) != 0; n++ {
x := 1 + rng.Intn(i)
y := 1 + rng.Intn(i)
z.LineTo(f32.Vec2{float32(x), float32(y)})
z.LineTo(float32(x), float32(y))
}
z.ClosePath()
@ -235,10 +228,10 @@ var basicMask = []byte{
func testBasicPath(t *testing.T, prefix string, dst draw.Image, src image.Image, op draw.Op, want []byte) {
z := NewRasterizer(16, 16)
z.MoveTo(f32.Vec2{2, 2})
z.LineTo(f32.Vec2{8, 2})
z.QuadTo(f32.Vec2{14, 2}, f32.Vec2{14, 14})
z.CubeTo(f32.Vec2{8, 2}, f32.Vec2{5, 20}, f32.Vec2{2, 8})
z.MoveTo(2, 2)
z.LineTo(8, 2)
z.QuadTo(14, 2, 14, 14)
z.CubeTo(8, 2, 5, 20, 2, 8)
z.ClosePath()
z.DrawOp = op
@ -366,44 +359,46 @@ const (
type benchmarkGlyphDatum struct {
// n being 0, 1 or 2 means moveTo, lineTo or quadTo.
n uint32
p f32.Vec2
q f32.Vec2
px float32
py float32
qx float32
qy float32
}
// benchmarkGlyphData is the 'a' glyph from the Roboto Regular font, translated
// so that its top left corner is (0, 0).
var benchmarkGlyphData = []benchmarkGlyphDatum{
{0, f32.Vec2{699, 1102}, f32.Vec2{0, 0}},
{2, f32.Vec2{683, 1070}, f32.Vec2{673, 988}},
{2, f32.Vec2{544, 1122}, f32.Vec2{365, 1122}},
{2, f32.Vec2{205, 1122}, f32.Vec2{102.5, 1031.5}},
{2, f32.Vec2{0, 941}, f32.Vec2{0, 802}},
{2, f32.Vec2{0, 633}, f32.Vec2{128.5, 539.5}},
{2, f32.Vec2{257, 446}, f32.Vec2{490, 446}},
{1, f32.Vec2{670, 446}, f32.Vec2{0, 0}},
{1, f32.Vec2{670, 361}, f32.Vec2{0, 0}},
{2, f32.Vec2{670, 264}, f32.Vec2{612, 206.5}},
{2, f32.Vec2{554, 149}, f32.Vec2{441, 149}},
{2, f32.Vec2{342, 149}, f32.Vec2{275, 199}},
{2, f32.Vec2{208, 249}, f32.Vec2{208, 320}},
{1, f32.Vec2{22, 320}, f32.Vec2{0, 0}},
{2, f32.Vec2{22, 239}, f32.Vec2{79.5, 163.5}},
{2, f32.Vec2{137, 88}, f32.Vec2{235.5, 44}},
{2, f32.Vec2{334, 0}, f32.Vec2{452, 0}},
{2, f32.Vec2{639, 0}, f32.Vec2{745, 93.5}},
{2, f32.Vec2{851, 187}, f32.Vec2{855, 351}},
{1, f32.Vec2{855, 849}, f32.Vec2{0, 0}},
{2, f32.Vec2{855, 998}, f32.Vec2{893, 1086}},
{1, f32.Vec2{893, 1102}, f32.Vec2{0, 0}},
{1, f32.Vec2{699, 1102}, f32.Vec2{0, 0}},
{0, f32.Vec2{392, 961}, f32.Vec2{0, 0}},
{2, f32.Vec2{479, 961}, f32.Vec2{557, 916}},
{2, f32.Vec2{635, 871}, f32.Vec2{670, 799}},
{1, f32.Vec2{670, 577}, f32.Vec2{0, 0}},
{1, f32.Vec2{525, 577}, f32.Vec2{0, 0}},
{2, f32.Vec2{185, 577}, f32.Vec2{185, 776}},
{2, f32.Vec2{185, 863}, f32.Vec2{243, 912}},
{2, f32.Vec2{301, 961}, f32.Vec2{392, 961}},
{0, 699, 1102, 0, 0},
{2, 683, 1070, 673, 988},
{2, 544, 1122, 365, 1122},
{2, 205, 1122, 102.5, 1031.5},
{2, 0, 941, 0, 802},
{2, 0, 633, 128.5, 539.5},
{2, 257, 446, 490, 446},
{1, 670, 446, 0, 0},
{1, 670, 361, 0, 0},
{2, 670, 264, 612, 206.5},
{2, 554, 149, 441, 149},
{2, 342, 149, 275, 199},
{2, 208, 249, 208, 320},
{1, 22, 320, 0, 0},
{2, 22, 239, 79.5, 163.5},
{2, 137, 88, 235.5, 44},
{2, 334, 0, 452, 0},
{2, 639, 0, 745, 93.5},
{2, 851, 187, 855, 351},
{1, 855, 849, 0, 0},
{2, 855, 998, 893, 1086},
{1, 893, 1102, 0, 0},
{1, 699, 1102, 0, 0},
{0, 392, 961, 0, 0},
{2, 479, 961, 557, 916},
{2, 635, 871, 670, 799},
{1, 670, 577, 0, 0},
{1, 525, 577, 0, 0},
{2, 185, 577, 185, 776},
{2, 185, 863, 243, 912},
{2, 301, 961, 392, 961},
}
func scaledBenchmarkGlyphData(height int) (width int, data []benchmarkGlyphDatum) {
@ -412,10 +407,10 @@ func scaledBenchmarkGlyphData(height int) (width int, data []benchmarkGlyphDatum
// Clone the benchmarkGlyphData slice and scale its coordinates.
data = append(data, benchmarkGlyphData...)
for i := range data {
data[i].p[0] *= scale
data[i].p[1] *= scale
data[i].q[0] *= scale
data[i].q[1] *= scale
data[i].px *= scale
data[i].py *= scale
data[i].qx *= scale
data[i].qy *= scale
}
return int(math.Ceil(float64(benchmarkGlyphWidth * scale))), data
@ -457,11 +452,11 @@ func benchGlyph(b *testing.B, colorModel byte, loose bool, height int, op draw.O
for _, d := range data {
switch d.n {
case 0:
z.MoveTo(d.p)
z.MoveTo(d.px, d.py)
case 1:
z.LineTo(d.p)
z.LineTo(d.px, d.py)
case 2:
z.QuadTo(d.p, d.q)
z.QuadTo(d.px, d.py, d.qx, d.qy)
}
}
z.Draw(dst, bounds, src, image.Point{})