From db5b86472e9930de825937a7d862c6c554588ea6 Mon Sep 17 00:00:00 2001 From: wsw Date: Sun, 20 Nov 2016 20:31:05 +0800 Subject: [PATCH 1/6] Pattern, solid(color) and surface(image) --- context.go | 76 +++++++++++++++--------- examples/fill-pattern.go | 20 +++++++ pattern.go | 121 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 28 deletions(-) create mode 100644 examples/fill-pattern.go create mode 100644 pattern.go diff --git a/context.go b/context.go index e186027..d62e2cb 100644 --- a/context.go +++ b/context.go @@ -45,25 +45,27 @@ const ( ) type Context struct { - width int - height int - im *image.RGBA - mask *image.Alpha - color color.Color - strokePath raster.Path - fillPath raster.Path - start Point - current Point - hasCurrent bool - dashes []float64 - lineWidth float64 - lineCap LineCap - lineJoin LineJoin - fillRule FillRule - fontFace font.Face - fontHeight float64 - matrix Matrix - stack []*Context + width int + height int + im *image.RGBA + mask *image.Alpha + color color.Color + fillPattern Pattern + strokePattern Pattern + strokePath raster.Path + fillPath raster.Path + start Point + current Point + hasCurrent bool + dashes []float64 + lineWidth float64 + lineCap LineCap + lineJoin LineJoin + fillRule FillRule + fontFace font.Face + fontHeight float64 + matrix Matrix + stack []*Context } // NewContext creates a new image.RGBA with the specified width and height @@ -172,9 +174,25 @@ func (dc *Context) SetFillRuleEvenOdd() { // Color Setters +func (dc *Context) setFillAndStrokeColor(c color.Color) { + dc.color = c + dc.fillPattern = NewSolidPattern(c) + dc.strokePattern = NewSolidPattern(c) +} + +// SetFillStyle sets current fill style +func (dc *Context) SetFillStyle(pattern Pattern) { + dc.fillPattern = pattern +} + +// SetStrokeStyle sets current stroke style +func (dc *Context) SetStrokeStyle(pattern Pattern) { + dc.strokePattern = pattern +} + // SetColor sets the current color. func (dc *Context) SetColor(c color.Color) { - dc.color = c + dc.setFillAndStrokeColor(c) } // SetHexColor sets the current color using a hex string. The leading pound @@ -189,6 +207,7 @@ func (dc *Context) SetHexColor(x string) { // 255, inclusive. func (dc *Context) SetRGBA255(r, g, b, a int) { dc.color = color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(a)} + dc.setFillAndStrokeColor(dc.color) } // SetRGB255 sets the current color. r, g, b values should be between 0 and 255, @@ -206,6 +225,7 @@ func (dc *Context) SetRGBA(r, g, b, a float64) { uint8(b * 255), uint8(a * 255), } + dc.setFillAndStrokeColor(dc.color) } // SetRGB sets the current color. r, g, b values should be between 0 and 1, @@ -372,13 +392,13 @@ func (dc *Context) fill(painter raster.Painter) { // operation. func (dc *Context) StrokePreserve() { if dc.mask == nil { - painter := raster.NewRGBAPainter(dc.im) - painter.SetColor(dc.color) + painter := newPatternPainter(dc.im) + painter.setPattern(dc.strokePattern) dc.stroke(painter) } else { im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height)) - painter := raster.NewRGBAPainter(im) - painter.SetColor(dc.color) + painter := newPatternPainter(im) + painter.setPattern(dc.strokePattern) dc.stroke(painter) draw.DrawMask(dc.im, dc.im.Bounds(), im, image.ZP, dc.mask, image.ZP, draw.Over) } @@ -396,13 +416,13 @@ func (dc *Context) Stroke() { // are implicity closed. The path is preserved after this operation. func (dc *Context) FillPreserve() { if dc.mask == nil { - painter := raster.NewRGBAPainter(dc.im) - painter.SetColor(dc.color) + painter := newPatternPainter(dc.im) + painter.setPattern(dc.fillPattern) dc.fill(painter) } else { im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height)) - painter := raster.NewRGBAPainter(im) - painter.SetColor(dc.color) + painter := newPatternPainter(im) + painter.setPattern(dc.fillPattern) dc.fill(painter) draw.DrawMask(dc.im, dc.im.Bounds(), im, image.ZP, dc.mask, image.ZP, draw.Over) } diff --git a/examples/fill-pattern.go b/examples/fill-pattern.go new file mode 100644 index 0000000..4500350 --- /dev/null +++ b/examples/fill-pattern.go @@ -0,0 +1,20 @@ +package main + +import "github.com/fogleman/gg" + +func main() { + im, err := gg.LoadPNG("examples/lenna.png") + if err != nil { + panic(err) + } + pattern := gg.NewSurfacePattern(im, gg.RepeatBoth) + dc := gg.NewContext(600, 600) + dc.MoveTo(20, 20) + dc.LineTo(590, 20) + dc.LineTo(590, 590) + dc.LineTo(20, 590) + dc.ClosePath() + dc.SetFillStyle(pattern) + dc.Fill() + dc.SavePNG("out.png") +} diff --git a/pattern.go b/pattern.go new file mode 100644 index 0000000..8b20764 --- /dev/null +++ b/pattern.go @@ -0,0 +1,121 @@ +package gg + +import ( + "image" + "image/color" + + "github.com/golang/freetype/raster" +) + +type RepeatOp int + +const ( + RepeatBoth RepeatOp = iota + RepeatX + RepeatY + RepeatNone +) + +type Pattern interface { + ColorAt(x, y int) color.Color +} + +// Solid Pattern +type solidPattern struct { + color color.Color +} + +func (p *solidPattern) ColorAt(x, y int) color.Color { + return p.color +} + +func NewSolidPattern(color color.Color) Pattern { + return &solidPattern{color: color} +} + +// Surface Pattern +type surfacePattern struct { + im image.Image + op RepeatOp +} + +func (p *surfacePattern) ColorAt(x, y int) color.Color { + b := p.im.Bounds() + switch p.op { + case RepeatX: + if y >= b.Dy() { + return color.Transparent + } + case RepeatY: + if x >= b.Dx() { + return color.Transparent + } + case RepeatNone: + if x >= b.Dx() || y >= b.Dy() { + return color.Transparent + } + } + x = x%b.Dx() + b.Min.X + y = y%b.Dy() + b.Min.Y + return p.im.At(x, y) +} + +func NewSurfacePattern(im image.Image, op RepeatOp) Pattern { + return &surfacePattern{im: im, op: op} +} + +type patternPainter struct { + im *image.RGBA + p Pattern +} + +// Paint satisfies the Painter interface. +func (r *patternPainter) Paint(ss []raster.Span, done bool) { + b := r.im.Bounds() + for _, s := range ss { + if s.Y < b.Min.Y { + continue + } + if s.Y >= b.Max.Y { + return + } + if s.X0 < b.Min.X { + s.X0 = b.Min.X + } + if s.X1 > b.Max.X { + s.X1 = b.Max.X + } + if s.X0 >= s.X1 { + continue + } + ma := s.Alpha + const m = 1<<16 - 1 + y := s.Y - r.im.Rect.Min.Y + x0 := s.X0 - r.im.Rect.Min.X + // x1 := x0 + s.X1 - s.X0 + // RGBAPainter.Paint() in $GOPATH/src/github.com/golang/freetype/raster/paint.go + i0 := (s.Y-r.im.Rect.Min.Y)*r.im.Stride + (s.X0-r.im.Rect.Min.X)*4 + i1 := i0 + (s.X1-s.X0)*4 + for i, x := i0, x0; i < i1; i, x = i+4, x+1 { + c := r.p.ColorAt(x, y) + cr, cg, cb, ca := c.RGBA() + dr := uint32(r.im.Pix[i+0]) + dg := uint32(r.im.Pix[i+1]) + db := uint32(r.im.Pix[i+2]) + da := uint32(r.im.Pix[i+3]) + a := (m - (ca * ma / m)) * 0x101 + r.im.Pix[i+0] = uint8((dr*a + cr*ma) / m >> 8) + r.im.Pix[i+1] = uint8((dg*a + cg*ma) / m >> 8) + r.im.Pix[i+2] = uint8((db*a + cb*ma) / m >> 8) + r.im.Pix[i+3] = uint8((da*a + ca*ma) / m >> 8) + } + } +} + +func (r *patternPainter) setPattern(pattern Pattern) { + r.p = pattern +} + +func newPatternPainter(im *image.RGBA) *patternPainter { + return &patternPainter{im: im} +} From 1aa57065bb81ca9a953286a84b5cfcff4294c3bf Mon Sep 17 00:00:00 2001 From: wsw Date: Mon, 21 Nov 2016 11:01:12 +0800 Subject: [PATCH 2/6] linear gradient --- examples/gradient-linear.go | 39 ++++++ examples/{fill-pattern.go => pattern-fill.go} | 17 ++- gradient.go | 131 ++++++++++++++++++ 3 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 examples/gradient-linear.go rename examples/{fill-pattern.go => pattern-fill.go} (51%) create mode 100644 gradient.go diff --git a/examples/gradient-linear.go b/examples/gradient-linear.go new file mode 100644 index 0000000..5f1ceec --- /dev/null +++ b/examples/gradient-linear.go @@ -0,0 +1,39 @@ +package main + +import ( + "image/color" + + "github.com/fogleman/gg" +) + +func main() { + dc := gg.NewContext(500, 400) + + grad := gg.NewLinearGradient(20, 320, 400, 20) + grad.AddColorStop(0, color.RGBA{0, 255, 0, 255}) + grad.AddColorStop(1, color.RGBA{0, 0, 255, 255}) + grad.AddColorStop(0.5, color.RGBA{255, 0, 0, 255}) + + dc.SetColor(color.White) + dc.DrawRectangle(20, 20, 400-20, 300) + dc.Stroke() + + dc.SetStrokeStyle(grad) + dc.SetLineWidth(4) + dc.MoveTo(10, 10) + dc.LineTo(410, 10) + dc.LineTo(410, 100) + dc.LineTo(10, 100) + dc.ClosePath() + dc.Stroke() + + dc.SetFillStyle(grad) + dc.MoveTo(10, 120) + dc.LineTo(410, 120) + dc.LineTo(410, 300) + dc.LineTo(10, 300) + dc.ClosePath() + dc.Fill() + + dc.SavePNG("out.png") +} diff --git a/examples/fill-pattern.go b/examples/pattern-fill.go similarity index 51% rename from examples/fill-pattern.go rename to examples/pattern-fill.go index 4500350..d46998b 100644 --- a/examples/fill-pattern.go +++ b/examples/pattern-fill.go @@ -1,12 +1,27 @@ package main -import "github.com/fogleman/gg" +import ( + "fmt" + "math" + + "github.com/fogleman/gg" +) func main() { im, err := gg.LoadPNG("examples/lenna.png") if err != nil { panic(err) } + var x0, y0, x1, y1 float64 = 5, 5, 15, 15 + a := math.Atan2(y1-y0, x1-x0) + d := a * 180 / math.Pi + fmt.Println(a, d) + a = (2*math.Pi - a) + d = a * 180 / math.Pi + fmt.Println(a, d) + m := gg.Translate(-x0, -y0).Rotate(a) + tx, ty := m.TransformPoint(5, 5) + fmt.Println(tx, ty) pattern := gg.NewSurfacePattern(im, gg.RepeatBoth) dc := gg.NewContext(600, 600) dc.MoveTo(20, 20) diff --git a/gradient.go b/gradient.go new file mode 100644 index 0000000..5c155ba --- /dev/null +++ b/gradient.go @@ -0,0 +1,131 @@ +package gg + +import ( + "image/color" + "math" + "sort" +) + +type stop struct { + pos float64 + color color.Color +} + +type stops []stop + +// Len satisfies the Sort interface. +func (s stops) Len() int { + return len(s) +} + +// Less satisfies the Sort interface. +func (s stops) Less(i, j int) bool { + return s[i].pos < s[j].pos +} + +// Swap satisfies the Sort interface. +func (s stops) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +type Gradient interface { + Pattern + AddColorStop(offset float64, color color.Color) +} + +// Linear Gradient +type linearGradient struct { + x0, y0, x1, y1 float64 + dx, dy float64 + mag float64 + px0, py0 float64 + stops stops +} + +func (g *linearGradient) ColorAt(x, y int) color.Color { + if len(g.stops) == 0 { + return color.Transparent + } + + fx, fy := float64(x), float64(y) + x0, y0 := g.x0, g.y0 + dx, dy := g.dx, g.dy + + // Horizontal + if dy == 0 && dx != 0 { + return getColor((fx-x0)/dx, g.stops) + } + + // Vertical + if dx == 0 && dy != 0 { + return getColor((fy-y0)/dy, g.stops) + } + + px0, py0 := g.px0, g.py0 + mag := g.mag + + s0 := (px0-x0)*(fy-y0) - (py0-y0)*(fx-x0) + if s0 > 0 { + return g.stops[0].color + } + u := ((fx-x0)*(px0-x0) + (fy-y0)*(py0-y0)) / (mag * mag) + x2, y2 := x0+u*(px0-x0), y0+u*(py0-y0) + d := math.Hypot(fx-x2, fy-y2) / mag + return getColor(d, g.stops) +} + +func (g *linearGradient) AddColorStop(offset float64, color color.Color) { + g.stops = append(g.stops, stop{pos: offset, color: color}) + sort.Sort(g.stops) +} + +func NewLinearGradient(x0, y0, x1, y1 float64) Gradient { + dx, dy := x1-x0, y1-y0 + mag := math.Hypot(dx, dy) + px0, py0 := x0-dy, y0+dx + g := &linearGradient{ + x0: x0, y0: y0, + x1: x1, y1: y1, + dx: dx, dy: dy, + px0: px0, py0: py0, + mag: mag, + } + return g +} + +func getColor(pos float64, stops stops) color.Color { + if pos <= 0.0 || len(stops) == 1 { + return stops[0].color + } + + last := stops[len(stops)-1] + + if pos >= last.pos { + return last.color + } + + for i, stop := range stops[1:] { + if pos < stop.pos { + pos = (pos - stops[i].pos) / (stop.pos - stops[i].pos) + return colorLerp(stops[i].color, stop.color, pos) + } + } + + return last.color +} + +func colorLerp(c0, c1 color.Color, t float64) color.Color { + r0, g0, b0, a0 := c0.RGBA() + r1, g1, b1, a1 := c1.RGBA() + + return color.NRGBA{ + lerp(r0, r1, t), + lerp(g0, g1, t), + lerp(b0, b1, t), + lerp(a0, a1, t), + } +} + +func lerp(a, b uint32, t float64) uint8 { + return uint8(int32(float64(a)*(1.0-t)+float64(b)*t) >> 8) +} From 298fbeaa77d7cfa78533af6ed0e474123f6d34a0 Mon Sep 17 00:00:00 2001 From: wsw Date: Mon, 21 Nov 2016 14:51:07 +0800 Subject: [PATCH 3/6] cleanup, add commets --- gradient.go | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/gradient.go b/gradient.go index 5c155ba..78b4e52 100644 --- a/gradient.go +++ b/gradient.go @@ -36,9 +36,6 @@ type Gradient interface { // Linear Gradient type linearGradient struct { x0, y0, x1, y1 float64 - dx, dy float64 - mag float64 - px0, py0 float64 stops stops } @@ -48,8 +45,8 @@ func (g *linearGradient) ColorAt(x, y int) color.Color { } fx, fy := float64(x), float64(y) - x0, y0 := g.x0, g.y0 - dx, dy := g.dx, g.dy + x0, y0, x1, y1 := g.x0, g.y0, g.x1, g.y1 + dx, dy := x1-x0, y1-y0 // Horizontal if dy == 0 && dx != 0 { @@ -61,15 +58,15 @@ func (g *linearGradient) ColorAt(x, y int) color.Color { return getColor((fy-y0)/dy, g.stops) } - px0, py0 := g.px0, g.py0 - mag := g.mag - - s0 := (px0-x0)*(fy-y0) - (py0-y0)*(fx-x0) - if s0 > 0 { + // Dot product + s0 := dx*(fx-x0) + dy*(fy-y0) + if s0 < 0 { return g.stops[0].color } - u := ((fx-x0)*(px0-x0) + (fy-y0)*(py0-y0)) / (mag * mag) - x2, y2 := x0+u*(px0-x0), y0+u*(py0-y0) + // Calculate distance to (x0,y0) alone (x0,y0)->(x1,y1) + mag := math.Hypot(dx, dy) + u := ((fx-x0)*-dy + (fy-y0)*dx) / (mag * mag) + x2, y2 := x0+u*-dy, y0+u*dx d := math.Hypot(fx-x2, fy-y2) / mag return getColor(d, g.stops) } @@ -80,15 +77,9 @@ func (g *linearGradient) AddColorStop(offset float64, color color.Color) { } func NewLinearGradient(x0, y0, x1, y1 float64) Gradient { - dx, dy := x1-x0, y1-y0 - mag := math.Hypot(dx, dy) - px0, py0 := x0-dy, y0+dx g := &linearGradient{ x0: x0, y0: y0, x1: x1, y1: y1, - dx: dx, dy: dy, - px0: px0, py0: py0, - mag: mag, } return g } From 8da875f641cf08c9e64019c413c3f456f51e8a33 Mon Sep 17 00:00:00 2001 From: wsw Date: Mon, 21 Nov 2016 14:57:15 +0800 Subject: [PATCH 4/6] cleanup again --- examples/pattern-fill.go | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/examples/pattern-fill.go b/examples/pattern-fill.go index d46998b..4500350 100644 --- a/examples/pattern-fill.go +++ b/examples/pattern-fill.go @@ -1,27 +1,12 @@ package main -import ( - "fmt" - "math" - - "github.com/fogleman/gg" -) +import "github.com/fogleman/gg" func main() { im, err := gg.LoadPNG("examples/lenna.png") if err != nil { panic(err) } - var x0, y0, x1, y1 float64 = 5, 5, 15, 15 - a := math.Atan2(y1-y0, x1-x0) - d := a * 180 / math.Pi - fmt.Println(a, d) - a = (2*math.Pi - a) - d = a * 180 / math.Pi - fmt.Println(a, d) - m := gg.Translate(-x0, -y0).Rotate(a) - tx, ty := m.TransformPoint(5, 5) - fmt.Println(tx, ty) pattern := gg.NewSurfacePattern(im, gg.RepeatBoth) dc := gg.NewContext(600, 600) dc.MoveTo(20, 20) From 214e3152fb8cf293b2be76766b5a424cbcdd160a Mon Sep 17 00:00:00 2001 From: wsw Date: Mon, 21 Nov 2016 21:13:54 +0800 Subject: [PATCH 5/6] radial gradient from package 'pixman' --- examples/gradient-radial.go | 27 +++++++++++++ gradient.go | 77 +++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 examples/gradient-radial.go diff --git a/examples/gradient-radial.go b/examples/gradient-radial.go new file mode 100644 index 0000000..d336135 --- /dev/null +++ b/examples/gradient-radial.go @@ -0,0 +1,27 @@ +package main + +import ( + "image/color" + + "github.com/fogleman/gg" +) + +func main() { + dc := gg.NewContext(400, 200) + + grad := gg.NewRadialGradient(100, 100, 10, 100, 120, 80) + grad.AddColorStop(0, color.RGBA{0, 255, 0, 255}) + grad.AddColorStop(1, color.RGBA{0, 0, 255, 255}) + + dc.SetFillStyle(grad) + dc.DrawRectangle(0, 0, 200, 200) + dc.Fill() + + dc.SetColor(color.White) + dc.DrawCircle(100, 100, 10) + dc.Stroke() + dc.DrawCircle(100, 120, 80) + dc.Stroke() + + dc.SavePNG("out.png") +} diff --git a/gradient.go b/gradient.go index 78b4e52..836dcda 100644 --- a/gradient.go +++ b/gradient.go @@ -84,6 +84,83 @@ func NewLinearGradient(x0, y0, x1, y1 float64) Gradient { return g } +// Radial Gradient +type circle struct { + x, y, r float64 +} + +type radialGradient struct { + c0, c1, cd circle + a, inva float64 + mindr float64 + stops stops +} + +func dot3(x0, y0, z0, x1, y1, z1 float64) float64 { + return x0*x1 + y0*y1 + z0*z1 +} + +func (g *radialGradient) ColorAt(x, y int) color.Color { + if len(g.stops) == 0 { + return color.Transparent + } + + // copy from pixman's pixman-radial-gradient.c + + dx, dy := float64(x)+0.5-g.c0.x, float64(y)+0.5-g.c0.y + b := dot3(dx, dy, g.c0.r, g.cd.x, g.cd.y, g.cd.r) + c := dot3(dx, dy, -g.c0.r, dx, dy, g.c0.r) + + if g.a == 0 { + if b == 0 { + return color.Transparent + } + t := 2 * c / b + if t*g.cd.r >= g.mindr { + return getColor(t, g.stops) + } + return color.Transparent + } + + discr := dot3(b, g.a, 0, b, -c, 0) + if discr >= 0 { + sqrtdiscr := math.Sqrt(discr) + t0 := (b + sqrtdiscr) * g.inva + t1 := (b - sqrtdiscr) * g.inva + + if t0*g.cd.r >= g.mindr { + return getColor(t0, g.stops) + } else if t1*g.cd.r >= g.mindr { + return getColor(t1, g.stops) + } + } + + return color.Transparent +} + +func (g *radialGradient) AddColorStop(offset float64, color color.Color) { + g.stops = append(g.stops, stop{pos: offset, color: color}) + sort.Sort(g.stops) +} + +func NewRadialGradient(x0, y0, r0, x1, y1, r1 float64) Gradient { + c0 := circle{x0, y0, r0} + c1 := circle{x1, y1, r1} + cd := circle{x1 - x0, y1 - y0, r1 - r0} + a := dot3(cd.x, cd.y, -cd.r, cd.x, cd.y, cd.r) + inva := 1 / a + mindr := -c0.r + g := &radialGradient{ + c0: c0, + c1: c1, + cd: cd, + a: a, + inva: inva, + mindr: mindr, + } + return g +} + func getColor(pos float64, stops stops) color.Color { if pos <= 0.0 || len(stops) == 1 { return stops[0].color From d60a19b37a387a7330b11b1ee17c7b6e742368f6 Mon Sep 17 00:00:00 2001 From: wsw Date: Tue, 22 Nov 2016 12:56:58 +0800 Subject: [PATCH 6/6] int/float64 things --- gradient.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gradient.go b/gradient.go index 836dcda..1625520 100644 --- a/gradient.go +++ b/gradient.go @@ -115,7 +115,7 @@ func (g *radialGradient) ColorAt(x, y int) color.Color { if b == 0 { return color.Transparent } - t := 2 * c / b + t := 0.5 * c / b if t*g.cd.r >= g.mindr { return getColor(t, g.stops) } @@ -148,7 +148,10 @@ func NewRadialGradient(x0, y0, r0, x1, y1, r1 float64) Gradient { c1 := circle{x1, y1, r1} cd := circle{x1 - x0, y1 - y0, r1 - r0} a := dot3(cd.x, cd.y, -cd.r, cd.x, cd.y, cd.r) - inva := 1 / a + var inva float64 + if a != 0 { + inva = 1.0 / a + } mindr := -c0.r g := &radialGradient{ c0: c0,