gg/context.go

733 lines
19 KiB
Go
Raw Normal View History

2016-02-23 22:30:53 +01:00
// Package gg provides a simple API for rendering 2D graphics in pure Go.
2016-02-19 15:43:37 +01:00
package gg
2016-02-18 22:02:57 +01:00
import (
"image"
"image/color"
"image/draw"
"image/png"
"io"
2016-02-19 04:11:53 +01:00
"math"
2016-02-18 22:02:57 +01:00
"github.com/golang/freetype/raster"
2016-02-19 04:53:47 +01:00
"golang.org/x/image/font"
"golang.org/x/image/font/basicfont"
2016-02-18 22:02:57 +01:00
)
type LineCap int
const (
LineCapRound LineCap = iota
LineCapButt
LineCapSquare
)
type LineJoin int
const (
LineJoinRound LineJoin = iota
LineJoinBevel
)
2016-02-19 03:30:17 +01:00
type FillRule int
const (
FillRuleWinding FillRule = iota
FillRuleEvenOdd
)
2016-02-25 03:26:51 +01:00
type Align int
const (
AlignLeft Align = iota
AlignCenter
AlignRight
)
2016-02-18 22:02:57 +01:00
type Context struct {
2016-02-19 20:03:52 +01:00
width int
height int
im *image.RGBA
2016-02-25 20:16:48 +01:00
mask *image.Alpha
2016-02-19 20:03:52 +01:00
color color.Color
2016-02-20 21:10:16 +01:00
strokePath raster.Path
fillPath raster.Path
start Point
current Point
2016-02-22 04:22:17 +01:00
hasCurrent bool
2016-02-23 03:00:39 +01:00
dashes []float64
2016-02-19 20:03:52 +01:00
lineWidth float64
lineCap LineCap
lineJoin LineJoin
fillRule FillRule
fontFace font.Face
fontHeight float64
matrix Matrix
stack []*Context
2016-02-18 22:02:57 +01:00
}
2016-02-23 22:30:53 +01:00
// NewContext creates a new image.RGBA with the specified width and height
// and prepares a context for rendering onto that image.
2016-02-18 22:02:57 +01:00
func NewContext(width, height int) *Context {
2016-02-19 17:07:25 +01:00
return NewContextForRGBA(image.NewRGBA(image.Rect(0, 0, width, height)))
}
2016-02-23 22:30:53 +01:00
// NewContextForImage copies the specified image into a new image.RGBA
// and prepares a context for rendering onto that image.
2016-02-19 17:07:25 +01:00
func NewContextForImage(im image.Image) *Context {
return NewContextForRGBA(imageToRGBA(im))
}
2016-02-23 22:30:53 +01:00
// NewContextForRGBA prepares a context for rendering onto the specified image.
// No copy is made.
2016-02-19 17:07:25 +01:00
func NewContextForRGBA(im *image.RGBA) *Context {
2016-02-18 22:02:57 +01:00
return &Context{
2016-02-19 20:03:52 +01:00
width: im.Bounds().Size().X,
height: im.Bounds().Size().Y,
im: im,
color: color.Transparent,
lineWidth: 1,
fillRule: FillRuleWinding,
fontFace: basicfont.Face7x13,
fontHeight: 13,
matrix: Identity(),
2016-02-18 22:02:57 +01:00
}
}
2016-02-23 22:30:53 +01:00
// Image returns the image that has been drawn by this context.
2016-02-19 04:11:53 +01:00
func (dc *Context) Image() image.Image {
return dc.im
2016-02-18 22:02:57 +01:00
}
2016-02-23 22:30:53 +01:00
// Width returns the width of the image in pixels.
2016-02-19 04:11:53 +01:00
func (dc *Context) Width() int {
return dc.width
2016-02-18 22:02:57 +01:00
}
2016-02-23 22:30:53 +01:00
// Height returns the height of the image in pixels.
2016-02-19 04:11:53 +01:00
func (dc *Context) Height() int {
return dc.height
2016-02-18 22:02:57 +01:00
}
2016-02-23 22:30:53 +01:00
// SavePNG encodes the image as a PNG and writes it to disk.
2016-02-19 19:54:55 +01:00
func (dc *Context) SavePNG(path string) error {
2016-02-19 19:49:03 +01:00
return SavePNG(path, dc.im)
2016-02-18 22:02:57 +01:00
}
// 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)
}
2016-02-23 22:30:53 +01:00
// 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.
2016-02-23 03:00:39 +01:00
func (dc *Context) SetDash(dashes ...float64) {
dc.dashes = dashes
}
2016-02-19 17:07:25 +01:00
func (dc *Context) SetLineWidth(lineWidth float64) {
dc.lineWidth = lineWidth
2016-02-18 22:02:57 +01:00
}
2016-02-19 17:07:25 +01:00
func (dc *Context) SetLineCap(lineCap LineCap) {
dc.lineCap = lineCap
2016-02-18 22:02:57 +01:00
}
2016-02-19 17:20:50 +01:00
func (dc *Context) SetLineCapRound() {
dc.lineCap = LineCapRound
}
func (dc *Context) SetLineCapButt() {
dc.lineCap = LineCapButt
}
func (dc *Context) SetLineCapSquare() {
dc.lineCap = LineCapSquare
}
2016-02-19 17:07:25 +01:00
func (dc *Context) SetLineJoin(lineJoin LineJoin) {
dc.lineJoin = lineJoin
2016-02-18 22:02:57 +01:00
}
2016-02-19 17:20:50 +01:00
func (dc *Context) SetLineJoinRound() {
dc.lineJoin = LineJoinRound
}
func (dc *Context) SetLineJoinBevel() {
dc.lineJoin = LineJoinBevel
}
2016-02-19 17:07:25 +01:00
func (dc *Context) SetFillRule(fillRule FillRule) {
dc.fillRule = fillRule
2016-02-18 22:02:57 +01:00
}
2016-02-19 17:20:50 +01:00
func (dc *Context) SetFillRuleWinding() {
dc.fillRule = FillRuleWinding
}
func (dc *Context) SetFillRuleEvenOdd() {
dc.fillRule = FillRuleEvenOdd
}
2016-02-19 17:07:25 +01:00
// Color Setters
2016-02-23 22:30:53 +01:00
// SetColor sets the current color.
2016-02-19 17:07:25 +01:00
func (dc *Context) SetColor(c color.Color) {
dc.color = c
2016-02-18 22:02:57 +01:00
}
2016-02-23 22:30:53 +01:00
// 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.
2016-02-19 17:07:25 +01:00
func (dc *Context) SetHexColor(x string) {
2016-02-20 21:42:36 +01:00
r, g, b, a := parseHexColor(x)
dc.SetRGBA255(r, g, b, a)
2016-02-19 17:07:25 +01:00
}
2016-02-23 22:30:53 +01:00
// SetRGBA255 sets the current color. r, g, b, a values should be between 0 and
// 255, inclusive.
2016-02-19 17:07:25 +01:00
func (dc *Context) SetRGBA255(r, g, b, a int) {
dc.color = color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(a)}
}
2016-02-23 22:30:53 +01:00
// SetRGB255 sets the current color. r, g, b values should be between 0 and 255,
// inclusive. Alpha will be set to 255 (fully opaque).
2016-02-19 17:07:25 +01:00
func (dc *Context) SetRGB255(r, g, b int) {
dc.SetRGBA255(r, g, b, 255)
}
2016-02-23 22:30:53 +01:00
// SetRGBA sets the current color. r, g, b, a values should be between 0 and 1,
// inclusive.
2016-02-19 17:07:25 +01:00
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),
2016-02-18 22:02:57 +01:00
}
}
2016-02-23 22:30:53 +01:00
// SetRGB sets the current color. r, g, b values should be between 0 and 1,
// inclusive. Alpha will be set to 1 (fully opaque).
2016-02-19 17:07:25 +01:00
func (dc *Context) SetRGB(r, g, b float64) {
dc.SetRGBA(r, g, b, 1)
2016-02-19 03:30:17 +01:00
}
2016-02-19 17:07:25 +01:00
// Path Manipulation
2016-02-23 22:30:53 +01:00
// MoveTo starts a new subpath within the current path starting at the
// specified point.
2016-02-19 04:11:53 +01:00
func (dc *Context) MoveTo(x, y float64) {
2016-02-22 04:22:17 +01:00
if dc.hasCurrent {
dc.fillPath.Add1(dc.start.Fixed())
2016-02-20 21:10:16 +01:00
}
2016-02-19 19:16:40 +01:00
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
2016-02-22 04:22:17 +01:00
dc.hasCurrent = true
2016-02-18 22:02:57 +01:00
}
2016-02-23 22:30:53 +01:00
// 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)
2016-02-19 04:11:53 +01:00
func (dc *Context) LineTo(x, y float64) {
2016-02-22 04:22:17 +01:00
if !dc.hasCurrent {
2016-02-19 04:11:53 +01:00
dc.MoveTo(x, y)
2016-02-19 03:30:17 +01:00
} else {
2016-02-20 21:27:48 +01:00
x, y = dc.TransformPoint(x, y)
p := Point{x, y}
dc.strokePath.Add1(p.Fixed())
dc.fillPath.Add1(p.Fixed())
dc.current = p
2016-02-19 03:30:17 +01:00
}
2016-02-18 22:02:57 +01:00
}
2016-02-23 22:30:53 +01:00
// 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)
2016-02-19 04:11:53 +01:00
func (dc *Context) QuadraticTo(x1, y1, x2, y2 float64) {
2016-02-22 04:22:17 +01:00
if !dc.hasCurrent {
2016-02-19 04:11:53 +01:00
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
}
2016-02-23 22:30:53 +01:00
// 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) {
2016-02-22 04:22:17 +01:00
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)
2016-02-21 22:34:28 +01:00
previous := dc.current.Fixed()
for _, p := range points[1:] {
f := p.Fixed()
2016-02-21 22:34:28 +01:00
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
2016-02-19 03:30:17 +01:00
}
2016-02-18 22:02:57 +01:00
}
2016-02-23 22:30:53 +01:00
// 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.
2016-02-19 04:11:53 +01:00
func (dc *Context) ClosePath() {
2016-02-22 04:22:17 +01:00
if dc.hasCurrent {
dc.strokePath.Add1(dc.start.Fixed())
dc.fillPath.Add1(dc.start.Fixed())
dc.current = dc.start
2016-02-19 03:30:17 +01:00
}
2016-02-18 22:02:57 +01:00
}
2016-02-23 22:30:53 +01:00
// ClearPath clears the current path. There is no current point after this
// operation.
2016-02-19 17:07:25 +01:00
func (dc *Context) ClearPath() {
2016-02-20 21:10:16 +01:00
dc.strokePath.Clear()
dc.fillPath.Clear()
2016-02-22 04:22:17 +01:00
dc.hasCurrent = false
}
2016-02-23 22:30:53 +01:00
// NewSubPath starts a new subpath within the current path. There is no current
// point after this operation.
2016-02-22 04:22:17 +01:00
func (dc *Context) NewSubPath() {
if dc.hasCurrent {
dc.fillPath.Add1(dc.start.Fixed())
}
dc.hasCurrent = false
2016-02-18 22:02:57 +01:00
}
2016-02-19 17:07:25 +01:00
// Path Drawing
2016-02-20 02:54:59 +01:00
func (dc *Context) capper() raster.Capper {
2016-02-19 17:07:25 +01:00
switch dc.lineCap {
case LineCapButt:
2016-02-20 02:54:59 +01:00
return raster.ButtCapper
2016-02-19 17:07:25 +01:00
case LineCapRound:
2016-02-20 02:54:59 +01:00
return raster.RoundCapper
2016-02-19 17:07:25 +01:00
case LineCapSquare:
2016-02-20 02:54:59 +01:00
return raster.SquareCapper
2016-02-19 17:07:25 +01:00
}
2016-02-20 02:54:59 +01:00
return nil
}
func (dc *Context) joiner() raster.Joiner {
2016-02-19 17:07:25 +01:00
switch dc.lineJoin {
case LineJoinBevel:
2016-02-20 02:54:59 +01:00
return raster.BevelJoiner
2016-02-19 17:07:25 +01:00
case LineJoinRound:
2016-02-20 02:54:59 +01:00
return raster.RoundJoiner
2016-02-19 17:07:25 +01:00
}
2016-02-20 02:54:59 +01:00
return nil
}
2016-02-25 20:16:48 +01:00
func (dc *Context) stroke(painter raster.Painter) {
2016-02-23 03:00:39 +01:00
path := dc.strokePath
if len(dc.dashes) > 0 {
path = dashed(path, dc.dashes)
2016-02-25 03:45:09 +01:00
} else {
// TODO: this is a temporary workaround to remove tiny segments
// that result in rendering issues
path = rasterPath(flattenPath(path))
2016-02-23 03:00:39 +01:00
}
2016-02-19 04:11:53 +01:00
r := raster.NewRasterizer(dc.width, dc.height)
2016-02-18 22:02:57 +01:00
r.UseNonZeroWinding = true
2016-02-23 04:33:22 +01:00
r.AddStroke(path, fix(dc.lineWidth), dc.capper(), dc.joiner())
2016-02-18 22:02:57 +01:00
r.Rasterize(painter)
}
2016-02-25 20:16:48 +01:00
func (dc *Context) fill(painter raster.Painter) {
path := dc.fillPath
if dc.hasCurrent {
path = make(raster.Path, len(dc.fillPath))
copy(path, dc.fillPath)
path.Add1(dc.start.Fixed())
}
r := raster.NewRasterizer(dc.width, dc.height)
r.UseNonZeroWinding = dc.fillRule == FillRuleWinding
r.AddPath(path)
r.Rasterize(painter)
}
// StrokePreserve strokes the current path with the current color, line width,
// line cap, line join and dash settings. The path is preserved after this
// operation.
func (dc *Context) StrokePreserve() {
if dc.mask == nil {
painter := raster.NewRGBAPainter(dc.im)
painter.SetColor(dc.color)
dc.stroke(painter)
} else {
im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height))
painter := raster.NewRGBAPainter(im)
painter.SetColor(dc.color)
dc.stroke(painter)
draw.DrawMask(dc.im, dc.im.Bounds(), im, image.ZP, dc.mask, image.ZP, draw.Over)
}
}
2016-02-23 22:30:53 +01:00
// 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.
2016-02-19 04:11:53 +01:00
func (dc *Context) Stroke() {
dc.StrokePreserve()
2016-02-19 17:07:25 +01:00
dc.ClearPath()
2016-02-18 22:02:57 +01:00
}
2016-02-23 22:30:53 +01:00
// FillPreserve fills the current path with the current color. Open subpaths
// are implicity closed. The path is preserved after this operation.
2016-02-19 04:11:53 +01:00
func (dc *Context) FillPreserve() {
2016-02-25 20:16:48 +01:00
if dc.mask == nil {
painter := raster.NewRGBAPainter(dc.im)
painter.SetColor(dc.color)
dc.fill(painter)
} else {
im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height))
painter := raster.NewRGBAPainter(im)
painter.SetColor(dc.color)
dc.fill(painter)
draw.DrawMask(dc.im, dc.im.Bounds(), im, image.ZP, dc.mask, image.ZP, draw.Over)
2016-02-20 21:10:16 +01:00
}
2016-02-18 22:02:57 +01:00
}
2016-02-23 22:30:53 +01:00
// Fill fills the current path with the current color. Open subpaths
// are implicity closed. The path is cleared after this operation.
2016-02-19 04:11:53 +01:00
func (dc *Context) Fill() {
dc.FillPreserve()
2016-02-19 17:07:25 +01:00
dc.ClearPath()
2016-02-19 04:11:53 +01:00
}
2016-02-25 20:16:48 +01:00
// 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 {
2016-02-26 14:39:32 +01:00
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)
2016-02-25 20:16:48 +01:00
dc.mask = mask
}
}
// Clip updates the clipping region by intersecting the current
// clipping region with the current path as it would be filled by dc.Fill().
// The path is cleared after this operation.
func (dc *Context) Clip() {
dc.ClipPreserve()
dc.ClearPath()
}
// ResetClip clears the clipping region.
func (dc *Context) ResetClip() {
dc.mask = nil
}
2016-02-19 04:53:47 +01:00
// Convenient Drawing Functions
2016-02-23 22:30:53 +01:00
// Clear fills the entire image with the current color.
2016-02-19 17:07:25 +01:00
func (dc *Context) Clear() {
2016-02-20 03:00:56 +01:00
src := image.NewUniform(dc.color)
draw.Draw(dc.im, dc.im.Bounds(), src, image.ZP, draw.Src)
2016-02-19 17:07:25 +01:00
}
2016-02-19 04:11:53 +01:00
func (dc *Context) DrawLine(x1, y1, x2, y2 float64) {
dc.MoveTo(x1, y1)
dc.LineTo(x2, y2)
}
2016-02-19 17:07:25 +01:00
func (dc *Context) DrawRectangle(x, y, w, h float64) {
dc.MoveTo(x, y)
dc.LineTo(x+w, y)
dc.LineTo(x+w, y+h)
dc.LineTo(x, y+h)
dc.LineTo(x, y)
}
2016-02-20 03:15:22 +01:00
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.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))
}
2016-02-19 17:07:25 +01:00
func (dc *Context) DrawEllipticalArc(x, y, rx, ry, angle1, angle2 float64) {
2016-02-19 04:11:53 +01:00
const n = 16
2016-02-20 02:46:19 +01:00
for i := 0; i < n; i++ {
2016-02-19 04:11:53 +01:00
p1 := float64(i+0) / n
p2 := float64(i+1) / n
a1 := angle1 + (angle2-angle1)*p1
a2 := angle1 + (angle2-angle1)*p2
x0 := x + rx*math.Cos(a1)
y0 := y + ry*math.Sin(a1)
x1 := x + rx*math.Cos(a1+(a2-a1)/2)
y1 := y + ry*math.Sin(a1+(a2-a1)/2)
x2 := x + rx*math.Cos(a2)
y2 := y + ry*math.Sin(a2)
cx := 2*x1 - x0/2 - x2/2
cy := 2*y1 - y0/2 - y2/2
if i == 0 {
dc.MoveTo(x0, y0)
}
dc.QuadraticTo(cx, cy, x2, y2)
}
}
func (dc *Context) DrawEllipse(x, y, rx, ry float64) {
2016-02-19 17:07:25 +01:00
dc.DrawEllipticalArc(x, y, rx, ry, 0, 2*math.Pi)
2016-02-19 04:11:53 +01:00
}
func (dc *Context) DrawArc(x, y, r, angle1, angle2 float64) {
2016-02-19 17:07:25 +01:00
dc.DrawEllipticalArc(x, y, r, r, angle1, angle2)
2016-02-19 04:11:53 +01:00
}
func (dc *Context) DrawCircle(x, y, r float64) {
2016-02-19 17:07:25 +01:00
dc.DrawEllipticalArc(x, y, r, r, 0, 2*math.Pi)
2016-02-18 22:02:57 +01:00
}
2016-02-19 04:53:47 +01:00
2016-02-25 18:46:59 +01:00
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()
}
2016-02-23 22:30:53 +01:00
// DrawImage draws the specified image at the specified point.
// Currently, rotation and scaling transforms are not supported.
2016-02-19 19:49:03 +01:00
func (dc *Context) DrawImage(im image.Image, x, y int) {
2016-02-20 03:35:56 +01:00
dc.DrawImageAnchored(im, x, y, 0, 0)
}
2016-02-23 22:30:53 +01:00
// 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.
2016-02-20 03:35:56 +01:00
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))
2016-02-19 19:49:03 +01:00
p := image.Pt(x, y)
2016-02-20 03:35:56 +01:00
r := image.Rectangle{p, p.Add(s)}
2016-02-25 20:16:48 +01:00
if dc.mask == nil {
draw.Draw(dc.im, r, im, image.ZP, draw.Over)
} else {
draw.DrawMask(dc.im, r, im, image.ZP, dc.mask, p, draw.Over)
}
2016-02-19 19:49:03 +01:00
}
2016-02-19 04:53:47 +01:00
// Text Functions
func (dc *Context) SetFontFace(fontFace font.Face) {
dc.fontFace = fontFace
}
2016-02-19 20:03:52 +01:00
func (dc *Context) LoadFontFace(path string, points float64) {
2016-02-23 04:36:34 +01:00
if face, err := loadFontFace(path, points); err == nil {
dc.fontFace = face
dc.fontHeight = points * 72 / 96
} else {
dc.fontFace = basicfont.Face7x13
dc.fontHeight = 13
}
2016-02-19 04:53:47 +01:00
}
2016-02-25 20:16:48 +01:00
func (dc *Context) drawString(im *image.RGBA, s string, x, y float64) {
d := &font.Drawer{
Dst: im,
Src: image.NewUniform(dc.color),
Face: dc.fontFace,
Dot: fixp(x, y),
}
d.DrawString(s)
}
2016-02-23 22:30:53 +01:00
// DrawString draws the specified text at the specified point.
// Currently, rotation and scaling transforms are not supported.
2016-02-20 03:35:56 +01:00
func (dc *Context) DrawString(s string, x, y float64) {
dc.DrawStringAnchored(s, x, y, 0, 0)
}
2016-02-23 22:30:53 +01:00
// 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.
2016-02-20 03:35:56 +01:00
func (dc *Context) DrawStringAnchored(s string, x, y, ax, ay float64) {
w, h := dc.MeasureString(s)
x -= ax * w
y += ay * h
2016-02-19 19:16:40 +01:00
x, y = dc.TransformPoint(x, y)
2016-02-25 20:16:48 +01:00
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)
2016-02-19 04:53:47 +01:00
}
}
2016-02-25 03:26:51 +01:00
// DrawStringWrapped word-wraps the specified string to the given max width
// and then draws it at the specified anchor point using the given line
// spacing and text alignment.
func (dc *Context) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align) {
lines := dc.WordWrap(s, width)
h := float64(len(lines)) * dc.fontHeight * lineSpacing
h -= (lineSpacing - 1) * dc.fontHeight
x -= ax * width
y -= ay * h
switch align {
case AlignLeft:
ax = 0
case AlignCenter:
ax = 0.5
x += width / 2
case AlignRight:
ax = 1
x += width
}
ay = 1
2016-02-24 23:22:21 +01:00
for _, line := range lines {
2016-02-25 03:26:51 +01:00
dc.DrawStringAnchored(line, x, y, ax, ay)
y += dc.fontHeight * lineSpacing
2016-02-24 23:22:21 +01:00
}
}
2016-02-23 22:30:53 +01:00
// MeasureString returns the rendered width and height of the specified text
// given the current font face.
2016-02-19 20:03:52 +01:00
func (dc *Context) MeasureString(s string) (w, h float64) {
2016-02-19 04:53:47 +01:00
d := &font.Drawer{
Face: dc.fontFace,
}
a := d.MeasureString(s)
2016-02-19 20:03:52 +01:00
return float64(a >> 6), dc.fontHeight
2016-02-19 04:53:47 +01:00
}
2016-02-19 19:16:40 +01:00
2016-02-25 03:26:51 +01:00
// 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)
}
2016-02-19 19:16:40 +01:00
// Transformation Matrix Operations
2016-02-23 22:30:53 +01:00
// Identity resets the current transformation matrix to the identity matrix.
// This results in no translating, scaling, rotating, or shearing.
2016-02-19 19:16:40 +01:00
func (dc *Context) Identity() {
dc.matrix = Identity()
}
2016-02-23 22:30:53 +01:00
// Translate updates the current matrix with a translation.
2016-02-19 19:16:40 +01:00
func (dc *Context) Translate(x, y float64) {
dc.matrix = dc.matrix.Translate(x, y)
}
2016-02-23 22:30:53 +01:00
// Scale updates the current matrix with a scaling factor.
// Scaling occurs about the origin.
2016-02-19 19:16:40 +01:00
func (dc *Context) Scale(x, y float64) {
dc.matrix = dc.matrix.Scale(x, y)
}
2016-02-23 22:30:53 +01:00
// ScaleAbout updates the current matrix with a scaling factor.
// Scaling occurs about the specified point.
2016-02-20 20:45:17 +01:00
func (dc *Context) ScaleAbout(sx, sy, x, y float64) {
2016-02-21 21:54:54 +01:00
dc.Translate(x, y)
dc.Scale(sx, sy)
dc.Translate(-x, -y)
2016-02-20 20:45:17 +01:00
}
2016-02-23 22:30:53 +01:00
// Rotate updates the current matrix with a clockwise rotation.
// Rotation occurs about the origin. Angle is specified in radians.
2016-02-19 19:16:40 +01:00
func (dc *Context) Rotate(angle float64) {
dc.matrix = dc.matrix.Rotate(angle)
}
2016-02-23 22:30:53 +01:00
// RotateAbout updates the current matrix with a clockwise rotation.
// Rotation occurs about the specified point. Angle is specified in radians.
2016-02-19 19:16:40 +01:00
func (dc *Context) RotateAbout(angle, x, y float64) {
2016-02-21 21:54:54 +01:00
dc.Translate(x, y)
dc.Rotate(angle)
dc.Translate(-x, -y)
2016-02-19 19:16:40 +01:00
}
2016-02-23 22:30:53 +01:00
// Shear updates the current matrix with a shearing angle.
// Shearing occurs about the origin.
2016-02-19 19:16:40 +01:00
func (dc *Context) Shear(x, y float64) {
dc.matrix = dc.matrix.Shear(x, y)
}
2016-02-23 22:30:53 +01:00
// ShearAbout updates the current matrix with a shearing angle.
// Shearing occurs about the specified point.
2016-02-20 20:53:33 +01:00
func (dc *Context) ShearAbout(sx, sy, x, y float64) {
2016-02-21 21:54:54 +01:00
dc.Translate(x, y)
dc.Shear(sx, sy)
dc.Translate(-x, -y)
2016-02-20 20:53:33 +01:00
}
2016-02-23 22:30:53 +01:00
// TransformPoint multiplies the specified point by the current matrix,
// returning a transformed position.
2016-02-19 19:16:40 +01:00
func (dc *Context) TransformPoint(x, y float64) (tx, ty float64) {
return dc.matrix.TransformPoint(x, y)
}
2016-02-19 19:34:04 +01:00
2016-02-23 22:30:53 +01:00
// InvertY flips the Y axis so that Y grows from bottom to top and Y=0 is at
// the bottom of the image.
2016-02-20 03:00:56 +01:00
func (dc *Context) InvertY() {
dc.Translate(0, float64(dc.height))
dc.Scale(1, -1)
}
2016-02-19 19:34:04 +01:00
// Stack
2016-02-23 22:30:53 +01:00
// Push saves the current state of the context for later retrieval. These
// can be nested.
2016-02-19 19:34:04 +01:00
func (dc *Context) Push() {
2016-02-19 20:10:39 +01:00
x := *dc
dc.stack = append(dc.stack, &x)
2016-02-19 19:34:04 +01:00
}
2016-02-23 22:44:53 +01:00
// Pop restores the last saved context state from the stack.
2016-02-19 19:34:04 +01:00
func (dc *Context) Pop() {
2016-02-20 01:45:49 +01:00
before := *dc
2016-02-19 19:34:04 +01:00
s := dc.stack
x, s := s[len(s)-1], s[:len(s)-1]
2016-02-19 20:10:39 +01:00
*dc = *x
2016-02-25 20:16:48 +01:00
dc.mask = before.mask
2016-02-20 21:10:16 +01:00
dc.strokePath = before.strokePath
dc.fillPath = before.fillPath
2016-02-21 00:12:34 +01:00
dc.start = before.start
dc.current = before.current
dc.hasCurrent = before.hasCurrent
2016-02-19 19:34:04 +01:00
}