diff options
Diffstat (limited to 'vendor/github.com/fogleman/gg/context.go')
| -rw-r--r-- | vendor/github.com/fogleman/gg/context.go | 738 |
1 files changed, 738 insertions, 0 deletions
diff --git a/vendor/github.com/fogleman/gg/context.go b/vendor/github.com/fogleman/gg/context.go new file mode 100644 index 0000000..e186027 --- /dev/null +++ b/vendor/github.com/fogleman/gg/context.go @@ -0,0 +1,738 @@ +// Package gg provides a simple API for rendering 2D graphics in pure Go. +package gg + +import ( + "image" + "image/color" + "image/draw" + "image/png" + "io" + "math" + + "github.com/golang/freetype/raster" + "golang.org/x/image/font" + "golang.org/x/image/font/basicfont" +) + +type LineCap int + +const ( + LineCapRound LineCap = iota + LineCapButt + LineCapSquare +) + +type LineJoin int + +const ( + LineJoinRound LineJoin = iota + LineJoinBevel +) + +type FillRule int + +const ( + FillRuleWinding FillRule = iota + FillRuleEvenOdd +) + +type Align int + +const ( + AlignLeft Align = iota + AlignCenter + AlignRight +) + +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 +} + +// NewContext creates a new image.RGBA with the specified width and height +// and prepares a context for rendering onto that image. +func NewContext(width, height int) *Context { + return NewContextForRGBA(image.NewRGBA(image.Rect(0, 0, width, height))) +} + +// NewContextForImage copies the specified image into a new image.RGBA +// and prepares a context for rendering onto that image. +func NewContextForImage(im image.Image) *Context { + return NewContextForRGBA(imageToRGBA(im)) +} + +// NewContextForRGBA prepares a context for rendering onto the specified image. +// No copy is made. +func NewContextForRGBA(im *image.RGBA) *Context { + return &Context{ + width: im.Bounds().Size().X, + height: im.Bounds().Size().Y, + im: im, + color: color.Transparent, + lineWidth: 1, + fillRule: FillRuleWinding, + fontFace: basicfont.Face7x13, + fontHeight: 13, + matrix: Identity(), + } +} + +// Image returns the image that has been drawn by this context. +func (dc *Context) Image() image.Image { + return dc.im +} + +// Width returns the width of the image in pixels. +func (dc *Context) Width() int { + return dc.width +} + +// Height returns the height of the image in pixels. +func (dc *Context) Height() int { + return dc.height +} + +// SavePNG encodes the image as a PNG and writes it to disk. +func (dc *Context) SavePNG(path string) error { + return SavePNG(path, dc.im) +} + +// EncodePNG encodes the image as a PNG and writes it to the provided io.Writer. +func (dc *Context) EncodePNG(w io.Writer) error { + return png.Encode(w, dc.im) +} + +// SetDash sets the current dash pattern to use. Call with zero arguments to +// disable dashes. The values specify the lengths of each dash, with +// alternating on and off lengths. +func (dc *Context) SetDash(dashes ...float64) { + dc.dashes = dashes +} + +func (dc *Context) SetLineWidth(lineWidth float64) { + dc.lineWidth = lineWidth +} + +func (dc *Context) SetLineCap(lineCap LineCap) { + dc.lineCap = lineCap +} + +func (dc *Context) SetLineCapRound() { + dc.lineCap = LineCapRound +} + +func (dc *Context) SetLineCapButt() { + dc.lineCap = LineCapButt +} + +func (dc *Context) SetLineCapSquare() { + dc.lineCap = LineCapSquare +} + +func (dc *Context) SetLineJoin(lineJoin LineJoin) { + dc.lineJoin = lineJoin +} + +func (dc *Context) SetLineJoinRound() { + dc.lineJoin = LineJoinRound +} + +func (dc *Context) SetLineJoinBevel() { + dc.lineJoin = LineJoinBevel +} + +func (dc *Context) SetFillRule(fillRule FillRule) { + dc.fillRule = fillRule +} + +func (dc *Context) SetFillRuleWinding() { + dc.fillRule = FillRuleWinding +} + +func (dc *Context) SetFillRuleEvenOdd() { + dc.fillRule = FillRuleEvenOdd +} + +// Color Setters + +// SetColor sets the current color. +func (dc *Context) SetColor(c color.Color) { + dc.color = c +} + +// SetHexColor sets the current color using a hex string. The leading pound +// sign (#) is optional. Both 3- and 6-digit variations are supported. 8 digits +// may be provided to set the alpha value as well. +func (dc *Context) SetHexColor(x string) { + r, g, b, a := parseHexColor(x) + dc.SetRGBA255(r, g, b, a) +} + +// SetRGBA255 sets the current color. r, g, b, a values should be between 0 and +// 255, inclusive. +func (dc *Context) SetRGBA255(r, g, b, a int) { + dc.color = color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(a)} +} + +// SetRGB255 sets the current color. r, g, b values should be between 0 and 255, +// inclusive. Alpha will be set to 255 (fully opaque). +func (dc *Context) SetRGB255(r, g, b int) { + dc.SetRGBA255(r, g, b, 255) +} + +// SetRGBA sets the current color. r, g, b, a values should be between 0 and 1, +// inclusive. +func (dc *Context) SetRGBA(r, g, b, a float64) { + dc.color = color.NRGBA{ + uint8(r * 255), + uint8(g * 255), + uint8(b * 255), + uint8(a * 255), + } +} + +// SetRGB sets the current color. r, g, b values should be between 0 and 1, +// inclusive. Alpha will be set to 1 (fully opaque). +func (dc *Context) SetRGB(r, g, b float64) { + dc.SetRGBA(r, g, b, 1) +} + +// Path Manipulation + +// MoveTo starts a new subpath within the current path starting at the +// specified point. +func (dc *Context) MoveTo(x, y float64) { + if dc.hasCurrent { + dc.fillPath.Add1(dc.start.Fixed()) + } + x, y = dc.TransformPoint(x, y) + p := Point{x, y} + dc.strokePath.Start(p.Fixed()) + dc.fillPath.Start(p.Fixed()) + dc.start = p + dc.current = p + dc.hasCurrent = true +} + +// LineTo adds a line segment to the current path starting at the current +// point. If there is no current point, it is equivalent to MoveTo(x, y) +func (dc *Context) LineTo(x, y float64) { + if !dc.hasCurrent { + dc.MoveTo(x, y) + } else { + x, y = dc.TransformPoint(x, y) + p := Point{x, y} + dc.strokePath.Add1(p.Fixed()) + dc.fillPath.Add1(p.Fixed()) + dc.current = p + } +} + +// QuadraticTo adds a quadratic bezier curve to the current path starting at +// the current point. If there is no current point, it first performs +// MoveTo(x1, y1) +func (dc *Context) QuadraticTo(x1, y1, x2, y2 float64) { + if !dc.hasCurrent { + dc.MoveTo(x1, y1) + } + x1, y1 = dc.TransformPoint(x1, y1) + x2, y2 = dc.TransformPoint(x2, y2) + p1 := Point{x1, y1} + p2 := Point{x2, y2} + dc.strokePath.Add2(p1.Fixed(), p2.Fixed()) + dc.fillPath.Add2(p1.Fixed(), p2.Fixed()) + dc.current = p2 +} + +// CubicTo adds a cubic bezier curve to the current path starting at the +// current point. If there is no current point, it first performs +// MoveTo(x1, y1). Because freetype/raster does not support cubic beziers, +// this is emulated with many small line segments. +func (dc *Context) CubicTo(x1, y1, x2, y2, x3, y3 float64) { + if !dc.hasCurrent { + dc.MoveTo(x1, y1) + } + x0, y0 := dc.current.X, dc.current.Y + x1, y1 = dc.TransformPoint(x1, y1) + x2, y2 = dc.TransformPoint(x2, y2) + x3, y3 = dc.TransformPoint(x3, y3) + points := CubicBezier(x0, y0, x1, y1, x2, y2, x3, y3) + previous := dc.current.Fixed() + for _, p := range points[1:] { + f := p.Fixed() + if f == previous { + // TODO: this fixes some rendering issues but not all + continue + } + previous = f + dc.strokePath.Add1(f) + dc.fillPath.Add1(f) + dc.current = p + } +} + +// ClosePath adds a line segment from the current point to the beginning +// of the current subpath. If there is no current point, this is a no-op. +func (dc *Context) ClosePath() { + if dc.hasCurrent { + dc.strokePath.Add1(dc.start.Fixed()) + dc.fillPath.Add1(dc.start.Fixed()) + dc.current = dc.start + } +} + +// ClearPath clears the current path. There is no current point after this +// operation. +func (dc *Context) ClearPath() { + dc.strokePath.Clear() + dc.fillPath.Clear() + dc.hasCurrent = false +} + +// NewSubPath starts a new subpath within the current path. There is no current +// point after this operation. +func (dc *Context) NewSubPath() { + if dc.hasCurrent { + dc.fillPath.Add1(dc.start.Fixed()) + } + dc.hasCurrent = false +} + +// Path Drawing + +func (dc *Context) capper() raster.Capper { + switch dc.lineCap { + case LineCapButt: + return raster.ButtCapper + case LineCapRound: + return raster.RoundCapper + case LineCapSquare: + return raster.SquareCapper + } + return nil +} + +func (dc *Context) joiner() raster.Joiner { + switch dc.lineJoin { + case LineJoinBevel: + return raster.BevelJoiner + case LineJoinRound: + return raster.RoundJoiner + } + return nil +} + +func (dc *Context) stroke(painter raster.Painter) { + path := dc.strokePath + if len(dc.dashes) > 0 { + path = dashed(path, dc.dashes) + } else { + // TODO: this is a temporary workaround to remove tiny segments + // that result in rendering issues + path = rasterPath(flattenPath(path)) + } + r := raster.NewRasterizer(dc.width, dc.height) + r.UseNonZeroWinding = true + r.AddStroke(path, fix(dc.lineWidth), dc.capper(), dc.joiner()) + r.Rasterize(painter) +} + +func (dc *Context) fill(painter raster.Painter) { + path := dc.fillPath + if dc.hasCurrent { + path = make(raster.Path, len(dc.fillPath)) + copy(path, dc.fillPath) + path.Add1(dc.start.Fixed()) + } + r := raster.NewRasterizer(dc.width, dc.height) + r.UseNonZeroWinding = dc.fillRule == FillRuleWinding + r.AddPath(path) + r.Rasterize(painter) +} + +// StrokePreserve strokes the current path with the current color, line width, +// line cap, line join and dash settings. The path is preserved after this +// operation. +func (dc *Context) StrokePreserve() { + if dc.mask == nil { + painter := raster.NewRGBAPainter(dc.im) + painter.SetColor(dc.color) + dc.stroke(painter) + } else { + im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height)) + painter := raster.NewRGBAPainter(im) + painter.SetColor(dc.color) + dc.stroke(painter) + draw.DrawMask(dc.im, dc.im.Bounds(), im, image.ZP, dc.mask, image.ZP, draw.Over) + } +} + +// Stroke strokes the current path with the current color, line width, +// line cap, line join and dash settings. The path is cleared after this +// operation. +func (dc *Context) Stroke() { + dc.StrokePreserve() + dc.ClearPath() +} + +// FillPreserve fills the current path with the current color. Open subpaths +// 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) + dc.fill(painter) + } else { + im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height)) + painter := raster.NewRGBAPainter(im) + painter.SetColor(dc.color) + dc.fill(painter) + draw.DrawMask(dc.im, dc.im.Bounds(), im, image.ZP, dc.mask, image.ZP, draw.Over) + } +} + +// Fill fills the current path with the current color. Open subpaths +// are implicity closed. The path is cleared after this operation. +func (dc *Context) Fill() { + dc.FillPreserve() + dc.ClearPath() +} + +// ClipPreserve updates the clipping region by intersecting the current +// clipping region with the current path as it would be filled by dc.Fill(). +// The path is preserved after this operation. +func (dc *Context) ClipPreserve() { + clip := image.NewAlpha(image.Rect(0, 0, dc.width, dc.height)) + painter := raster.NewAlphaOverPainter(clip) + dc.fill(painter) + if dc.mask == nil { + dc.mask = clip + } else { + mask := image.NewAlpha(image.Rect(0, 0, dc.width, dc.height)) + draw.DrawMask(mask, mask.Bounds(), clip, image.ZP, dc.mask, image.ZP, draw.Over) + dc.mask = mask + } +} + +// Clip updates the clipping region by intersecting the current +// clipping region with the current path as it would be filled by dc.Fill(). +// The path is cleared after this operation. +func (dc *Context) Clip() { + dc.ClipPreserve() + dc.ClearPath() +} + +// ResetClip clears the clipping region. +func (dc *Context) ResetClip() { + dc.mask = nil +} + +// Convenient Drawing Functions + +// Clear fills the entire image with the current color. +func (dc *Context) Clear() { + src := image.NewUniform(dc.color) + draw.Draw(dc.im, dc.im.Bounds(), src, image.ZP, draw.Src) +} + +func (dc *Context) DrawLine(x1, y1, x2, y2 float64) { + dc.MoveTo(x1, y1) + dc.LineTo(x2, y2) +} + +func (dc *Context) DrawRectangle(x, y, w, h float64) { + dc.NewSubPath() + dc.MoveTo(x, y) + dc.LineTo(x+w, y) + dc.LineTo(x+w, y+h) + dc.LineTo(x, y+h) + dc.ClosePath() +} + +func (dc *Context) DrawRoundedRectangle(x, y, w, h, r float64) { + x0, x1, x2, x3 := x, x+r, x+w-r, x+w + y0, y1, y2, y3 := y, y+r, y+h-r, y+h + dc.NewSubPath() + dc.MoveTo(x1, y0) + dc.LineTo(x2, y0) + dc.DrawArc(x2, y1, r, Radians(270), Radians(360)) + dc.LineTo(x3, y2) + dc.DrawArc(x2, y2, r, Radians(0), Radians(90)) + dc.LineTo(x1, y3) + dc.DrawArc(x1, y2, r, Radians(90), Radians(180)) + dc.LineTo(x0, y1) + dc.DrawArc(x1, y1, r, Radians(180), Radians(270)) + dc.ClosePath() +} + +func (dc *Context) DrawEllipticalArc(x, y, rx, ry, angle1, angle2 float64) { + const n = 16 + for i := 0; i < n; i++ { + p1 := float64(i+0) / n + p2 := float64(i+1) / n + a1 := angle1 + (angle2-angle1)*p1 + a2 := angle1 + (angle2-angle1)*p2 + x0 := x + rx*math.Cos(a1) + y0 := y + ry*math.Sin(a1) + x1 := x + rx*math.Cos(a1+(a2-a1)/2) + y1 := y + ry*math.Sin(a1+(a2-a1)/2) + x2 := x + rx*math.Cos(a2) + y2 := y + ry*math.Sin(a2) + cx := 2*x1 - x0/2 - x2/2 + cy := 2*y1 - y0/2 - y2/2 + if i == 0 && !dc.hasCurrent { + dc.MoveTo(x0, y0) + } + dc.QuadraticTo(cx, cy, x2, y2) + } +} + +func (dc *Context) DrawEllipse(x, y, rx, ry float64) { + dc.NewSubPath() + dc.DrawEllipticalArc(x, y, rx, ry, 0, 2*math.Pi) + dc.ClosePath() +} + +func (dc *Context) DrawArc(x, y, r, angle1, angle2 float64) { + dc.DrawEllipticalArc(x, y, r, r, angle1, angle2) +} + +func (dc *Context) DrawCircle(x, y, r float64) { + dc.NewSubPath() + dc.DrawEllipticalArc(x, y, r, r, 0, 2*math.Pi) + dc.ClosePath() +} + +func (dc *Context) DrawRegularPolygon(n int, x, y, r, rotation float64) { + angle := 2 * math.Pi / float64(n) + rotation -= math.Pi / 2 + if n%2 == 0 { + rotation += angle / 2 + } + dc.NewSubPath() + for i := 0; i < n; i++ { + a := rotation + angle*float64(i) + dc.LineTo(x+r*math.Cos(a), y+r*math.Sin(a)) + } + dc.ClosePath() +} + +// DrawImage draws the specified image at the specified point. +// Currently, rotation and scaling transforms are not supported. +func (dc *Context) DrawImage(im image.Image, x, y int) { + dc.DrawImageAnchored(im, x, y, 0, 0) +} + +// DrawImageAnchored draws the specified image at the specified anchor point. +// The anchor point is x - w * ax, y - h * ay, where w, h is the size of the +// image. Use ax=0.5, ay=0.5 to center the image at the specified point. +func (dc *Context) DrawImageAnchored(im image.Image, x, y int, ax, ay float64) { + s := im.Bounds().Size() + x -= int(ax * float64(s.X)) + y -= int(ay * float64(s.Y)) + p := image.Pt(x, y) + r := image.Rectangle{p, p.Add(s)} + if dc.mask == nil { + draw.Draw(dc.im, r, im, image.ZP, draw.Over) + } else { + draw.DrawMask(dc.im, r, im, image.ZP, dc.mask, p, draw.Over) + } +} + +// Text Functions + +func (dc *Context) SetFontFace(fontFace font.Face) { + dc.fontFace = fontFace +} + +func (dc *Context) LoadFontFace(path string, points float64) error { + face, err := loadFontFace(path, points) + if err == nil { + dc.fontFace = face + dc.fontHeight = points * 72 / 96 + } + return err +} + +func (dc *Context) drawString(im *image.RGBA, s string, x, y float64) { + d := &font.Drawer{ + Dst: im, + Src: image.NewUniform(dc.color), + Face: dc.fontFace, + Dot: fixp(x, y), + } + d.DrawString(s) +} + +// DrawString draws the specified text at the specified point. +// Currently, rotation and scaling transforms are not supported. +func (dc *Context) DrawString(s string, x, y float64) { + dc.DrawStringAnchored(s, x, y, 0, 0) +} + +// DrawStringAnchored draws the specified text at the specified anchor point. +// The anchor point is x - w * ax, y - h * ay, where w, h is the size of the +// text. Use ax=0.5, ay=0.5 to center the text at the specified point. +func (dc *Context) DrawStringAnchored(s string, x, y, ax, ay float64) { + w, h := dc.MeasureString(s) + x, y = dc.TransformPoint(x, y) + x -= ax * w + y += ay * h + if dc.mask == nil { + dc.drawString(dc.im, s, x, y) + } else { + im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height)) + dc.drawString(im, s, x, y) + draw.DrawMask(dc.im, dc.im.Bounds(), im, image.ZP, dc.mask, image.ZP, draw.Over) + } +} + +// DrawStringWrapped word-wraps the specified string to the given max width +// and then draws it at the specified anchor point using the given line +// spacing and text alignment. +func (dc *Context) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align) { + lines := dc.WordWrap(s, width) + h := float64(len(lines)) * dc.fontHeight * lineSpacing + h -= (lineSpacing - 1) * dc.fontHeight + x -= ax * width + y -= ay * h + switch align { + case AlignLeft: + ax = 0 + case AlignCenter: + ax = 0.5 + x += width / 2 + case AlignRight: + ax = 1 + x += width + } + ay = 1 + for _, line := range lines { + dc.DrawStringAnchored(line, x, y, ax, ay) + y += dc.fontHeight * lineSpacing + } +} + +// MeasureString returns the rendered width and height of the specified text +// given the current font face. +func (dc *Context) MeasureString(s string) (w, h float64) { + d := &font.Drawer{ + Face: dc.fontFace, + } + a := d.MeasureString(s) + return float64(a >> 6), dc.fontHeight +} + +// WordWrap wraps the specified string to the given max width and current +// font face. +func (dc *Context) WordWrap(s string, w float64) []string { + return wordWrap(dc, s, w) +} + +// Transformation Matrix Operations + +// Identity resets the current transformation matrix to the identity matrix. +// This results in no translating, scaling, rotating, or shearing. +func (dc *Context) Identity() { + dc.matrix = Identity() +} + +// Translate updates the current matrix with a translation. +func (dc *Context) Translate(x, y float64) { + dc.matrix = dc.matrix.Translate(x, y) +} + +// Scale updates the current matrix with a scaling factor. +// Scaling occurs about the origin. +func (dc *Context) Scale(x, y float64) { + dc.matrix = dc.matrix.Scale(x, y) +} + +// ScaleAbout updates the current matrix with a scaling factor. +// Scaling occurs about the specified point. +func (dc *Context) ScaleAbout(sx, sy, x, y float64) { + dc.Translate(x, y) + dc.Scale(sx, sy) + dc.Translate(-x, -y) +} + +// Rotate updates the current matrix with a clockwise rotation. +// Rotation occurs about the origin. Angle is specified in radians. +func (dc *Context) Rotate(angle float64) { + dc.matrix = dc.matrix.Rotate(angle) +} + +// RotateAbout updates the current matrix with a clockwise rotation. +// Rotation occurs about the specified point. Angle is specified in radians. +func (dc *Context) RotateAbout(angle, x, y float64) { + dc.Translate(x, y) + dc.Rotate(angle) + dc.Translate(-x, -y) +} + +// Shear updates the current matrix with a shearing angle. +// Shearing occurs about the origin. +func (dc *Context) Shear(x, y float64) { + dc.matrix = dc.matrix.Shear(x, y) +} + +// ShearAbout updates the current matrix with a shearing angle. +// Shearing occurs about the specified point. +func (dc *Context) ShearAbout(sx, sy, x, y float64) { + dc.Translate(x, y) + dc.Shear(sx, sy) + dc.Translate(-x, -y) +} + +// TransformPoint multiplies the specified point by the current matrix, +// returning a transformed position. +func (dc *Context) TransformPoint(x, y float64) (tx, ty float64) { + return dc.matrix.TransformPoint(x, y) +} + +// InvertY flips the Y axis so that Y grows from bottom to top and Y=0 is at +// the bottom of the image. +func (dc *Context) InvertY() { + dc.Translate(0, float64(dc.height)) + dc.Scale(1, -1) +} + +// Stack + +// Push saves the current state of the context for later retrieval. These +// can be nested. +func (dc *Context) Push() { + x := *dc + dc.stack = append(dc.stack, &x) +} + +// Pop restores the last saved context state from the stack. +func (dc *Context) Pop() { + before := *dc + s := dc.stack + x, s := s[len(s)-1], s[:len(s)-1] + *dc = *x + dc.mask = before.mask + dc.strokePath = before.strokePath + dc.fillPath = before.fillPath + dc.start = before.start + dc.current = before.current + dc.hasCurrent = before.hasCurrent +} |
