diff --git a/freetype/raster/geom.go b/freetype/raster/geom.go index 0f07705..5de33eb 100644 --- a/freetype/raster/geom.go +++ b/freetype/raster/geom.go @@ -10,7 +10,7 @@ import ( "math" ) -// A 24.8 fixed point number. +// A Fixed is a 24.8 fixed point number. type Fixed int32 // String returns a human-readable representation of a 24.8 fixed point number. @@ -37,11 +37,27 @@ func maxAbs(a, b Fixed) Fixed { return a } -// A two-dimensional point or vector, in 24.8 fixed point format. +// A Point represents a two-dimensional point or vector, in 24.8 fixed point +// format. type Point struct { X, Y Fixed } +// Add returns the vector p + q. +func (p Point) Add(q Point) Point { + return Point{p.X + q.X, p.Y + q.Y} +} + +// Sub returns the vector p - q. +func (p Point) Sub(q Point) Point { + return Point{p.X - q.X, p.Y - q.Y} +} + +// Mul returns the vector k * p. +func (p Point) Mul(k Fixed) Point { + return Point{p.X * k / 256, p.Y * k / 256} +} + // Len returns the length of the vector p. func (p Point) Len() Fixed { // TODO(nigeltao): use fixed point math. @@ -62,13 +78,15 @@ func (p Point) Norm(length Fixed) Point { } // RotateCW returns the vector p rotated clockwise by 90 degrees. +// Note that the Y-axis grows downwards, so {1, 0}.RotateCW is {0, 1}. func (p Point) RotateCW() Point { - return Point{p.Y, -p.X} + return Point{-p.Y, p.X} } // RotateCCW returns the vector p rotated counter-clockwise by 90 degrees. +// Note that the Y-axis grows downwards, so {1, 0}.RotateCCW is {0, -1}. func (p Point) RotateCCW() Point { - return Point{-p.Y, p.X} + return Point{p.Y, -p.X} } // An Adder accumulates points on a curve. @@ -97,16 +115,16 @@ func (p Path) String() string { switch p[i] { case 0: s += "S0" + fmt.Sprint([]Fixed(p[i+1:i+3])) - i += 3 + i += 4 case 1: s += "A1" + fmt.Sprint([]Fixed(p[i+1:i+3])) - i += 3 + i += 4 case 2: s += "A2" + fmt.Sprint([]Fixed(p[i+1:i+5])) - i += 5 + i += 6 case 3: s += "A3" + fmt.Sprint([]Fixed(p[i+1:i+7])) - i += 7 + i += 8 default: panic("freetype/raster: bad path") } @@ -134,36 +152,39 @@ func (p *Path) Clear() { // Start starts a new curve at the given point. func (p *Path) Start(a Point) { n := len(*p) - p.grow(3) + p.grow(4) (*p)[n] = 0 (*p)[n+1] = a.X (*p)[n+2] = a.Y + (*p)[n+3] = 0 } // Add1 adds a linear segment to the current curve. func (p *Path) Add1(b Point) { n := len(*p) - p.grow(3) + p.grow(4) (*p)[n] = 1 (*p)[n+1] = b.X (*p)[n+2] = b.Y + (*p)[n+3] = 1 } // Add2 adds a quadratic segment to the current curve. func (p *Path) Add2(b, c Point) { n := len(*p) - p.grow(5) + p.grow(6) (*p)[n] = 2 (*p)[n+1] = b.X (*p)[n+2] = b.Y (*p)[n+3] = c.X (*p)[n+4] = c.Y + (*p)[n+5] = 2 } // Add3 adds a cubic segment to the current curve. func (p *Path) Add3(b, c, d Point) { n := len(*p) - p.grow(7) + p.grow(8) (*p)[n] = 3 (*p)[n+1] = b.X (*p)[n+2] = b.Y @@ -171,6 +192,7 @@ func (p *Path) Add3(b, c, d Point) { (*p)[n+4] = c.Y (*p)[n+5] = d.X (*p)[n+6] = d.Y + (*p)[n+7] = 3 } // AddPath adds the Path q to p. @@ -180,6 +202,9 @@ func (p *Path) AddPath(q Path) { copy((*p)[n:n+m], q) } +// TODO(nigeltao): should a Cap be a func rather than an int, so that callers +// can specify custom cap styles? Similarly for Join. + // A Cap signifies how to begin or end a stroked curve. type Cap int @@ -203,7 +228,8 @@ func (p *Path) AddStroke(q Path, width Fixed, cap Cap, join Join) { Stroke(p, q, width, cap, join) } -// Stroke adds the stroked Path q to p. +// Stroke adds the stroked Path q to p. The resultant stroked path is typically +// self-intersecting and should be rasterized with UseNonZeroWinding. func Stroke(p Adder, q Path, width Fixed, cap Cap, join Join) { if len(q) == 0 { return @@ -212,43 +238,76 @@ func Stroke(p Adder, q Path, width Fixed, cap Cap, join Join) { panic("freetype/raster: bad path") } i := 0 - for j := 3; j < len(q); { + for j := 4; j < len(q); { switch q[j] { case 0: stroke(p, q[i:j], width, cap, join) - i, j = j, j+3 + i, j = j, j+4 case 1: - j += 3 + j += 4 case 2: - j += 5 + j += 6 case 3: - j += 7 + j += 8 } } stroke(p, q[i:len(q)], width, cap, join) } +func addCap(p Adder, cap Cap, center, end Point) { + switch cap { + case RoundCap: + // The cubic Bézier approximation to a circle involves the magic number + // (sqrt(2) - 1) * 4/3, which is approximately 141 / 256. + const k = 141 + d := end.Sub(center) + e := d.RotateCCW() + side := center.Add(e) + start := center.Sub(d) + d, e = d.Mul(k), e.Mul(k) + p.Add3(start.Add(e), side.Sub(d), side) + p.Add3(side.Add(d), end.Add(e), end) + case ButtCap: + p.Add1(end) + case SquareCap: + d := end.Sub(center) + e := d.RotateCCW() + side := center.Add(e) + p.Add1(side.Sub(d)) + p.Add1(side.Add(d)) + p.Add1(end) + } +} + // stroke adds the stroked Path q to p, where q consists of exactly one curve. func stroke(p Adder, q Path, width Fixed, cap Cap, join Join) { - // TODO(nigeltao): replace this placeholder stroking algorithm. It only - // handles linear segments, and it doesn't cap or join but instead only - // fattens each segment independently by half the width, and doesn't - // correct for overlaps. + // Stroking is implemented by deriving two paths each width/2 apart from q. + // The left-hand-side path is added immediately to p; the right-hand-side + // path is accumulated in r, and once we've finished adding the LHS to p + // we add the RHS in reverse order. + r := Path(make([]Fixed, 0, len(q))) + var start Point a := Point{q[1], q[2]} - for i := 3; i < len(q); { + i := 4 + for i < len(q) { switch q[i] { case 1: bx, by := q[i+1], q[i+2] delta := Point{bx - a.X, by - a.Y} normal := delta.Norm(width / 2).RotateCCW() - start := Point{a.X + normal.X, a.Y + normal.Y} - p.Start(start) + if i == 4 { + start = Point{a.X + normal.X, a.Y + normal.Y} + p.Start(start) + r.Start(Point{a.X - normal.X, a.Y - normal.Y}) + } else { + // TODO(nigeltao): handle joins. + p.Add1(Point{a.X + normal.X, a.Y + normal.Y}) + r.Add1(Point{a.X - normal.X, a.Y - normal.Y}) + } p.Add1(Point{bx + normal.X, by + normal.Y}) - p.Add1(Point{bx - normal.X, by - normal.Y}) - p.Add1(Point{a.X - normal.X, a.Y - normal.Y}) - p.Add1(start) + r.Add1(Point{bx - normal.X, by - normal.Y}) a = Point{q[i+1], q[i+2]} - i += 3 + i += 4 case 2: panic("freetype/raster: stroke unimplemented for quadratic segments") case 3: @@ -257,4 +316,33 @@ func stroke(p Adder, q Path, width Fixed, cap Cap, join Join) { panic("freetype/raster: bad path") } } + i = len(r) - 1 + addCap(p, cap, Point{q[len(q)-3], q[len(q)-2]}, Point{r[i-2], r[i-1]}) + // Add r reversed to p. + // For example, if r consists of a linear segment from A to B followed by a + // quadratic segment from B to C to D, then the values of r looks like: + // index: 01234567890123 + // value: 0AA01BB12CCDD2 + // So, when adding r backwards to p, we want to Add2(C, B) followed by Add1(A). +loop: + for { + switch r[i] { + case 0: + break loop + case 1: + i -= 4 + p.Add1(Point{r[i-2], r[i-1]}) + case 2: + i -= 6 + p.Add2(Point{r[i+2], r[i+3]}, Point{r[i-2], r[i-1]}) + case 3: + i -= 8 + p.Add3(Point{r[i+4], r[i+5]}, Point{r[i+2], r[i+3]}, Point{r[i-2], r[i-1]}) + default: + panic("freetype/raster: bad path") + } + } + // TODO(nigeltao): if q is a closed path then we should join the first and + // last segments instead of capping them. + addCap(p, cap, Point{q[1], q[2]}, start) } diff --git a/freetype/raster/raster.go b/freetype/raster/raster.go index d95c8c7..3c678ca 100644 --- a/freetype/raster/raster.go +++ b/freetype/raster/raster.go @@ -426,16 +426,16 @@ func (r *Rasterizer) AddPath(p Path) { switch p[i] { case 0: r.Start(Point{p[i+1], p[i+2]}) - i += 3 + i += 4 case 1: r.Add1(Point{p[i+1], p[i+2]}) - i += 3 + i += 4 case 2: r.Add2(Point{p[i+1], p[i+2]}, Point{p[i+3], p[i+4]}) - i += 5 + i += 6 case 3: r.Add3(Point{p[i+1], p[i+2]}, Point{p[i+3], p[i+4]}, Point{p[i+5], p[i+6]}) - i += 7 + i += 8 default: panic("freetype/raster: bad path") }