mirror of
				https://github.com/esimov/pigo.git
				synced 2025-10-31 19:32:40 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			769 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			769 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Package gg provides a simple API for rendering 2D graphics in pure Go.
 | |
| package gg
 | |
| 
 | |
| import (
 | |
| 	"image"
 | |
| 	"image/color"
 | |
| 	"image/draw"
 | |
| 	"image/png"
 | |
| 	"io"
 | |
| 	"math"
 | |
| 
 | |
| 	"github.com/golang/freetype/raster"
 | |
| 	"golang.org/x/image/font"
 | |
| 	"golang.org/x/image/font/basicfont"
 | |
| )
 | |
| 
 | |
| type LineCap int
 | |
| 
 | |
| const (
 | |
| 	LineCapRound LineCap = iota
 | |
| 	LineCapButt
 | |
| 	LineCapSquare
 | |
| )
 | |
| 
 | |
| type LineJoin int
 | |
| 
 | |
| const (
 | |
| 	LineJoinRound LineJoin = iota
 | |
| 	LineJoinBevel
 | |
| )
 | |
| 
 | |
| type FillRule int
 | |
| 
 | |
| const (
 | |
| 	FillRuleWinding FillRule = iota
 | |
| 	FillRuleEvenOdd
 | |
| )
 | |
| 
 | |
| type Align int
 | |
| 
 | |
| const (
 | |
| 	AlignLeft Align = iota
 | |
| 	AlignCenter
 | |
| 	AlignRight
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	defaultFillStyle   = NewSolidPattern(color.White)
 | |
| 	defaultStrokeStyle = NewSolidPattern(color.Black)
 | |
| )
 | |
| 
 | |
| type Context struct {
 | |
| 	width         int
 | |
| 	height        int
 | |
| 	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
 | |
| 	lineWidth     float64
 | |
| 	lineCap       LineCap
 | |
| 	lineJoin      LineJoin
 | |
| 	fillRule      FillRule
 | |
| 	fontFace      font.Face
 | |
| 	fontHeight    float64
 | |
| 	matrix        Matrix
 | |
| 	stack         []*Context
 | |
| }
 | |
| 
 | |
| // NewContext creates a new image.RGBA with the specified width and height
 | |
| // and prepares a context for rendering onto that image.
 | |
| func NewContext(width, height int) *Context {
 | |
| 	return NewContextForRGBA(image.NewRGBA(image.Rect(0, 0, width, height)))
 | |
| }
 | |
| 
 | |
| // NewContextForImage copies the specified image into a new image.RGBA
 | |
| // and prepares a context for rendering onto that image.
 | |
| func NewContextForImage(im image.Image) *Context {
 | |
| 	return NewContextForRGBA(imageToRGBA(im))
 | |
| }
 | |
| 
 | |
| // NewContextForRGBA prepares a context for rendering onto the specified image.
 | |
| // No copy is made.
 | |
| func NewContextForRGBA(im *image.RGBA) *Context {
 | |
| 	return &Context{
 | |
| 		width:         im.Bounds().Size().X,
 | |
| 		height:        im.Bounds().Size().Y,
 | |
| 		im:            im,
 | |
| 		color:         color.Transparent,
 | |
| 		fillPattern:   defaultFillStyle,
 | |
| 		strokePattern: defaultStrokeStyle,
 | |
| 		lineWidth:     1,
 | |
| 		fillRule:      FillRuleWinding,
 | |
| 		fontFace:      basicfont.Face7x13,
 | |
| 		fontHeight:    13,
 | |
| 		matrix:        Identity(),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Image returns the image that has been drawn by this context.
 | |
| func (dc *Context) Image() image.Image {
 | |
| 	return dc.im
 | |
| }
 | |
| 
 | |
| // Width returns the width of the image in pixels.
 | |
| func (dc *Context) Width() int {
 | |
| 	return dc.width
 | |
| }
 | |
| 
 | |
| // Height returns the height of the image in pixels.
 | |
| func (dc *Context) Height() int {
 | |
| 	return dc.height
 | |
| }
 | |
| 
 | |
| // SavePNG encodes the image as a PNG and writes it to disk.
 | |
| func (dc *Context) SavePNG(path string) error {
 | |
| 	return SavePNG(path, dc.im)
 | |
| }
 | |
| 
 | |
| // EncodePNG encodes the image as a PNG and writes it to the provided io.Writer.
 | |
| func (dc *Context) EncodePNG(w io.Writer) error {
 | |
| 	return png.Encode(w, dc.im)
 | |
| }
 | |
| 
 | |
| // SetDash sets the current dash pattern to use. Call with zero arguments to
 | |
| // disable dashes. The values specify the lengths of each dash, with
 | |
| // alternating on and off lengths.
 | |
| func (dc *Context) SetDash(dashes ...float64) {
 | |
| 	dc.dashes = dashes
 | |
| }
 | |
| 
 | |
| func (dc *Context) SetLineWidth(lineWidth float64) {
 | |
| 	dc.lineWidth = lineWidth
 | |
| }
 | |
| 
 | |
| func (dc *Context) SetLineCap(lineCap LineCap) {
 | |
| 	dc.lineCap = lineCap
 | |
| }
 | |
| 
 | |
| func (dc *Context) SetLineCapRound() {
 | |
| 	dc.lineCap = LineCapRound
 | |
| }
 | |
| 
 | |
| func (dc *Context) SetLineCapButt() {
 | |
| 	dc.lineCap = LineCapButt
 | |
| }
 | |
| 
 | |
| func (dc *Context) SetLineCapSquare() {
 | |
| 	dc.lineCap = LineCapSquare
 | |
| }
 | |
| 
 | |
| func (dc *Context) SetLineJoin(lineJoin LineJoin) {
 | |
| 	dc.lineJoin = lineJoin
 | |
| }
 | |
| 
 | |
| func (dc *Context) SetLineJoinRound() {
 | |
| 	dc.lineJoin = LineJoinRound
 | |
| }
 | |
| 
 | |
| func (dc *Context) SetLineJoinBevel() {
 | |
| 	dc.lineJoin = LineJoinBevel
 | |
| }
 | |
| 
 | |
| func (dc *Context) SetFillRule(fillRule FillRule) {
 | |
| 	dc.fillRule = fillRule
 | |
| }
 | |
| 
 | |
| func (dc *Context) SetFillRuleWinding() {
 | |
| 	dc.fillRule = FillRuleWinding
 | |
| }
 | |
| 
 | |
| func (dc *Context) SetFillRuleEvenOdd() {
 | |
| 	dc.fillRule = FillRuleEvenOdd
 | |
| }
 | |
| 
 | |
| // Color Setters
 | |
| 
 | |
| 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)
 | |
| 	} else {
 | |
| 		// TODO: this is a temporary workaround to remove tiny segments
 | |
| 		// that result in rendering issues
 | |
| 		path = rasterPath(flattenPath(path))
 | |
| 	}
 | |
| 	r := raster.NewRasterizer(dc.width, dc.height)
 | |
| 	r.UseNonZeroWinding = true
 | |
| 	r.AddStroke(path, fix(dc.lineWidth), dc.capper(), dc.joiner())
 | |
| 	r.Rasterize(painter)
 | |
| }
 | |
| 
 | |
| func (dc *Context) fill(painter raster.Painter) {
 | |
| 	path := dc.fillPath
 | |
| 	if dc.hasCurrent {
 | |
| 		path = make(raster.Path, len(dc.fillPath))
 | |
| 		copy(path, dc.fillPath)
 | |
| 		path.Add1(dc.start.Fixed())
 | |
| 	}
 | |
| 	r := raster.NewRasterizer(dc.width, dc.height)
 | |
| 	r.UseNonZeroWinding = dc.fillRule == FillRuleWinding
 | |
| 	r.AddPath(path)
 | |
| 	r.Rasterize(painter)
 | |
| }
 | |
| 
 | |
| // StrokePreserve strokes the current path with the current color, line width,
 | |
| // line cap, line join and dash settings. The path is preserved after this
 | |
| // operation.
 | |
| func (dc *Context) StrokePreserve() {
 | |
| 	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() {
 | |
| 	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
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // 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-a1)/2)
 | |
| 		y1 := y + ry*math.Sin(a1+(a2-a1)/2)
 | |
| 		x2 := x + rx*math.Cos(a2)
 | |
| 		y2 := y + ry*math.Sin(a2)
 | |
| 		cx := 2*x1 - x0/2 - x2/2
 | |
| 		cy := 2*y1 - y0/2 - y2/2
 | |
| 		if i == 0 && !dc.hasCurrent {
 | |
| 			dc.MoveTo(x0, y0)
 | |
| 		}
 | |
| 		dc.QuadraticTo(cx, cy, x2, y2)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (dc *Context) DrawEllipse(x, y, rx, ry float64) {
 | |
| 	dc.NewSubPath()
 | |
| 	dc.DrawEllipticalArc(x, y, rx, ry, 0, 2*math.Pi)
 | |
| 	dc.ClosePath()
 | |
| }
 | |
| 
 | |
| func (dc *Context) DrawArc(x, y, r, angle1, angle2 float64) {
 | |
| 	dc.DrawEllipticalArc(x, y, r, r, angle1, angle2)
 | |
| }
 | |
| 
 | |
| func (dc *Context) DrawCircle(x, y, r float64) {
 | |
| 	dc.NewSubPath()
 | |
| 	dc.DrawEllipticalArc(x, y, r, r, 0, 2*math.Pi)
 | |
| 	dc.ClosePath()
 | |
| }
 | |
| 
 | |
| func (dc *Context) DrawRegularPolygon(n int, x, y, r, rotation float64) {
 | |
| 	angle := 2 * math.Pi / float64(n)
 | |
| 	rotation -= math.Pi / 2
 | |
| 	if n%2 == 0 {
 | |
| 		rotation += angle / 2
 | |
| 	}
 | |
| 	dc.NewSubPath()
 | |
| 	for i := 0; i < n; i++ {
 | |
| 		a := rotation + angle*float64(i)
 | |
| 		dc.LineTo(x+r*math.Cos(a), y+r*math.Sin(a))
 | |
| 	}
 | |
| 	dc.ClosePath()
 | |
| }
 | |
| 
 | |
| // DrawImage draws the specified image at the specified point.
 | |
| // Currently, rotation and scaling transforms are not supported.
 | |
| func (dc *Context) DrawImage(im image.Image, x, y int) {
 | |
| 	dc.DrawImageAnchored(im, x, y, 0, 0)
 | |
| }
 | |
| 
 | |
| // DrawImageAnchored draws the specified image at the specified anchor point.
 | |
| // The anchor point is x - w * ax, y - h * ay, where w, h is the size of the
 | |
| // image. Use ax=0.5, ay=0.5 to center the image at the specified point.
 | |
| func (dc *Context) DrawImageAnchored(im image.Image, x, y int, ax, ay float64) {
 | |
| 	s := im.Bounds().Size()
 | |
| 	x -= int(ax * float64(s.X))
 | |
| 	y -= int(ay * float64(s.Y))
 | |
| 	p := image.Pt(x, y)
 | |
| 	r := image.Rectangle{p, p.Add(s)}
 | |
| 	if dc.mask == nil {
 | |
| 		draw.Draw(dc.im, r, im, image.ZP, draw.Over)
 | |
| 	} else {
 | |
| 		draw.DrawMask(dc.im, r, im, image.ZP, dc.mask, p, draw.Over)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Text Functions
 | |
| 
 | |
| func (dc *Context) SetFontFace(fontFace font.Face) {
 | |
| 	dc.fontFace = fontFace
 | |
| 	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) drawString(im *image.RGBA, s string, x, y float64) {
 | |
| 	d := &font.Drawer{
 | |
| 		Dst:  im,
 | |
| 		Src:  image.NewUniform(dc.color),
 | |
| 		Face: dc.fontFace,
 | |
| 		Dot:  fixp(x, y),
 | |
| 	}
 | |
| 	d.DrawString(s)
 | |
| }
 | |
| 
 | |
| // DrawString draws the specified text at the specified point.
 | |
| // Currently, rotation and scaling transforms are not supported.
 | |
| func (dc *Context) DrawString(s string, x, y float64) {
 | |
| 	dc.DrawStringAnchored(s, x, y, 0, 0)
 | |
| }
 | |
| 
 | |
| // DrawStringAnchored draws the specified text at the specified anchor point.
 | |
| // The anchor point is x - w * ax, y - h * ay, where w, h is the size of the
 | |
| // text. Use ax=0.5, ay=0.5 to center the text at the specified point.
 | |
| func (dc *Context) DrawStringAnchored(s string, x, y, ax, ay float64) {
 | |
| 	w, h := dc.MeasureString(s)
 | |
| 	x, y = dc.TransformPoint(x, y)
 | |
| 	x -= ax * w
 | |
| 	y += ay * h
 | |
| 	if dc.mask == nil {
 | |
| 		dc.drawString(dc.im, s, x, y)
 | |
| 	} else {
 | |
| 		im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height))
 | |
| 		dc.drawString(im, s, x, y)
 | |
| 		draw.DrawMask(dc.im, dc.im.Bounds(), im, image.ZP, dc.mask, image.ZP, draw.Over)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // DrawStringWrapped word-wraps the specified string to the given max width
 | |
| // and then draws it at the specified anchor point using the given line
 | |
| // spacing and text alignment.
 | |
| func (dc *Context) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align) {
 | |
| 	lines := dc.WordWrap(s, width)
 | |
| 	h := float64(len(lines)) * dc.fontHeight * lineSpacing
 | |
| 	h -= (lineSpacing - 1) * dc.fontHeight
 | |
| 	x -= ax * width
 | |
| 	y -= ay * h
 | |
| 	switch align {
 | |
| 	case AlignLeft:
 | |
| 		ax = 0
 | |
| 	case AlignCenter:
 | |
| 		ax = 0.5
 | |
| 		x += width / 2
 | |
| 	case AlignRight:
 | |
| 		ax = 1
 | |
| 		x += width
 | |
| 	}
 | |
| 	ay = 1
 | |
| 	for _, line := range lines {
 | |
| 		dc.DrawStringAnchored(line, x, y, ax, ay)
 | |
| 		y += dc.fontHeight * lineSpacing
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // MeasureString returns the rendered width and height of the specified text
 | |
| // given the current font face.
 | |
| func (dc *Context) MeasureString(s string) (w, h float64) {
 | |
| 	d := &font.Drawer{
 | |
| 		Face: dc.fontFace,
 | |
| 	}
 | |
| 	a := d.MeasureString(s)
 | |
| 	return float64(a >> 6), dc.fontHeight
 | |
| }
 | |
| 
 | |
| // WordWrap wraps the specified string to the given max width and current
 | |
| // font face.
 | |
| func (dc *Context) WordWrap(s string, w float64) []string {
 | |
| 	return wordWrap(dc, s, w)
 | |
| }
 | |
| 
 | |
| // Transformation Matrix Operations
 | |
| 
 | |
| // Identity resets the current transformation matrix to the identity matrix.
 | |
| // This results in no translating, scaling, rotating, or shearing.
 | |
| func (dc *Context) Identity() {
 | |
| 	dc.matrix = Identity()
 | |
| }
 | |
| 
 | |
| // Translate updates the current matrix with a translation.
 | |
| func (dc *Context) Translate(x, y float64) {
 | |
| 	dc.matrix = dc.matrix.Translate(x, y)
 | |
| }
 | |
| 
 | |
| // Scale updates the current matrix with a scaling factor.
 | |
| // Scaling occurs about the origin.
 | |
| func (dc *Context) Scale(x, y float64) {
 | |
| 	dc.matrix = dc.matrix.Scale(x, y)
 | |
| }
 | |
| 
 | |
| // ScaleAbout updates the current matrix with a scaling factor.
 | |
| // Scaling occurs about the specified point.
 | |
| func (dc *Context) ScaleAbout(sx, sy, x, y float64) {
 | |
| 	dc.Translate(x, y)
 | |
| 	dc.Scale(sx, sy)
 | |
| 	dc.Translate(-x, -y)
 | |
| }
 | |
| 
 | |
| // Rotate updates the current matrix with a clockwise rotation.
 | |
| // Rotation occurs about the origin. Angle is specified in radians.
 | |
| func (dc *Context) Rotate(angle float64) {
 | |
| 	dc.matrix = dc.matrix.Rotate(angle)
 | |
| }
 | |
| 
 | |
| // RotateAbout updates the current matrix with a clockwise rotation.
 | |
| // Rotation occurs about the specified point. Angle is specified in radians.
 | |
| func (dc *Context) RotateAbout(angle, x, y float64) {
 | |
| 	dc.Translate(x, y)
 | |
| 	dc.Rotate(angle)
 | |
| 	dc.Translate(-x, -y)
 | |
| }
 | |
| 
 | |
| // Shear updates the current matrix with a shearing angle.
 | |
| // Shearing occurs about the origin.
 | |
| func (dc *Context) Shear(x, y float64) {
 | |
| 	dc.matrix = dc.matrix.Shear(x, y)
 | |
| }
 | |
| 
 | |
| // ShearAbout updates the current matrix with a shearing angle.
 | |
| // Shearing occurs about the specified point.
 | |
| func (dc *Context) ShearAbout(sx, sy, x, y float64) {
 | |
| 	dc.Translate(x, y)
 | |
| 	dc.Shear(sx, sy)
 | |
| 	dc.Translate(-x, -y)
 | |
| }
 | |
| 
 | |
| // TransformPoint multiplies the specified point by the current matrix,
 | |
| // returning a transformed position.
 | |
| func (dc *Context) TransformPoint(x, y float64) (tx, ty float64) {
 | |
| 	return dc.matrix.TransformPoint(x, y)
 | |
| }
 | |
| 
 | |
| // InvertY flips the Y axis so that Y grows from bottom to top and Y=0 is at
 | |
| // the bottom of the image.
 | |
| func (dc *Context) InvertY() {
 | |
| 	dc.Translate(0, float64(dc.height))
 | |
| 	dc.Scale(1, -1)
 | |
| }
 | |
| 
 | |
| // Stack
 | |
| 
 | |
| // Push saves the current state of the context for later retrieval. These
 | |
| // can be nested.
 | |
| func (dc *Context) Push() {
 | |
| 	x := *dc
 | |
| 	dc.stack = append(dc.stack, &x)
 | |
| }
 | |
| 
 | |
| // Pop restores the last saved context state from the stack.
 | |
| func (dc *Context) Pop() {
 | |
| 	before := *dc
 | |
| 	s := dc.stack
 | |
| 	x, s := s[len(s)-1], s[:len(s)-1]
 | |
| 	*dc = *x
 | |
| 	dc.mask = before.mask
 | |
| 	dc.strokePath = before.strokePath
 | |
| 	dc.fillPath = before.fillPath
 | |
| 	dc.start = before.start
 | |
| 	dc.current = before.current
 | |
| 	dc.hasCurrent = before.hasCurrent
 | |
| }
 | 
