diff --git a/vector/acc_test.go b/vector/acc_test.go index 1dce439..0e2ae58 100644 --- a/vector/acc_test.go +++ b/vector/acc_test.go @@ -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 diff --git a/vector/raster_fixed.go b/vector/raster_fixed.go index 086b9fc..ccfa342 100644 --- a/vector/raster_fixed.go +++ b/vector/raster_fixed.go @@ -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 diff --git a/vector/raster_floating.go b/vector/raster_floating.go index 119845a..143b376 100644 --- a/vector/raster_floating.go +++ b/vector/raster_floating.go @@ -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 diff --git a/vector/vector.go b/vector/vector.go index 418a956..852a4f8 100644 --- a/vector/vector.go +++ b/vector/vector.go @@ -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 { @@ -106,9 +94,11 @@ type Rasterizer struct { useFloatingPointMath bool - size image.Point - first f32.Vec2 - pen f32.Vec2 + size image.Point + 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 } diff --git a/vector/vector_test.go b/vector/vector_test.go index f84d040..f32b992 100644 --- a/vector/vector_test.go +++ b/vector/vector_test.go @@ -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 @@ -365,45 +358,47 @@ const ( type benchmarkGlyphDatum struct { // n being 0, 1 or 2 means moveTo, lineTo or quadTo. - n uint32 - p f32.Vec2 - q f32.Vec2 + n uint32 + 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{})