Commits
Diff
diff --git a/gg/context.go b/gg/context.go
deleted file mode 100644
index 1ddb09f..0000000
--- a/gg/context.go
+++ /dev/null
@@ -1,922 +0,0 @@
-// Package gg provides a simple API for rendering 2D graphics in pure Go.
-package gg
-
-import (
- "errors"
- "image"
- "image/color"
- "image/jpeg"
- "image/png"
- "io"
- "math"
- "strings"
-
- "github.com/golang/freetype/raster"
- "golang.org/x/image/draw"
- "golang.org/x/image/font"
- "golang.org/x/image/font/basicfont"
- "golang.org/x/image/math/f64"
-)
-
-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
-)
-
-var (
- defaultFillStyle = NewSolidPattern(color.White)
- defaultStrokeStyle = NewSolidPattern(color.Black)
-)
-
-type Context struct {
- width int
- height int
- rasterizer *raster.Rasterizer
- 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
- dashOffset 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 {
- w := im.Bounds().Size().X
- h := im.Bounds().Size().Y
- return &Context{
- width: w,
- height: h,
- rasterizer: raster.NewRasterizer(w, h),
- im: im,
- color: color.Transparent,
- fillPattern: defaultFillStyle,
- strokePattern: defaultStrokeStyle,
- lineWidth: 1,
- fillRule: FillRuleWinding,
- fontFace: basicfont.Face7x13,
- fontHeight: 13,
- matrix: Identity(),
- }
-}
-
-// GetCurrentPoint will return the current point and if there is a current point.
-// The point will have been transformed by the context's transformation matrix.
-func (dc *Context) GetCurrentPoint() (Point, bool) {
- if dc.hasCurrent {
- return dc.current, true
- }
- return Point{}, false
-}
-
-// 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)
-}
-
-// SaveJPG encodes the image as a JPG and writes it to disk.
-func (dc *Context) SaveJPG(path string, quality int) error {
- return SaveJPG(path, dc.im, quality)
-}
-
-// 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)
-}
-
-// EncodeJPG encodes the image as a JPG and writes it to the provided io.Writer
-// in JPEG 4:2:0 baseline format with the given options.
-// Default parameters are used if a nil *jpeg.Options is passed.
-func (dc *Context) EncodeJPG(w io.Writer, o *jpeg.Options) error {
- return jpeg.Encode(w, dc.im, o)
-}
-
-// 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
-}
-
-// SetDashOffset sets the initial offset into the dash pattern to use when
-// stroking dashed paths.
-func (dc *Context) SetDashOffset(offset float64) {
- dc.dashOffset = offset
-}
-
-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
-
-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) {
- // if pattern is SolidPattern, also change dc.color(for dc.Clear, dc.drawString)
- if fillStyle, ok := pattern.(*solidPattern); ok {
- dc.color = fillStyle.color
- }
- dc.fillPattern = pattern
-}
-
-// SetStrokeStyle sets current stroke style
-func (dc *Context) SetStrokeStyle(pattern Pattern) {
- dc.strokePattern = pattern
-}
-
-// SetColor sets the current color(for both fill and stroke).
-func (dc *Context) SetColor(c color.Color) {
- dc.setFillAndStrokeColor(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)}
- dc.setFillAndStrokeColor(dc.color)
-}
-
-// 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),
- }
- dc.setFillAndStrokeColor(dc.color)
-}
-
-// 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, dc.dashOffset)
- } else {
- // TODO: this is a temporary workaround to remove tiny segments
- // that result in rendering issues
- path = rasterPath(flattenPath(path))
- }
- r := dc.rasterizer
- r.UseNonZeroWinding = true
- r.Clear()
- 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 := dc.rasterizer
- r.UseNonZeroWinding = dc.fillRule == FillRuleWinding
- r.Clear()
- 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() {
- var painter raster.Painter
- if dc.mask == nil {
- if pattern, ok := dc.strokePattern.(*solidPattern); ok {
- // with a nil mask and a solid color pattern, we can be more efficient
- // TODO: refactor so we don't have to do this type assertion stuff?
- p := raster.NewRGBAPainter(dc.im)
- p.SetColor(pattern.color)
- painter = p
- }
- }
- if painter == nil {
- painter = newPatternPainter(dc.im, dc.mask, dc.strokePattern)
- }
- dc.stroke(painter)
-}
-
-// 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() {
- var painter raster.Painter
- if dc.mask == nil {
- if pattern, ok := dc.fillPattern.(*solidPattern); ok {
- // with a nil mask and a solid color pattern, we can be more efficient
- // TODO: refactor so we don't have to do this type assertion stuff?
- p := raster.NewRGBAPainter(dc.im)
- p.SetColor(pattern.color)
- painter = p
- }
- }
- if painter == nil {
- painter = newPatternPainter(dc.im, dc.mask, dc.fillPattern)
- }
- dc.fill(painter)
-}
-
-// 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
- }
-}
-
-// SetMask allows you to directly set the *image.Alpha to be used as a clipping
-// mask. It must be the same size as the context, else an error is returned
-// and the mask is unchanged.
-func (dc *Context) SetMask(mask *image.Alpha) error {
- if mask.Bounds().Size() != dc.im.Bounds().Size() {
- return errors.New("mask size must match context size")
- }
- dc.mask = mask
- return nil
-}
-
-// AsMask returns an *image.Alpha representing the alpha channel of this
-// context. This can be useful for advanced clipping operations where you first
-// render the mask geometry and then use it as a mask.
-func (dc *Context) AsMask() *image.Alpha {
- mask := image.NewAlpha(dc.im.Bounds())
- draw.Draw(mask, dc.im.Bounds(), dc.im, image.ZP, draw.Src)
- return mask
-}
-
-// InvertMask inverts the alpha values in the current clipping mask such that
-// a fully transparent region becomes fully opaque and vice versa.
-func (dc *Context) InvertMask() {
- if dc.mask == nil {
- dc.mask = image.NewAlpha(dc.im.Bounds())
- } else {
- for i, a := range dc.mask.Pix {
- dc.mask.Pix[i] = 255 - a
- }
- }
-}
-
-// 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)
-}
-
-// SetPixel sets the color of the specified pixel using the current color.
-func (dc *Context) SetPixel(x, y int) {
- dc.im.Set(x, y, dc.color)
-}
-
-// DrawPoint is like DrawCircle but ensures that a circle of the specified
-// size is drawn regardless of the current transformation matrix. The position
-// is still transformed, but not the shape of the point.
-func (dc *Context) DrawPoint(x, y, r float64) {
- dc.Push()
- tx, ty := dc.TransformPoint(x, y)
- dc.Identity()
- dc.DrawCircle(tx, ty, r)
- dc.Pop()
-}
-
-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)/2)
- y1 := y + ry*math.Sin((a1+a2)/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 {
- if dc.hasCurrent {
- dc.LineTo(x0, y0)
- } else {
- 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.
-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))
- transformer := draw.BiLinear
- fx, fy := float64(x), float64(y)
- m := dc.matrix.Translate(fx, fy)
- s2d := f64.Aff3{m.XX, m.XY, m.X0, m.YX, m.YY, m.Y0}
- if dc.mask == nil {
- transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, nil)
- } else {
- transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, &draw.Options{
- DstMask: dc.mask,
- DstMaskP: image.ZP,
- })
- }
-}
-
-// Text Functions
-
-func (dc *Context) SetFontFace(fontFace font.Face) {
- dc.fontFace = fontFace
- dc.fontHeight = float64(fontFace.Metrics().Height) / 64
-}
-
-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) FontHeight() float64 {
- return dc.fontHeight
-}
-
-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),
- }
- // based on Drawer.DrawString() in golang.org/x/image/font/font.go
- prevC := rune(-1)
- for _, c := range s {
- if prevC >= 0 {
- d.Dot.X += d.Face.Kern(prevC, c)
- }
- dr, mask, maskp, advance, ok := d.Face.Glyph(d.Dot, c)
- if !ok {
- // TODO: is falling back on the U+FFFD glyph the responsibility of
- // the Drawer or the Face?
- // TODO: set prevC = '\ufffd'?
- continue
- }
- sr := dr.Sub(dr.Min)
- transformer := draw.BiLinear
- fx, fy := float64(dr.Min.X), float64(dr.Min.Y)
- m := dc.matrix.Translate(fx, fy)
- s2d := f64.Aff3{m.XX, m.XY, m.X0, m.YX, m.YY, m.Y0}
- transformer.Transform(d.Dst, s2d, d.Src, sr, draw.Over, &draw.Options{
- SrcMask: mask,
- SrcMaskP: maskp,
- })
- d.Dot.X += advance
- prevC = c
- }
-}
-
-// DrawString draws the specified text at the specified point.
-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 -= 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)
-
- // sync h formula with MeasureMultilineString
- 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
- }
-}
-
-func (dc *Context) MeasureMultilineString(s string, lineSpacing float64) (width, height float64) {
- lines := strings.Split(s, "\n")
-
- // sync h formula with DrawStringWrapped
- height = float64(len(lines)) * dc.fontHeight * lineSpacing
- height -= (lineSpacing - 1) * dc.fontHeight
-
- d := &font.Drawer{
- Face: dc.fontFace,
- }
-
- // max width from lines
- for _, line := range lines {
- adv := d.MeasureString(line)
- currentWidth := float64(adv >> 6) // from gg.Context.MeasureString
- if currentWidth > width {
- width = currentWidth
- }
- }
-
- return width, height
-}
-
-// 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 anticlockwise 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 anticlockwise 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
-}
// Package gg provides a simple API for rendering 2D graphics in pure Go.
package gg
import (
"errors"
"image"
"image/color"
"image/jpeg"
"image/png"
"io"
"math"
"strings"
"github.com/golang/freetype/raster"
"golang.org/x/image/draw"
"golang.org/x/image/font"
"golang.org/x/image/font/basicfont"
"golang.org/x/image/math/f64"
)
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
)
var (
defaultFillStyle = NewSolidPattern(color.White)
defaultStrokeStyle = NewSolidPattern(color.Black)
)
type Context struct {
width int
height int
rasterizer *raster.Rasterizer
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
dashOffset 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 {
w := im.Bounds().Size().X
h := im.Bounds().Size().Y
return &Context{
width: w,
height: h,
rasterizer: raster.NewRasterizer(w, h),
im: im,
color: color.Transparent,
fillPattern: defaultFillStyle,
strokePattern: defaultStrokeStyle,
lineWidth: 1,
fillRule: FillRuleWinding,
fontFace: basicfont.Face7x13,
fontHeight: 13,
matrix: Identity(),
}
}
// GetCurrentPoint will return the current point and if there is a current point.
// The point will have been transformed by the context's transformation matrix.
func (dc *Context) GetCurrentPoint() (Point, bool) {
if dc.hasCurrent {
return dc.current, true
}
return Point{}, false
}
// 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)
}
// SaveJPG encodes the image as a JPG and writes it to disk.
func (dc *Context) SaveJPG(path string, quality int) error {
return SaveJPG(path, dc.im, quality)
}
// 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)
}
// EncodeJPG encodes the image as a JPG and writes it to the provided io.Writer
// in JPEG 4:2:0 baseline format with the given options.
// Default parameters are used if a nil *jpeg.Options is passed.
func (dc *Context) EncodeJPG(w io.Writer, o *jpeg.Options) error {
return jpeg.Encode(w, dc.im, o)
}
// 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
}
// SetDashOffset sets the initial offset into the dash pattern to use when
// stroking dashed paths.
func (dc *Context) SetDashOffset(offset float64) {
dc.dashOffset = offset
}
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
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) {
// if pattern is SolidPattern, also change dc.color(for dc.Clear, dc.drawString)
if fillStyle, ok := pattern.(*solidPattern); ok {
dc.color = fillStyle.color
}
dc.fillPattern = pattern
}
// SetStrokeStyle sets current stroke style
func (dc *Context) SetStrokeStyle(pattern Pattern) {
dc.strokePattern = pattern
}
// SetColor sets the current color(for both fill and stroke).
func (dc *Context) SetColor(c color.Color) {
dc.setFillAndStrokeColor(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)}
dc.setFillAndStrokeColor(dc.color)
}
// 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),
}
dc.setFillAndStrokeColor(dc.color)
}
// 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, dc.dashOffset)
} else {
// TODO: this is a temporary workaround to remove tiny segments
// that result in rendering issues
path = rasterPath(flattenPath(path))
}
r := dc.rasterizer
r.UseNonZeroWinding = true
r.Clear()
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 := dc.rasterizer
r.UseNonZeroWinding = dc.fillRule == FillRuleWinding
r.Clear()
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() {
var painter raster.Painter
if dc.mask == nil {
if pattern, ok := dc.strokePattern.(*solidPattern); ok {
// with a nil mask and a solid color pattern, we can be more efficient
// TODO: refactor so we don't have to do this type assertion stuff?
p := raster.NewRGBAPainter(dc.im)
p.SetColor(pattern.color)
painter = p
}
}
if painter == nil {
painter = newPatternPainter(dc.im, dc.mask, dc.strokePattern)
}
dc.stroke(painter)
}
// 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() {
var painter raster.Painter
if dc.mask == nil {
if pattern, ok := dc.fillPattern.(*solidPattern); ok {
// with a nil mask and a solid color pattern, we can be more efficient
// TODO: refactor so we don't have to do this type assertion stuff?
p := raster.NewRGBAPainter(dc.im)
p.SetColor(pattern.color)
painter = p
}
}
if painter == nil {
painter = newPatternPainter(dc.im, dc.mask, dc.fillPattern)
}
dc.fill(painter)
}
// 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
}
}
// SetMask allows you to directly set the *image.Alpha to be used as a clipping
// mask. It must be the same size as the context, else an error is returned
// and the mask is unchanged.
func (dc *Context) SetMask(mask *image.Alpha) error {
if mask.Bounds().Size() != dc.im.Bounds().Size() {
return errors.New("mask size must match context size")
}
dc.mask = mask
return nil
}
// AsMask returns an *image.Alpha representing the alpha channel of this
// context. This can be useful for advanced clipping operations where you first
// render the mask geometry and then use it as a mask.
func (dc *Context) AsMask() *image.Alpha {
mask := image.NewAlpha(dc.im.Bounds())
draw.Draw(mask, dc.im.Bounds(), dc.im, image.ZP, draw.Src)
return mask
}
// InvertMask inverts the alpha values in the current clipping mask such that
// a fully transparent region becomes fully opaque and vice versa.
func (dc *Context) InvertMask() {
if dc.mask == nil {
dc.mask = image.NewAlpha(dc.im.Bounds())
} else {
for i, a := range dc.mask.Pix {
dc.mask.Pix[i] = 255 - a
}
}
}
// 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)
}
// SetPixel sets the color of the specified pixel using the current color.
func (dc *Context) SetPixel(x, y int) {
dc.im.Set(x, y, dc.color)
}
// DrawPoint is like DrawCircle but ensures that a circle of the specified
// size is drawn regardless of the current transformation matrix. The position
// is still transformed, but not the shape of the point.
func (dc *Context) DrawPoint(x, y, r float64) {
dc.Push()
tx, ty := dc.TransformPoint(x, y)
dc.Identity()
dc.DrawCircle(tx, ty, r)
dc.Pop()
}
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)/2)
y1 := y + ry*math.Sin((a1+a2)/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 {
if dc.hasCurrent {
dc.LineTo(x0, y0)
} else {
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.
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))
transformer := draw.BiLinear
fx, fy := float64(x), float64(y)
m := dc.matrix.Translate(fx, fy)
s2d := f64.Aff3{m.XX, m.XY, m.X0, m.YX, m.YY, m.Y0}
if dc.mask == nil {
transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, nil)
} else {
transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, &draw.Options{
DstMask: dc.mask,
DstMaskP: image.ZP,
})
}
}
// Text Functions
func (dc *Context) SetFontFace(fontFace font.Face) {
dc.fontFace = fontFace
dc.fontHeight = float64(fontFace.Metrics().Height) / 64
}
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) FontHeight() float64 {
return dc.fontHeight
}
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),
}
// based on Drawer.DrawString() in golang.org/x/image/font/font.go
prevC := rune(-1)
for _, c := range s {
if prevC >= 0 {
d.Dot.X += d.Face.Kern(prevC, c)
}
dr, mask, maskp, advance, ok := d.Face.Glyph(d.Dot, c)
if !ok {
// TODO: is falling back on the U+FFFD glyph the responsibility of
// the Drawer or the Face?
// TODO: set prevC = '\ufffd'?
continue
}
sr := dr.Sub(dr.Min)
transformer := draw.BiLinear
fx, fy := float64(dr.Min.X), float64(dr.Min.Y)
m := dc.matrix.Translate(fx, fy)
s2d := f64.Aff3{m.XX, m.XY, m.X0, m.YX, m.YY, m.Y0}
transformer.Transform(d.Dst, s2d, d.Src, sr, draw.Over, &draw.Options{
SrcMask: mask,
SrcMaskP: maskp,
})
d.Dot.X += advance
prevC = c
}
}
// DrawString draws the specified text at the specified point.
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 -= 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)
// sync h formula with MeasureMultilineString
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
}
}
func (dc *Context) MeasureMultilineString(s string, lineSpacing float64) (width, height float64) {
lines := strings.Split(s, "\n")
// sync h formula with DrawStringWrapped
height = float64(len(lines)) * dc.fontHeight * lineSpacing
height -= (lineSpacing - 1) * dc.fontHeight
d := &font.Drawer{
Face: dc.fontFace,
}
// max width from lines
for _, line := range lines {
adv := d.MeasureString(line)
currentWidth := float64(adv >> 6) // from gg.Context.MeasureString
if currentWidth > width {
width = currentWidth
}
}
return width, height
}
// 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 anticlockwise 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 anticlockwise 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
}