// 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) LoadFontFaceFromBytes(fontData []byte, points float64) error { face, err := LoadFontFaceFromBytes(fontData, 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 }