diff --git a/vector/raster_floating.go b/vector/raster_floating.go index 8171edc..721cfcb 100644 --- a/vector/raster_floating.go +++ b/vector/raster_floating.go @@ -61,7 +61,7 @@ func (z *Rasterizer) floatingLineTo(b f32.Vec2) { x = xNext continue } - buf := z.area[y*width:] + buf := z.bufF32[y*width:] d := dy * dir x0, x1 := x, xNext if x > xNext { @@ -178,3 +178,18 @@ func floatingAccumulateOpOver(dst []uint8, src []float32) { dst[i] = uint8(outA >> 8) } } + +func floatingAccumulateMask(dst []uint32, src []float32) { + acc := float32(0) + for i, v := range src { + acc += v + a := acc + if a < 0 { + a = -a + } + if a > 1 { + a = 1 + } + dst[i] = uint32(almost65536 * a) + } +} diff --git a/vector/vector.go b/vector/vector.go index 2a47905..a4504a0 100644 --- a/vector/vector.go +++ b/vector/vector.go @@ -18,6 +18,7 @@ package vector // import "golang.org/x/image/vector" import ( "image" + "image/color" "image/draw" "math" @@ -52,14 +53,32 @@ func clamp(i, width int32) uint { // by the given width and height. func NewRasterizer(w, h int) *Rasterizer { return &Rasterizer{ - area: make([]float32, w*h), - size: image.Point{w, h}, + bufF32: make([]float32, w*h), + size: image.Point{w, h}, } } // Raster is a 2-D vector graphics rasterizer. type Rasterizer struct { - area []float32 + // bufXxx are buffers of float32 or uint32 values, holding either the + // individual or cumulative area values. + // + // We don't actually need both values at any given time, and to conserve + // memory, the integration of the individual to the cumulative could modify + // the buffer in place. In other words, we could use a single buffer, say + // of type []uint32, and add some math.Float32bits and math.Float32frombits + // calls to satisfy the compiler's type checking. As of Go 1.7, though, + // there is a performance penalty between: + // bufF32[i] += x + // and + // bufU32[i] = math.Float32bits(x + math.Float32frombits(bufU32[i])) + // + // See golang.org/issue/17220 for some discussion. + // + // TODO: use bufU32 in the fixed point math implementation. + bufF32 []float32 + bufU32 []uint32 + size image.Point first f32.Vec2 pen f32.Vec2 @@ -77,12 +96,12 @@ type Rasterizer struct { // // This includes setting z.DrawOp to draw.Over. func (z *Rasterizer) Reset(w, h int) { - if n := w * h; n > cap(z.area) { - z.area = make([]float32, n) + if n := w * h; n > cap(z.bufF32) { + z.bufF32 = make([]float32, n) } else { - z.area = z.area[:n] - for i := range z.area { - z.area[i] = 0 + z.bufF32 = z.bufF32[:n] + for i := range z.bufF32 { + z.bufF32[i] = 0 } } z.size = image.Point{w, h} @@ -202,6 +221,9 @@ func devSquared(a, b, c f32.Vec2) float32 { // The vector paths previously added via the XxxTo calls become the mask for // drawing src onto dst. func (z *Rasterizer) Draw(dst draw.Image, r image.Rectangle, src image.Image, sp image.Point) { + // TODO: adjust r and sp (and mp?) if src.Bounds() doesn't contain + // r.Add(sp.Sub(r.Min)). + if src, ok := src.(*image.Uniform); ok { _, _, _, srcA := src.RGBA() switch dst := dst.(type) { @@ -217,7 +239,19 @@ func (z *Rasterizer) Draw(dst draw.Image, r image.Rectangle, src image.Image, sp } } } - println("TODO: the general case") + + if n := z.size.X * z.size.Y; n > cap(z.bufU32) { + z.bufU32 = make([]uint32, n) + } else { + z.bufU32 = z.bufU32[:n] + } + floatingAccumulateMask(z.bufU32, z.bufF32) + + if z.DrawOp == draw.Over { + z.rasterizeOpOver(dst, r, src, sp) + } else { + z.rasterizeOpSrc(dst, r, src, sp) + } } func (z *Rasterizer) rasterizeDstAlphaSrcOpaqueOpSrc(dst *image.Alpha, r image.Rectangle) { @@ -225,7 +259,7 @@ func (z *Rasterizer) rasterizeDstAlphaSrcOpaqueOpSrc(dst *image.Alpha, r image.R // TODO: add a fixed point math implementation. // TODO: non-zero vs even-odd winding? if r == dst.Bounds() && r == z.Bounds() { - floatingAccumulateOpSrc(dst.Pix, z.area) + floatingAccumulateOpSrc(dst.Pix, z.bufF32) return } println("TODO: the general case") @@ -236,8 +270,50 @@ func (z *Rasterizer) rasterizeDstAlphaSrcOpaqueOpOver(dst *image.Alpha, r image. // TODO: add a fixed point math implementation. // TODO: non-zero vs even-odd winding? if r == dst.Bounds() && r == z.Bounds() { - floatingAccumulateOpOver(dst.Pix, z.area) + floatingAccumulateOpOver(dst.Pix, z.bufF32) return } println("TODO: the general case") } + +func (z *Rasterizer) rasterizeOpOver(dst draw.Image, r image.Rectangle, src image.Image, sp image.Point) { + out := color.RGBA64{} + outc := color.Color(&out) + for y, y1 := 0, r.Max.Y-r.Min.Y; y < y1; y++ { + for x, x1 := 0, r.Max.X-r.Min.X; x < x1; x++ { + sr, sg, sb, sa := src.At(sp.X+x, sp.Y+y).RGBA() + ma := z.bufU32[y*z.size.X+x] + + // This algorithm comes from the standard library's image/draw + // package. + dr, dg, db, da := dst.At(r.Min.X+x, r.Min.Y+y).RGBA() + a := 0xffff - (sa * ma / 0xffff) + out.R = uint16((dr*a + sr*ma) / 0xffff) + out.G = uint16((dg*a + sg*ma) / 0xffff) + out.B = uint16((db*a + sb*ma) / 0xffff) + out.A = uint16((da*a + sa*ma) / 0xffff) + + dst.Set(r.Min.X+x, r.Min.Y+y, outc) + } + } +} + +func (z *Rasterizer) rasterizeOpSrc(dst draw.Image, r image.Rectangle, src image.Image, sp image.Point) { + out := color.RGBA64{} + outc := color.Color(&out) + for y, y1 := 0, r.Max.Y-r.Min.Y; y < y1; y++ { + for x, x1 := 0, r.Max.X-r.Min.X; x < x1; x++ { + sr, sg, sb, sa := src.At(sp.X+x, sp.Y+y).RGBA() + ma := z.bufU32[y*z.size.X+x] + + // This algorithm comes from the standard library's image/draw + // package. + out.R = uint16(sr * ma / 0xffff) + out.G = uint16(sg * ma / 0xffff) + out.B = uint16(sb * ma / 0xffff) + out.A = uint16(sa * ma / 0xffff) + + dst.Set(r.Min.X+x, r.Min.Y+y, outc) + } + } +} diff --git a/vector/vector_test.go b/vector/vector_test.go index 604b8c1..354de33 100644 --- a/vector/vector_test.go +++ b/vector/vector_test.go @@ -32,35 +32,39 @@ func encodePNG(dstFilename string, src image.Image) error { return closeErr } -func TestBasicPath(t *testing.T) { - mask := []byte{ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xaa, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x5f, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x24, 0x00, 0x00, 0x00, - 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xa1, 0x00, 0x00, 0x00, - 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x14, 0x00, 0x00, - 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x4a, 0x00, 0x00, - 0x00, 0x00, 0xcc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x81, 0x00, 0x00, - 0x00, 0x00, 0x66, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xe4, 0xff, 0xff, 0xff, 0xb6, 0x00, 0x00, - 0x00, 0x00, 0x0c, 0xf2, 0xff, 0xff, 0xfe, 0x9e, 0x15, 0x00, 0x15, 0x96, 0xff, 0xce, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x88, 0xfc, 0xe3, 0x43, 0x00, 0x00, 0x00, 0x00, 0x06, 0xcd, 0xdc, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x10, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x25, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - } +var basicMask = []byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xaa, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x5f, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x24, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xa1, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x14, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x4a, 0x00, 0x00, + 0x00, 0x00, 0xcc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x81, 0x00, 0x00, + 0x00, 0x00, 0x66, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xe4, 0xff, 0xff, 0xff, 0xb6, 0x00, 0x00, + 0x00, 0x00, 0x0c, 0xf2, 0xff, 0xff, 0xfe, 0x9e, 0x15, 0x00, 0x15, 0x96, 0xff, 0xce, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x88, 0xfc, 0xe3, 0x43, 0x00, 0x00, 0x00, 0x00, 0x06, 0xcd, 0xdc, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x10, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x25, 0xde, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +} +func basicRasterizer() *Rasterizer { + 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.ClosePath() + return z +} + +func TestBasicPathDstAlpha(t *testing.T) { for _, background := range []uint8{0x00, 0x80} { for _, op := range []draw.Op{draw.Over, draw.Src} { - 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.ClosePath() - + z := basicRasterizer() dst := image.NewAlpha(z.Bounds()) for i := range dst.Pix { dst.Pix[i] = background @@ -69,13 +73,13 @@ func TestBasicPath(t *testing.T) { z.Draw(dst, dst.Bounds(), image.Opaque, image.Point{}) got := dst.Pix - want := make([]byte, len(mask)) + want := make([]byte, len(basicMask)) if op == draw.Over && background == 0x80 { - for i, ma := range mask { + for i, ma := range basicMask { want[i] = 0xff - (0xff-ma)/2 } } else { - copy(want, mask) + copy(want, basicMask) } if len(got) != len(want) { @@ -96,6 +100,66 @@ func TestBasicPath(t *testing.T) { } } +func TestBasicPathDstRGBA(t *testing.T) { + blue := color.RGBA{0x00, 0x00, 0xff, 0xff} + + for _, op := range []draw.Op{draw.Over, draw.Src} { + z := basicRasterizer() + dst := image.NewRGBA(z.Bounds()) + for y := 0; y < 16; y++ { + for x := 0; x < 16; x++ { + dst.SetRGBA(x, y, color.RGBA{ + R: uint8(y*0x11) / 2, + G: uint8(x*0x11) / 2, + B: 0x00, + A: 0x80, + }) + } + } + z.DrawOp = op + z.Draw(dst, dst.Bounds(), image.NewUniform(blue), image.Point{}) + got := dst.Pix + + want := make([]byte, len(basicMask)*4) + if op == draw.Over { + for y := 0; y < 16; y++ { + for x := 0; x < 16; x++ { + i := 16*y + x + ma := basicMask[i] + want[4*i+0] = uint8((uint32(0xff-ma) * uint32(y*0x11/2)) / 0xff) + want[4*i+1] = uint8((uint32(0xff-ma) * uint32(x*0x11/2)) / 0xff) + want[4*i+2] = ma + want[4*i+3] = ma/2 + 0x80 + } + } + } else { + for y := 0; y < 16; y++ { + for x := 0; x < 16; x++ { + i := 16*y + x + ma := basicMask[i] + want[4*i+0] = 0x00 + want[4*i+1] = 0x00 + want[4*i+2] = ma + want[4*i+3] = ma + } + } + } + + if len(got) != len(want) { + t.Errorf("op=%v: len(got)=%d and len(want)=%d differ", op, len(got), len(want)) + continue + } + for i := range got { + delta := int(got[i]) - int(want[i]) + // The +/- 2 allows different implementations to give different + // rounding errors. + if delta < -2 || +2 < delta { + t.Errorf("op=%v: i=%d: got %#02x, want %#02x", op, i, got[i], want[i]) + } + } + } +} + const ( benchmarkGlyphWidth = 893 benchmarkGlyphHeight = 1122 @@ -167,6 +231,12 @@ func benchGlyph(b *testing.B, cm color.Model, height int, op draw.Op) { case color.AlphaModel: dst = image.NewAlpha(z.Bounds()) src = image.Opaque + case color.NRGBAModel: + dst = image.NewNRGBA(z.Bounds()) + src = image.NewUniform(color.NRGBA{0x40, 0x80, 0xc0, 0xff}) + case color.RGBAModel: + dst = image.NewRGBA(z.Bounds()) + src = image.NewUniform(color.RGBA{0x40, 0x80, 0xc0, 0xff}) default: b.Fatal("unsupported color model") } @@ -200,4 +270,24 @@ func BenchmarkGlyphAlpha128Src(b *testing.B) { benchGlyph(b, color.AlphaModel, func BenchmarkGlyphAlpha256Over(b *testing.B) { benchGlyph(b, color.AlphaModel, 256, draw.Over) } func BenchmarkGlyphAlpha256Src(b *testing.B) { benchGlyph(b, color.AlphaModel, 256, draw.Src) } -// TODO: color.RGBAModel benchmarks. +func BenchmarkGlyphNRGBA16Over(b *testing.B) { benchGlyph(b, color.NRGBAModel, 16, draw.Over) } +func BenchmarkGlyphNRGBA16Src(b *testing.B) { benchGlyph(b, color.NRGBAModel, 16, draw.Src) } +func BenchmarkGlyphNRGBA32Over(b *testing.B) { benchGlyph(b, color.NRGBAModel, 32, draw.Over) } +func BenchmarkGlyphNRGBA32Src(b *testing.B) { benchGlyph(b, color.NRGBAModel, 32, draw.Src) } +func BenchmarkGlyphNRGBA64Over(b *testing.B) { benchGlyph(b, color.NRGBAModel, 64, draw.Over) } +func BenchmarkGlyphNRGBA64Src(b *testing.B) { benchGlyph(b, color.NRGBAModel, 64, draw.Src) } +func BenchmarkGlyphNRGBA128Over(b *testing.B) { benchGlyph(b, color.NRGBAModel, 128, draw.Over) } +func BenchmarkGlyphNRGBA128Src(b *testing.B) { benchGlyph(b, color.NRGBAModel, 128, draw.Src) } +func BenchmarkGlyphNRGBA256Over(b *testing.B) { benchGlyph(b, color.NRGBAModel, 256, draw.Over) } +func BenchmarkGlyphNRGBA256Src(b *testing.B) { benchGlyph(b, color.NRGBAModel, 256, draw.Src) } + +func BenchmarkGlyphRGBA16Over(b *testing.B) { benchGlyph(b, color.RGBAModel, 16, draw.Over) } +func BenchmarkGlyphRGBA16Src(b *testing.B) { benchGlyph(b, color.RGBAModel, 16, draw.Src) } +func BenchmarkGlyphRGBA32Over(b *testing.B) { benchGlyph(b, color.RGBAModel, 32, draw.Over) } +func BenchmarkGlyphRGBA32Src(b *testing.B) { benchGlyph(b, color.RGBAModel, 32, draw.Src) } +func BenchmarkGlyphRGBA64Over(b *testing.B) { benchGlyph(b, color.RGBAModel, 64, draw.Over) } +func BenchmarkGlyphRGBA64Src(b *testing.B) { benchGlyph(b, color.RGBAModel, 64, draw.Src) } +func BenchmarkGlyphRGBA128Over(b *testing.B) { benchGlyph(b, color.RGBAModel, 128, draw.Over) } +func BenchmarkGlyphRGBA128Src(b *testing.B) { benchGlyph(b, color.RGBAModel, 128, draw.Src) } +func BenchmarkGlyphRGBA256Over(b *testing.B) { benchGlyph(b, color.RGBAModel, 256, draw.Over) } +func BenchmarkGlyphRGBA256Src(b *testing.B) { benchGlyph(b, color.RGBAModel, 256, draw.Src) }