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/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/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/examples/pattern-fill.go b/examples/pattern-fill.go new file mode 100644 index 0000000..4500350 --- /dev/null +++ b/examples/pattern-fill.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/gradient.go b/gradient.go new file mode 100644 index 0000000..1625520 --- /dev/null +++ b/gradient.go @@ -0,0 +1,202 @@ +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 + 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, x1, y1 := g.x0, g.y0, g.x1, g.y1 + dx, dy := x1-x0, y1-y0 + + // 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) + } + + // Dot product + s0 := dx*(fx-x0) + dy*(fy-y0) + if s0 < 0 { + return g.stops[0].color + } + // 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) +} + +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 { + g := &linearGradient{ + x0: x0, y0: y0, + x1: x1, y1: y1, + } + 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 := 0.5 * 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) + var inva float64 + if a != 0 { + inva = 1.0 / 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 + } + + 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) +} 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} +}