gg/context.go
George Stark 2084297999 add method to change drawing interpolator
default interpolator used for drawing text and images is draw.BiLinear
and it's rather slow against e.g. draw.NearestNeighbor
The difference is significant especially on weak hardware
e.g. on raspberry pi zero (ARMv6) drawing short string can be 10-16 times
faster with draw.NearestNeighbor

Signed-off-by: George Stark <stark.georgy@gmail.com>
2021-03-30 13:39:24 +03:00

929 lines
24 KiB
Go

// 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
interpolator draw.Interpolator
}
// 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(),
interpolator: draw.BiLinear,
}
}
func (dc *Context) SetInterpolator(interpolator draw.Interpolator) {
dc.interpolator = interpolator
}
// 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 := dc.interpolator
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 := dc.interpolator
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
}