removing 3rd party deps.

This commit is contained in:
Will Charczuk 2016-07-08 17:57:14 -07:00
parent 7f36c08fbf
commit 8bc8b1087c
27 changed files with 1995 additions and 54 deletions

View File

@ -15,6 +15,7 @@ type Chart struct {
Width int Width int
Height int Height int
DPI float64
Background Style Background Style
Canvas Style Canvas Style
@ -28,6 +29,17 @@ type Chart struct {
Series []Series Series []Series
} }
// GetDPI returns the dpi for the chart.
func (c Chart) GetDPI(defaults ...float64) float64 {
if c.DPI == 0 {
if len(defaults) > 0 {
return defaults[0]
}
return DefaultDPI
}
return c.DPI
}
// GetFont returns the text font. // GetFont returns the text font.
func (c Chart) GetFont() (*truetype.Font, error) { func (c Chart) GetFont() (*truetype.Font, error) {
if c.Font == nil { if c.Font == nil {
@ -35,7 +47,7 @@ func (c Chart) GetFont() (*truetype.Font, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
c.Font = f return f, nil
} }
return c.Font, nil return c.Font, nil
} }
@ -45,7 +57,10 @@ func (c *Chart) Render(provider RendererProvider, w io.Writer) error {
if len(c.Series) == 0 { if len(c.Series) == 0 {
return errors.New("Please provide at least one series") return errors.New("Please provide at least one series")
} }
r := provider(c.Width, c.Height) r, err := provider(c.Width, c.Height)
if err != nil {
return err
}
if c.hasText() { if c.hasText() {
font, err := c.GetFont() font, err := c.GetFont()
if err != nil { if err != nil {
@ -53,6 +68,7 @@ func (c *Chart) Render(provider RendererProvider, w io.Writer) error {
} }
r.SetFont(font) r.SetFont(font)
} }
r.SetDPI(c.GetDPI(DefaultDPI))
canvasBox := c.calculateCanvasBox(r) canvasBox := c.calculateCanvasBox(r)
xrange, yrange := c.initRanges(canvasBox) xrange, yrange := c.initRanges(canvasBox)
@ -122,7 +138,7 @@ func (c Chart) calculateFinalLabelWidth(r Renderer) int {
} }
r.SetFontSize(c.FinalValueLabel.GetFontSize(DefaultFinalLabelFontSize)) r.SetFontSize(c.FinalValueLabel.GetFontSize(DefaultFinalLabelFontSize))
textWidth := r.MeasureText(finalLabelText) textWidth, _ := r.MeasureText(finalLabelText)
asw := c.getAxisWidth() asw := c.getAxisWidth()
pl := c.FinalValueLabel.Padding.GetLeft(DefaultFinalLabelPadding.Left) pl := c.FinalValueLabel.Padding.GetLeft(DefaultFinalLabelPadding.Left)
@ -355,7 +371,7 @@ func (c Chart) drawFinalValueLabel(r Renderer, canvasBox Box, index int, s Serie
ly := yrange.Translate(lv) + py ly := yrange.Translate(lv) + py
r.SetFontSize(c.FinalValueLabel.GetFontSize(DefaultFinalLabelFontSize)) r.SetFontSize(c.FinalValueLabel.GetFontSize(DefaultFinalLabelFontSize))
textWidth := r.MeasureText(ll) textWidth, _ := r.MeasureText(ll)
textHeight := int(math.Floor(DefaultFinalLabelFontSize)) textHeight := int(math.Floor(DefaultFinalLabelFontSize))
halfTextHeight := textHeight >> 1 halfTextHeight := textHeight >> 1
@ -409,7 +425,7 @@ func (c Chart) drawTitle(r Renderer) error {
r.SetFontColor(c.Canvas.GetFontColor(DefaultTextColor)) r.SetFontColor(c.Canvas.GetFontColor(DefaultTextColor))
titleFontSize := c.Canvas.GetFontSize(DefaultTitleFontSize) titleFontSize := c.Canvas.GetFontSize(DefaultTitleFontSize)
r.SetFontSize(titleFontSize) r.SetFontSize(titleFontSize)
textWidth := r.MeasureText(c.Title) textWidth, _ := r.MeasureText(c.Title)
titleX := (c.Width >> 1) - (textWidth >> 1) titleX := (c.Width >> 1) - (textWidth >> 1)
titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + int(titleFontSize) titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + int(titleFontSize)
r.Text(c.Title, titleX, titleY) r.Text(c.Title, titleX, titleY)

View File

@ -17,7 +17,7 @@ const (
// DefaultAxisLineWidth is the line width of the axis lines. // DefaultAxisLineWidth is the line width of the axis lines.
DefaultAxisLineWidth = 1.0 DefaultAxisLineWidth = 1.0
//DefaultDPI is the default dots per inch for the chart. //DefaultDPI is the default dots per inch for the chart.
DefaultDPI = 120.0 DefaultDPI = 92.0
// DefaultMinimumFontSize is the default minimum font size. // DefaultMinimumFontSize is the default minimum font size.
DefaultMinimumFontSize = 8.0 DefaultMinimumFontSize = 8.0
// DefaultFontSize is the default font size. // DefaultFontSize is the default font size.

9
drawing/constants.go Normal file
View File

@ -0,0 +1,9 @@
package drawing
const (
// DefaultDPI is the default image DPI.
DefaultDPI = 96.0
// EMRatio is the ratio of something to something else.
EMRatio = 64.0 / 72.0
)

158
drawing/curve.go Normal file
View File

@ -0,0 +1,158 @@
package drawing
import (
"math"
)
const (
// CurveRecursionLimit represents the maximum recursion that is really necessary to subsivide a curve into straight lines
CurveRecursionLimit = 32
)
// Cubic
// x1, y1, cpx1, cpy1, cpx2, cpy2, x2, y2 float64
// SubdivideCubic a Bezier cubic curve in 2 equivalents Bezier cubic curves.
// c1 and c2 parameters are the resulting curves
func SubdivideCubic(c, c1, c2 []float64) {
// First point of c is the first point of c1
c1[0], c1[1] = c[0], c[1]
// Last point of c is the last point of c2
c2[6], c2[7] = c[6], c[7]
// Subdivide segment using midpoints
c1[2] = (c[0] + c[2]) / 2
c1[3] = (c[1] + c[3]) / 2
midX := (c[2] + c[4]) / 2
midY := (c[3] + c[5]) / 2
c2[4] = (c[4] + c[6]) / 2
c2[5] = (c[5] + c[7]) / 2
c1[4] = (c1[2] + midX) / 2
c1[5] = (c1[3] + midY) / 2
c2[2] = (midX + c2[4]) / 2
c2[3] = (midY + c2[5]) / 2
c1[6] = (c1[4] + c2[2]) / 2
c1[7] = (c1[5] + c2[3]) / 2
// Last Point of c1 is equal to the first point of c2
c2[0], c2[1] = c1[6], c1[7]
}
// TraceCubic generate lines subdividing the cubic curve using a Liner
// flattening_threshold helps determines the flattening expectation of the curve
func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) {
// Allocation curves
var curves [CurveRecursionLimit * 8]float64
copy(curves[0:8], cubic[0:8])
i := 0
// current curve
var c []float64
var dx, dy, d2, d3 float64
for i >= 0 {
c = curves[i*8:]
dx = c[6] - c[0]
dy = c[7] - c[1]
d2 = math.Abs((c[2]-c[6])*dy - (c[3]-c[7])*dx)
d3 = math.Abs((c[4]-c[6])*dy - (c[5]-c[7])*dx)
// if it's flat then trace a line
if (d2+d3)*(d2+d3) < flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-1 {
t.LineTo(c[6], c[7])
i--
} else {
// second half of bezier go lower onto the stack
SubdivideCubic(c, curves[(i+1)*8:], curves[i*8:])
i++
}
}
}
// Quad
// x1, y1, cpx1, cpy2, x2, y2 float64
// SubdivideQuad a Bezier quad curve in 2 equivalents Bezier quad curves.
// c1 and c2 parameters are the resulting curves
func SubdivideQuad(c, c1, c2 []float64) {
// First point of c is the first point of c1
c1[0], c1[1] = c[0], c[1]
// Last point of c is the last point of c2
c2[4], c2[5] = c[4], c[5]
// Subdivide segment using midpoints
c1[2] = (c[0] + c[2]) / 2
c1[3] = (c[1] + c[3]) / 2
c2[2] = (c[2] + c[4]) / 2
c2[3] = (c[3] + c[5]) / 2
c1[4] = (c1[2] + c2[2]) / 2
c1[5] = (c1[3] + c2[3]) / 2
c2[0], c2[1] = c1[4], c1[5]
return
}
// TraceQuad generate lines subdividing the curve using a Liner
// flattening_threshold helps determines the flattening expectation of the curve
func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) {
// Allocates curves stack
var curves [CurveRecursionLimit * 6]float64
copy(curves[0:6], quad[0:6])
i := 0
// current curve
var c []float64
var dx, dy, d float64
for i >= 0 {
c = curves[i*6:]
dx = c[4] - c[0]
dy = c[5] - c[1]
d = math.Abs(((c[2]-c[4])*dy - (c[3]-c[5])*dx))
// if it's flat then trace a line
if (d*d) < flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-1 {
t.LineTo(c[4], c[5])
i--
} else {
// second half of bezier go lower onto the stack
SubdivideQuad(c, curves[(i+1)*6:], curves[i*6:])
i++
}
}
}
// TraceArc trace an arc using a Liner
func TraceArc(t Liner, x, y, rx, ry, start, angle, scale float64) (lastX, lastY float64) {
end := start + angle
clockWise := true
if angle < 0 {
clockWise = false
}
ra := (math.Abs(rx) + math.Abs(ry)) / 2
da := math.Acos(ra/(ra+0.125/scale)) * 2
//normalize
if !clockWise {
da = -da
}
angle = start + da
var curX, curY float64
for {
if (angle < end-da/4) != clockWise {
curX = x + math.Cos(end)*rx
curY = y + math.Sin(end)*ry
return curX, curY
}
curX = x + math.Cos(angle)*rx
curY = y + math.Sin(angle)*ry
angle += da
t.LineTo(curX, curY)
}
}

89
drawing/dasher.go Normal file
View File

@ -0,0 +1,89 @@
package drawing
// NewDashVertexConverter creates a new dash converter.
func NewDashVertexConverter(dash []float64, dashOffset float64, flattener Flattener) *DashVertexConverter {
var dasher DashVertexConverter
dasher.dash = dash
dasher.currentDash = 0
dasher.dashOffset = dashOffset
dasher.next = flattener
return &dasher
}
// DashVertexConverter is a converter for dash vertexes.
type DashVertexConverter struct {
next Flattener
x, y, distance float64
dash []float64
currentDash int
dashOffset float64
}
// LineTo implements the pathbuilder interface.
func (dasher *DashVertexConverter) LineTo(x, y float64) {
dasher.lineTo(x, y)
}
// MoveTo implements the pathbuilder interface.
func (dasher *DashVertexConverter) MoveTo(x, y float64) {
dasher.next.MoveTo(x, y)
dasher.x, dasher.y = x, y
dasher.distance = dasher.dashOffset
dasher.currentDash = 0
}
// LineJoin implements the pathbuilder interface.
func (dasher *DashVertexConverter) LineJoin() {
dasher.next.LineJoin()
}
// Close implements the pathbuilder interface.
func (dasher *DashVertexConverter) Close() {
dasher.next.Close()
}
// End implements the pathbuilder interface.
func (dasher *DashVertexConverter) End() {
dasher.next.End()
}
func (dasher *DashVertexConverter) lineTo(x, y float64) {
rest := dasher.dash[dasher.currentDash] - dasher.distance
for rest < 0 {
dasher.distance = dasher.distance - dasher.dash[dasher.currentDash]
dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash)
rest = dasher.dash[dasher.currentDash] - dasher.distance
}
d := distance(dasher.x, dasher.y, x, y)
for d >= rest {
k := rest / d
lx := dasher.x + k*(x-dasher.x)
ly := dasher.y + k*(y-dasher.y)
if dasher.currentDash%2 == 0 {
// line
dasher.next.LineTo(lx, ly)
} else {
// gap
dasher.next.End()
dasher.next.MoveTo(lx, ly)
}
d = d - rest
dasher.x, dasher.y = lx, ly
dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash)
rest = dasher.dash[dasher.currentDash]
}
dasher.distance = d
if dasher.currentDash%2 == 0 {
// line
dasher.next.LineTo(x, y)
} else {
// gap
dasher.next.End()
dasher.next.MoveTo(x, y)
}
if dasher.distance >= dasher.dash[dasher.currentDash] {
dasher.distance = dasher.distance - dasher.dash[dasher.currentDash]
dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash)
}
dasher.x, dasher.y = x, y
}

View File

@ -0,0 +1,41 @@
package drawing
// DemuxFlattener is a flattener
type DemuxFlattener struct {
Flatteners []Flattener
}
// MoveTo implements the path builder interface.
func (dc DemuxFlattener) MoveTo(x, y float64) {
for _, flattener := range dc.Flatteners {
flattener.MoveTo(x, y)
}
}
// LineTo implements the path builder interface.
func (dc DemuxFlattener) LineTo(x, y float64) {
for _, flattener := range dc.Flatteners {
flattener.LineTo(x, y)
}
}
// LineJoin implements the path builder interface.
func (dc DemuxFlattener) LineJoin() {
for _, flattener := range dc.Flatteners {
flattener.LineJoin()
}
}
// Close implements the path builder interface.
func (dc DemuxFlattener) Close() {
for _, flattener := range dc.Flatteners {
flattener.Close()
}
}
// End implements the path builder interface.
func (dc DemuxFlattener) End() {
for _, flattener := range dc.Flatteners {
flattener.End()
}
}

148
drawing/drawing.go Normal file
View File

@ -0,0 +1,148 @@
package drawing
import (
"image/color"
"github.com/golang/freetype/truetype"
)
// FillRule defines the type for fill rules
type FillRule int
const (
// FillRuleEvenOdd determines the "insideness" of a point in the shape
// by drawing a ray from that point to infinity in any direction
// and counting the number of path segments from the given shape that the ray crosses.
// If this number is odd, the point is inside; if even, the point is outside.
FillRuleEvenOdd FillRule = iota
// FillRuleWinding determines the "insideness" of a point in the shape
// by drawing a ray from that point to infinity in any direction
// and then examining the places where a segment of the shape crosses the ray.
// Starting with a count of zero, add one each time a path segment crosses
// the ray from left to right and subtract one each time
// a path segment crosses the ray from right to left. After counting the crossings,
// if the result is zero then the point is outside the path. Otherwise, it is inside.
FillRuleWinding
)
// LineCap is the style of line extremities
type LineCap int
const (
// RoundCap defines a rounded shape at the end of the line
RoundCap LineCap = iota
// ButtCap defines a squared shape exactly at the end of the line
ButtCap
// SquareCap defines a squared shape at the end of the line
SquareCap
)
// LineJoin is the style of segments joint
type LineJoin int
const (
// BevelJoin represents cut segments joint
BevelJoin LineJoin = iota
// RoundJoin represents rounded segments joint
RoundJoin
// MiterJoin represents peaker segments joint
MiterJoin
)
// StrokeStyle keeps stroke style attributes
// that is used by the Stroke method of a Drawer
type StrokeStyle struct {
// Color defines the color of stroke
Color color.Color
// Line width
Width float64
// Line cap style rounded, butt or square
LineCap LineCap
// Line join style bevel, round or miter
LineJoin LineJoin
// offset of the first dash
DashOffset float64
// array represented dash length pair values are plain dash and impair are space between dash
// if empty display plain line
Dash []float64
}
// SolidFillStyle define style attributes for a solid fill style
type SolidFillStyle struct {
// Color defines the line color
Color color.Color
// FillRule defines the file rule to used
FillRule FillRule
}
// Valign Vertical Alignment of the text
type Valign int
const (
// ValignTop top align text
ValignTop Valign = iota
// ValignCenter centered text
ValignCenter
// ValignBottom bottom aligned text
ValignBottom
// ValignBaseline align text with the baseline of the font
ValignBaseline
)
// Halign Horizontal Alignment of the text
type Halign int
const (
// HalignLeft Horizontally align to left
HalignLeft = iota
// HalignCenter Horizontally align to center
HalignCenter
// HalignRight Horizontally align to right
HalignRight
)
// TextStyle describe text property
type TextStyle struct {
// Color defines the color of text
Color color.Color
// Size font size
Size float64
// The font to use
Font *truetype.Font
// Horizontal Alignment of the text
Halign Halign
// Vertical Alignment of the text
Valign Valign
}
// ScalingPolicy is a constant to define how to scale an image
type ScalingPolicy int
const (
// ScalingNone no scaling applied
ScalingNone ScalingPolicy = iota
// ScalingStretch the image is stretched so that its width and height are exactly the given width and height
ScalingStretch
// ScalingWidth the image is scaled so that its width is exactly the given width
ScalingWidth
// ScalingHeight the image is scaled so that its height is exactly the given height
ScalingHeight
// ScalingFit the image is scaled to the largest scale that allow the image to fit within a rectangle width x height
ScalingFit
// ScalingSameArea the image is scaled so that its area is exactly the area of the given rectangle width x height
ScalingSameArea
// ScalingFill the image is scaled to the smallest scale that allow the image to fully cover a rectangle width x height
ScalingFill
)
// ImageScaling style attributes used to display the image
type ImageScaling struct {
// Horizontal Alignment of the image
Halign Halign
// Vertical Alignment of the image
Valign Valign
// Width Height used by scaling policy
Width, Height float64
// ScalingPolicy defines the scaling policy to applied to the image
ScalingPolicy ScalingPolicy
}

90
drawing/flattener.go Normal file
View File

@ -0,0 +1,90 @@
package drawing
// Liner receive segment definition
type Liner interface {
// LineTo Draw a line from the current position to the point (x, y)
LineTo(x, y float64)
}
// Flattener receive segment definition
type Flattener interface {
// MoveTo Start a New line from the point (x, y)
MoveTo(x, y float64)
// LineTo Draw a line from the current position to the point (x, y)
LineTo(x, y float64)
// LineJoin add the most recent starting point to close the path to create a polygon
LineJoin()
// Close add the most recent starting point to close the path to create a polygon
Close()
// End mark the current line as finished so we can draw caps
End()
}
// Flatten convert curves into straight segments keeping join segments info
func Flatten(path *Path, flattener Flattener, scale float64) {
// First Point
var startX, startY float64 = 0, 0
// Current Point
var x, y float64 = 0, 0
i := 0
for _, cmp := range path.Components {
switch cmp {
case MoveToComponent:
x, y = path.Points[i], path.Points[i+1]
startX, startY = x, y
if i != 0 {
flattener.End()
}
flattener.MoveTo(x, y)
i += 2
case LineToComponent:
x, y = path.Points[i], path.Points[i+1]
flattener.LineTo(x, y)
flattener.LineJoin()
i += 2
case QuadCurveToComponent:
TraceQuad(flattener, path.Points[i-2:], 0.5)
x, y = path.Points[i+2], path.Points[i+3]
flattener.LineTo(x, y)
i += 4
case CubicCurveToComponent:
TraceCubic(flattener, path.Points[i-2:], 0.5)
x, y = path.Points[i+4], path.Points[i+5]
flattener.LineTo(x, y)
i += 6
case ArcToComponent:
x, y = TraceArc(flattener, path.Points[i], path.Points[i+1], path.Points[i+2], path.Points[i+3], path.Points[i+4], path.Points[i+5], scale)
flattener.LineTo(x, y)
i += 6
case CloseComponent:
flattener.LineTo(startX, startY)
flattener.Close()
}
}
flattener.End()
}
type SegmentedPath struct {
Points []float64
}
func (p *SegmentedPath) MoveTo(x, y float64) {
p.Points = append(p.Points, x, y)
// TODO need to mark this point as moveto
}
func (p *SegmentedPath) LineTo(x, y float64) {
p.Points = append(p.Points, x, y)
}
func (p *SegmentedPath) LineJoin() {
// TODO need to mark the current point as linejoin
}
func (p *SegmentedPath) Close() {
// TODO Close
}
func (p *SegmentedPath) End() {
// Nothing to do
}

30
drawing/free_type_path.go Normal file
View File

@ -0,0 +1,30 @@
package drawing
import (
"github.com/golang/freetype/raster"
"golang.org/x/image/math/fixed"
)
// FtLineBuilder is a builder for freetype raster glyphs.
type FtLineBuilder struct {
Adder raster.Adder
}
// MoveTo implements the path builder interface.
func (liner FtLineBuilder) MoveTo(x, y float64) {
liner.Adder.Start(fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)})
}
// LineTo implements the path builder interface.
func (liner FtLineBuilder) LineTo(x, y float64) {
liner.Adder.Add1(fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)})
}
// LineJoin implements the path builder interface.
func (liner FtLineBuilder) LineJoin() {}
// Close implements the path builder interface.
func (liner FtLineBuilder) Close() {}
// End implements the path builder interface.
func (liner FtLineBuilder) End() {}

View File

@ -0,0 +1,82 @@
package drawing
import (
"image"
"image/color"
"github.com/golang/freetype/truetype"
)
// GraphicContext describes the interface for the various backends (images, pdf, opengl, ...)
type GraphicContext interface {
// PathBuilder describes the interface for path drawing
PathBuilder
// BeginPath creates a new path
BeginPath()
// GetMatrixTransform returns the current transformation matrix
GetMatrixTransform() Matrix
// SetMatrixTransform sets the current transformation matrix
SetMatrixTransform(tr Matrix)
// ComposeMatrixTransform composes the current transformation matrix with tr
ComposeMatrixTransform(tr Matrix)
// Rotate applies a rotation to the current transformation matrix. angle is in radian.
Rotate(angle float64)
// Translate applies a translation to the current transformation matrix.
Translate(tx, ty float64)
// Scale applies a scale to the current transformation matrix.
Scale(sx, sy float64)
// SetStrokeColor sets the current stroke color
SetStrokeColor(c color.Color)
// SetFillColor sets the current fill color
SetFillColor(c color.Color)
// SetFillRule sets the current fill rule
SetFillRule(f FillRule)
// SetLineWidth sets the current line width
SetLineWidth(lineWidth float64)
// SetLineCap sets the current line cap
SetLineCap(cap LineCap)
// SetLineJoin sets the current line join
SetLineJoin(join LineJoin)
// SetLineDash sets the current dash
SetLineDash(dash []float64, dashOffset float64)
// SetFontSize sets the current font size
SetFontSize(fontSize float64)
// GetFontSize gets the current font size
GetFontSize() float64
// SetFont sets the font for the context
SetFont(f *truetype.Font)
// GetFont returns the current font
GetFont() *truetype.Font
// DrawImage draws the raster image in the current canvas
DrawImage(image image.Image)
// Save the context and push it to the context stack
Save()
// Restore remove the current context and restore the last one
Restore()
// Clear fills the current canvas with a default transparent color
Clear()
// ClearRect fills the specified rectangle with a default transparent color
ClearRect(x1, y1, x2, y2 int)
// SetDPI sets the current DPI
SetDPI(dpi int)
// GetDPI gets the current DPI
GetDPI() int
// GetStringBounds gets pixel bounds(dimensions) of given string
GetStringBounds(s string) (left, top, right, bottom float64)
// CreateStringPath creates a path from the string s at x, y
CreateStringPath(text string, x, y float64) (cursor float64)
// FillString draws the text at point (0, 0)
FillString(text string) (cursor float64)
// FillStringAt draws the text at the specified point (x, y)
FillStringAt(text string, x, y float64) (cursor float64)
// StrokeString draws the contour of the text at point (0, 0)
StrokeString(text string) (cursor float64)
// StrokeStringAt draws the contour of the text at point (x, y)
StrokeStringAt(text string, x, y float64) (cursor float64)
// Stroke strokes the paths with the color specified by SetStrokeColor
Stroke(paths ...*Path)
// Fill fills the paths with the color specified by SetFillColor
Fill(paths ...*Path)
// FillStroke first fills the paths and than strokes them
FillStroke(paths ...*Path)
}

13
drawing/image_filter.go Normal file
View File

@ -0,0 +1,13 @@
package drawing
// ImageFilter defines the type of filter to use
type ImageFilter int
const (
// LinearFilter defines a linear filter
LinearFilter ImageFilter = iota
// BilinearFilter defines a bilinear filter
BilinearFilter
// BicubicFilter defines a bicubic filter
BicubicFilter
)

48
drawing/line.go Normal file
View File

@ -0,0 +1,48 @@
package drawing
import (
"image/color"
"image/draw"
)
// PolylineBresenham draws a polyline to an image
func PolylineBresenham(img draw.Image, c color.Color, s ...float64) {
for i := 2; i < len(s); i += 2 {
Bresenham(img, c, int(s[i-2]+0.5), int(s[i-1]+0.5), int(s[i]+0.5), int(s[i+1]+0.5))
}
}
// Bresenham draws a line between (x0, y0) and (x1, y1)
func Bresenham(img draw.Image, color color.Color, x0, y0, x1, y1 int) {
dx := abs(x1 - x0)
dy := abs(y1 - y0)
var sx, sy int
if x0 < x1 {
sx = 1
} else {
sx = -1
}
if y0 < y1 {
sy = 1
} else {
sy = -1
}
err := dx - dy
var e2 int
for {
img.Set(x0, y0, color)
if x0 == x1 && y0 == y1 {
return
}
e2 = 2 * err
if e2 > -dy {
err = err - dy
x0 = x0 + sx
}
if e2 < dx {
err = err + dx
y0 = y0 + sy
}
}
}

219
drawing/matrix.go Normal file
View File

@ -0,0 +1,219 @@
package drawing
import (
"math"
)
// Matrix represents an affine transformation
type Matrix [6]float64
const (
epsilon = 1e-6
)
// Determinant compute the determinant of the matrix
func (tr Matrix) Determinant() float64 {
return tr[0]*tr[3] - tr[1]*tr[2]
}
// Transform applies the transformation matrix to points. It modify the points passed in parameter.
func (tr Matrix) Transform(points []float64) {
for i, j := 0, 1; j < len(points); i, j = i+2, j+2 {
x := points[i]
y := points[j]
points[i] = x*tr[0] + y*tr[2] + tr[4]
points[j] = x*tr[1] + y*tr[3] + tr[5]
}
}
// TransformPoint applies the transformation matrix to point. It returns the point the transformed point.
func (tr Matrix) TransformPoint(x, y float64) (xres, yres float64) {
xres = x*tr[0] + y*tr[2] + tr[4]
yres = x*tr[1] + y*tr[3] + tr[5]
return xres, yres
}
func minMax(x, y float64) (min, max float64) {
if x > y {
return y, x
}
return x, y
}
// Transform applies the transformation matrix to the rectangle represented by the min and the max point of the rectangle
func (tr Matrix) TransformRectangle(x0, y0, x2, y2 float64) (nx0, ny0, nx2, ny2 float64) {
points := []float64{x0, y0, x2, y0, x2, y2, x0, y2}
tr.Transform(points)
points[0], points[2] = minMax(points[0], points[2])
points[4], points[6] = minMax(points[4], points[6])
points[1], points[3] = minMax(points[1], points[3])
points[5], points[7] = minMax(points[5], points[7])
nx0 = math.Min(points[0], points[4])
ny0 = math.Min(points[1], points[5])
nx2 = math.Max(points[2], points[6])
ny2 = math.Max(points[3], points[7])
return nx0, ny0, nx2, ny2
}
// InverseTransform applies the transformation inverse matrix to the rectangle represented by the min and the max point of the rectangle
func (tr Matrix) InverseTransform(points []float64) {
d := tr.Determinant() // matrix determinant
for i, j := 0, 1; j < len(points); i, j = i+2, j+2 {
x := points[i]
y := points[j]
points[i] = ((x-tr[4])*tr[3] - (y-tr[5])*tr[2]) / d
points[j] = ((y-tr[5])*tr[0] - (x-tr[4])*tr[1]) / d
}
}
// InverseTransformPoint applies the transformation inverse matrix to point. It returns the point the transformed point.
func (tr Matrix) InverseTransformPoint(x, y float64) (xres, yres float64) {
d := tr.Determinant() // matrix determinant
xres = ((x-tr[4])*tr[3] - (y-tr[5])*tr[2]) / d
yres = ((y-tr[5])*tr[0] - (x-tr[4])*tr[1]) / d
return xres, yres
}
// VectorTransform applies the transformation matrix to points without using the translation parameter of the affine matrix.
// It modify the points passed in parameter.
func (tr Matrix) VectorTransform(points []float64) {
for i, j := 0, 1; j < len(points); i, j = i+2, j+2 {
x := points[i]
y := points[j]
points[i] = x*tr[0] + y*tr[2]
points[j] = x*tr[1] + y*tr[3]
}
}
// NewIdentityMatrix creates an identity transformation matrix.
func NewIdentityMatrix() Matrix {
return Matrix{1, 0, 0, 1, 0, 0}
}
// NewTranslationMatrix creates a transformation matrix with a translation tx and ty translation parameter
func NewTranslationMatrix(tx, ty float64) Matrix {
return Matrix{1, 0, 0, 1, tx, ty}
}
// NewScaleMatrix creates a transformation matrix with a sx, sy scale factor
func NewScaleMatrix(sx, sy float64) Matrix {
return Matrix{sx, 0, 0, sy, 0, 0}
}
// NewRotationMatrix creates a rotation transformation matrix. angle is in radian
func NewRotationMatrix(angle float64) Matrix {
c := math.Cos(angle)
s := math.Sin(angle)
return Matrix{c, s, -s, c, 0, 0}
}
// NewMatrixFromRects creates a transformation matrix, combining a scale and a translation, that transform rectangle1 into rectangle2.
func NewMatrixFromRects(rectangle1, rectangle2 [4]float64) Matrix {
xScale := (rectangle2[2] - rectangle2[0]) / (rectangle1[2] - rectangle1[0])
yScale := (rectangle2[3] - rectangle2[1]) / (rectangle1[3] - rectangle1[1])
xOffset := rectangle2[0] - (rectangle1[0] * xScale)
yOffset := rectangle2[1] - (rectangle1[1] * yScale)
return Matrix{xScale, 0, 0, yScale, xOffset, yOffset}
}
// Inverse computes the inverse matrix
func (tr *Matrix) Inverse() {
d := tr.Determinant() // matrix determinant
tr0, tr1, tr2, tr3, tr4, tr5 := tr[0], tr[1], tr[2], tr[3], tr[4], tr[5]
tr[0] = tr3 / d
tr[1] = -tr1 / d
tr[2] = -tr2 / d
tr[3] = tr0 / d
tr[4] = (tr2*tr5 - tr3*tr4) / d
tr[5] = (tr1*tr4 - tr0*tr5) / d
}
func (tr Matrix) Copy() Matrix {
var result Matrix
copy(result[:], tr[:])
return result
}
// Compose multiplies trToConcat x tr
func (tr *Matrix) Compose(trToCompose Matrix) {
tr0, tr1, tr2, tr3, tr4, tr5 := tr[0], tr[1], tr[2], tr[3], tr[4], tr[5]
tr[0] = trToCompose[0]*tr0 + trToCompose[1]*tr2
tr[1] = trToCompose[1]*tr3 + trToCompose[0]*tr1
tr[2] = trToCompose[2]*tr0 + trToCompose[3]*tr2
tr[3] = trToCompose[3]*tr3 + trToCompose[2]*tr1
tr[4] = trToCompose[4]*tr0 + trToCompose[5]*tr2 + tr4
tr[5] = trToCompose[5]*tr3 + trToCompose[4]*tr1 + tr5
}
// Scale adds a scale to the matrix
func (tr *Matrix) Scale(sx, sy float64) {
tr[0] = sx * tr[0]
tr[1] = sx * tr[1]
tr[2] = sy * tr[2]
tr[3] = sy * tr[3]
}
// Translate adds a translation to the matrix
func (tr *Matrix) Translate(tx, ty float64) {
tr[4] = tx*tr[0] + ty*tr[2] + tr[4]
tr[5] = ty*tr[3] + tx*tr[1] + tr[5]
}
// Rotate adds a rotation to the matrix. angle is in radian
func (tr *Matrix) Rotate(angle float64) {
c := math.Cos(angle)
s := math.Sin(angle)
t0 := c*tr[0] + s*tr[2]
t1 := s*tr[3] + c*tr[1]
t2 := c*tr[2] - s*tr[0]
t3 := c*tr[3] - s*tr[1]
tr[0] = t0
tr[1] = t1
tr[2] = t2
tr[3] = t3
}
// GetTranslation
func (tr Matrix) GetTranslation() (x, y float64) {
return tr[4], tr[5]
}
// GetScaling
func (tr Matrix) GetScaling() (x, y float64) {
return tr[0], tr[3]
}
// GetScale computes a scale for the matrix
func (tr Matrix) GetScale() float64 {
x := 0.707106781*tr[0] + 0.707106781*tr[1]
y := 0.707106781*tr[2] + 0.707106781*tr[3]
return math.Sqrt(x*x + y*y)
}
// ******************** Testing ********************
// Equals tests if a two transformation are equal. A tolerance is applied when comparing matrix elements.
func (tr1 Matrix) Equals(tr2 Matrix) bool {
for i := 0; i < 6; i = i + 1 {
if !fequals(tr1[i], tr2[i]) {
return false
}
}
return true
}
// IsIdentity tests if a transformation is the identity transformation. A tolerance is applied when comparing matrix elements.
func (tr Matrix) IsIdentity() bool {
return fequals(tr[4], 0) && fequals(tr[5], 0) && tr.IsTranslation()
}
// IsTranslation tests if a transformation is is a pure translation. A tolerance is applied when comparing matrix elements.
func (tr Matrix) IsTranslation() bool {
return fequals(tr[0], 1) && fequals(tr[1], 0) && fequals(tr[2], 0) && fequals(tr[3], 1)
}
// fequals compares two floats. return true if the distance between the two floats is less than epsilon, false otherwise
func fequals(float1, float2 float64) bool {
return math.Abs(float1-float2) <= epsilon
}

31
drawing/painter.go Normal file
View File

@ -0,0 +1,31 @@
package drawing
import (
"image"
"image/color"
"golang.org/x/image/draw"
"golang.org/x/image/math/f64"
"github.com/golang/freetype/raster"
)
// Painter implements the freetype raster.Painter and has a SetColor method like the RGBAPainter
type Painter interface {
raster.Painter
SetColor(color color.Color)
}
// DrawImage draws an image into dest using an affine transformation matrix, an op and a filter
func DrawImage(src image.Image, dest draw.Image, tr Matrix, op draw.Op, filter ImageFilter) {
var transformer draw.Transformer
switch filter {
case LinearFilter:
transformer = draw.NearestNeighbor
case BilinearFilter:
transformer = draw.BiLinear
case BicubicFilter:
transformer = draw.CatmullRom
}
transformer.Transform(dest, f64.Aff3{tr[0], tr[1], tr[4], tr[2], tr[3], tr[5]}, src, src.Bounds(), op, nil)
}

186
drawing/path.go Normal file
View File

@ -0,0 +1,186 @@
package drawing
import (
"fmt"
"math"
)
// PathBuilder describes the interface for path drawing.
type PathBuilder interface {
// LastPoint returns the current point of the current sub path
LastPoint() (x, y float64)
// MoveTo creates a new subpath that start at the specified point
MoveTo(x, y float64)
// LineTo adds a line to the current subpath
LineTo(x, y float64)
// QuadCurveTo adds a quadratic Bézier curve to the current subpath
QuadCurveTo(cx, cy, x, y float64)
// CubicCurveTo adds a cubic Bézier curve to the current subpath
CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64)
// ArcTo adds an arc to the current subpath
ArcTo(cx, cy, rx, ry, startAngle, angle float64)
// Close creates a line from the current point to the last MoveTo
// point (if not the same) and mark the path as closed so the
// first and last lines join nicely.
Close()
}
// PathComponent represents component of a path
type PathComponent int
const (
// MoveToComponent is a MoveTo component in a Path
MoveToComponent PathComponent = iota
// LineToComponent is a LineTo component in a Path
LineToComponent
// QuadCurveToComponent is a QuadCurveTo component in a Path
QuadCurveToComponent
// CubicCurveToComponent is a CubicCurveTo component in a Path
CubicCurveToComponent
// ArcToComponent is a ArcTo component in a Path
ArcToComponent
// CloseComponent is a ArcTo component in a Path
CloseComponent
)
// Path stores points
type Path struct {
// Components is a slice of PathComponent in a Path and mark the role of each points in the Path
Components []PathComponent
// Points are combined with Components to have a specific role in the path
Points []float64
// Last Point of the Path
x, y float64
}
func (p *Path) appendToPath(cmd PathComponent, points ...float64) {
p.Components = append(p.Components, cmd)
p.Points = append(p.Points, points...)
}
// LastPoint returns the current point of the current path
func (p *Path) LastPoint() (x, y float64) {
return p.x, p.y
}
// MoveTo starts a new path at (x, y) position
func (p *Path) MoveTo(x, y float64) {
p.appendToPath(MoveToComponent, x, y)
p.x = x
p.y = y
}
// LineTo adds a line to the current path
func (p *Path) LineTo(x, y float64) {
if len(p.Components) == 0 { //special case when no move has been done
p.MoveTo(0, 0)
}
p.appendToPath(LineToComponent, x, y)
p.x = x
p.y = y
}
// QuadCurveTo adds a quadratic bezier curve to the current path
func (p *Path) QuadCurveTo(cx, cy, x, y float64) {
if len(p.Components) == 0 { //special case when no move has been done
p.MoveTo(0, 0)
}
p.appendToPath(QuadCurveToComponent, cx, cy, x, y)
p.x = x
p.y = y
}
// CubicCurveTo adds a cubic bezier curve to the current path
func (p *Path) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {
if len(p.Components) == 0 { //special case when no move has been done
p.MoveTo(0, 0)
}
p.appendToPath(CubicCurveToComponent, cx1, cy1, cx2, cy2, x, y)
p.x = x
p.y = y
}
// ArcTo adds an arc to the path
func (p *Path) ArcTo(cx, cy, rx, ry, startAngle, angle float64) {
endAngle := startAngle + angle
clockWise := true
if angle < 0 {
clockWise = false
}
// normalize
if clockWise {
for endAngle < startAngle {
endAngle += math.Pi * 2.0
}
} else {
for startAngle < endAngle {
startAngle += math.Pi * 2.0
}
}
startX := cx + math.Cos(startAngle)*rx
startY := cy + math.Sin(startAngle)*ry
if len(p.Components) > 0 {
p.LineTo(startX, startY)
} else {
p.MoveTo(startX, startY)
}
p.appendToPath(ArcToComponent, cx, cy, rx, ry, startAngle, angle)
p.x = cx + math.Cos(endAngle)*rx
p.y = cy + math.Sin(endAngle)*ry
}
// Close closes the current path
func (p *Path) Close() {
p.appendToPath(CloseComponent)
}
// Copy make a clone of the current path and return it
func (p *Path) Copy() (dest *Path) {
dest = new(Path)
dest.Components = make([]PathComponent, len(p.Components))
copy(dest.Components, p.Components)
dest.Points = make([]float64, len(p.Points))
copy(dest.Points, p.Points)
dest.x, dest.y = p.x, p.y
return dest
}
// Clear reset the path
func (p *Path) Clear() {
p.Components = p.Components[0:0]
p.Points = p.Points[0:0]
return
}
// IsEmpty returns true if the path is empty
func (p *Path) IsEmpty() bool {
return len(p.Components) == 0
}
// String returns a debug text view of the path
func (p *Path) String() string {
s := ""
j := 0
for _, cmd := range p.Components {
switch cmd {
case MoveToComponent:
s += fmt.Sprintf("MoveTo: %f, %f\n", p.Points[j], p.Points[j+1])
j = j + 2
case LineToComponent:
s += fmt.Sprintf("LineTo: %f, %f\n", p.Points[j], p.Points[j+1])
j = j + 2
case QuadCurveToComponent:
s += fmt.Sprintf("QuadCurveTo: %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3])
j = j + 4
case CubicCurveToComponent:
s += fmt.Sprintf("CubicCurveTo: %f, %f, %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3], p.Points[j+4], p.Points[j+5])
j = j + 6
case ArcToComponent:
s += fmt.Sprintf("ArcTo: %f, %f, %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3], p.Points[j+4], p.Points[j+5])
j = j + 6
case CloseComponent:
s += "Close\n"
}
}
return s
}

View File

@ -0,0 +1,282 @@
package drawing
import (
"errors"
"image"
"image/color"
"math"
"github.com/golang/freetype/raster"
"github.com/golang/freetype/truetype"
"golang.org/x/image/draw"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
// NewRasterGraphicContext creates a new Graphic context from an image.
func NewRasterGraphicContext(img draw.Image) (*RasterGraphicContext, error) {
var painter Painter
switch selectImage := img.(type) {
case *image.RGBA:
painter = raster.NewRGBAPainter(selectImage)
default:
return nil, errors.New("NewRasterGraphicContext() :: invalid image type")
}
return NewRasterGraphicContextWithPainter(img, painter), nil
}
// NewRasterGraphicContextWithPainter creates a new Graphic context from an image and a Painter (see Freetype-go)
func NewRasterGraphicContextWithPainter(img draw.Image, painter Painter) *RasterGraphicContext {
width, height := img.Bounds().Dx(), img.Bounds().Dy()
return &RasterGraphicContext{
NewStackGraphicContext(),
img,
painter,
raster.NewRasterizer(width, height),
raster.NewRasterizer(width, height),
&truetype.GlyphBuf{},
DefaultDPI,
}
}
// RasterGraphicContext is the implementation of GraphicContext for a raster image
type RasterGraphicContext struct {
*StackGraphicContext
img draw.Image
painter Painter
fillRasterizer *raster.Rasterizer
strokeRasterizer *raster.Rasterizer
glyphBuf *truetype.GlyphBuf
DPI float64
}
// SetDPI sets the screen resolution in dots per inch.
func (rgc *RasterGraphicContext) SetDPI(dpi float64) {
rgc.DPI = dpi
rgc.recalc()
}
// GetDPI returns the resolution of the Image GraphicContext
func (rgc *RasterGraphicContext) GetDPI() float64 {
return rgc.DPI
}
// Clear fills the current canvas with a default transparent color
func (rgc *RasterGraphicContext) Clear() {
width, height := rgc.img.Bounds().Dx(), rgc.img.Bounds().Dy()
rgc.ClearRect(0, 0, width, height)
}
// ClearRect fills the current canvas with a default transparent color at the specified rectangle
func (rgc *RasterGraphicContext) ClearRect(x1, y1, x2, y2 int) {
imageColor := image.NewUniform(rgc.current.FillColor)
draw.Draw(rgc.img, image.Rect(x1, y1, x2, y2), imageColor, image.ZP, draw.Over)
}
// DrawImage draws the raster image in the current canvas
func (rgc *RasterGraphicContext) DrawImage(img image.Image) {
DrawImage(img, rgc.img, rgc.current.Tr, draw.Over, BilinearFilter)
}
// FillString draws the text at point (0, 0)
func (rgc *RasterGraphicContext) FillString(text string) (cursor float64, err error) {
cursor, err = rgc.FillStringAt(text, 0, 0)
return
}
// FillStringAt draws the text at the specified point (x, y)
func (rgc *RasterGraphicContext) FillStringAt(text string, x, y float64) (cursor float64, err error) {
cursor, err = rgc.CreateStringPath(text, x, y)
rgc.Fill()
return
}
// StrokeString draws the contour of the text at point (0, 0)
func (rgc *RasterGraphicContext) StrokeString(text string) (cursor float64, err error) {
cursor, err = rgc.StrokeStringAt(text, 0, 0)
return
}
// StrokeStringAt draws the contour of the text at point (x, y)
func (rgc *RasterGraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64, err error) {
cursor, err = rgc.CreateStringPath(text, x, y)
rgc.Stroke()
return
}
func (rgc *RasterGraphicContext) drawGlyph(glyph truetype.Index, dx, dy float64) error {
if err := rgc.glyphBuf.Load(rgc.current.Font, fixed.Int26_6(rgc.current.Scale), glyph, font.HintingNone); err != nil {
return err
}
e0 := 0
for _, e1 := range rgc.glyphBuf.Ends {
DrawContour(rgc, rgc.glyphBuf.Points[e0:e1], dx, dy)
e0 = e1
}
return nil
}
// CreateStringPath creates a path from the string s at x, y, and returns the string width.
// The text is placed so that the left edge of the em square of the first character of s
// and the baseline intersect at x, y. The majority of the affected pixels will be
// above and to the right of the point, but some may be below or to the left.
// For example, drawing a string that starts with a 'J' in an italic font may
// affect pixels below and left of the point.
func (rgc *RasterGraphicContext) CreateStringPath(s string, x, y float64) (cursor float64, err error) {
f := rgc.GetFont()
if f == nil {
err = errors.New("No font loaded, cannot continue")
return
}
rgc.recalc()
startx := x
prev, hasPrev := truetype.Index(0), false
for _, rc := range s {
index := f.Index(rc)
if hasPrev {
x += fUnitsToFloat64(f.Kern(fixed.Int26_6(rgc.current.Scale), prev, index))
}
err = rgc.drawGlyph(index, x, y)
if err != nil {
cursor = x - startx
return
}
x += fUnitsToFloat64(f.HMetric(fixed.Int26_6(rgc.current.Scale), index).AdvanceWidth)
prev, hasPrev = index, true
}
cursor = x - startx
return
}
// GetStringBounds returns the approximate pixel bounds of a string.
// The the left edge of the em square of the first character of s
// and the baseline intersect at 0, 0 in the returned coordinates.
func (rgc *RasterGraphicContext) GetStringBounds(s string) (left, top, right, bottom float64, err error) {
f := rgc.GetFont()
if f == nil {
err = errors.New("No font loaded, cannot continue")
return
}
rgc.recalc()
cursor := 0.0
prev, hasPrev := truetype.Index(0), false
for _, rc := range s {
index := f.Index(rc)
if hasPrev {
cursor += fUnitsToFloat64(f.Kern(fixed.Int26_6(rgc.current.Scale), prev, index))
}
if err = rgc.glyphBuf.Load(rgc.current.Font, fixed.Int26_6(rgc.current.Scale), index, font.HintingNone); err != nil {
return
}
e0 := 0
for _, e1 := range rgc.glyphBuf.Ends {
ps := rgc.glyphBuf.Points[e0:e1]
for _, p := range ps {
x, y := pointToF64Point(p)
top = math.Min(top, y)
bottom = math.Max(bottom, y)
left = math.Min(left, x+cursor)
right = math.Max(right, x+cursor)
}
e0 = e1
}
cursor += fUnitsToFloat64(f.HMetric(fixed.Int26_6(rgc.current.Scale), index).AdvanceWidth)
prev, hasPrev = index, true
}
return
}
// recalc recalculates scale and bounds values from the font size, screen
// resolution and font metrics, and invalidates the glyph cache.
func (rgc *RasterGraphicContext) recalc() {
rgc.current.Scale = rgc.current.FontSize * float64(rgc.DPI) * EMRatio
}
// SetFont sets the font used to draw text.
func (rgc *RasterGraphicContext) SetFont(font *truetype.Font) {
rgc.current.Font = font
}
// GetFont returns the font used to draw text.
func (rgc *RasterGraphicContext) GetFont() *truetype.Font {
return rgc.current.Font
}
// SetFontSize sets the font size in points (as in ``a 12 point font'').
func (rgc *RasterGraphicContext) SetFontSize(fontSize float64) {
rgc.current.FontSize = fontSize
rgc.recalc()
}
func (rgc *RasterGraphicContext) paint(rasterizer *raster.Rasterizer, color color.Color) {
rgc.painter.SetColor(color)
rasterizer.Rasterize(rgc.painter)
rasterizer.Clear()
rgc.current.Path.Clear()
}
// Stroke strokes the paths with the color specified by SetStrokeColor
func (rgc *RasterGraphicContext) Stroke(paths ...*Path) {
paths = append(paths, rgc.current.Path)
rgc.strokeRasterizer.UseNonZeroWinding = true
stroker := NewLineStroker(rgc.current.Cap, rgc.current.Join, Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.strokeRasterizer}})
stroker.HalfLineWidth = rgc.current.LineWidth / 2
var liner Flattener
if rgc.current.Dash != nil && len(rgc.current.Dash) > 0 {
liner = NewDashVertexConverter(rgc.current.Dash, rgc.current.DashOffset, stroker)
} else {
liner = stroker
}
for _, p := range paths {
Flatten(p, liner, rgc.current.Tr.GetScale())
}
rgc.paint(rgc.strokeRasterizer, rgc.current.StrokeColor)
}
// Fill fills the paths with the color specified by SetFillColor
func (rgc *RasterGraphicContext) Fill(paths ...*Path) {
paths = append(paths, rgc.current.Path)
rgc.fillRasterizer.UseNonZeroWinding = rgc.current.FillRule == FillRuleWinding
flattener := Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.fillRasterizer}}
for _, p := range paths {
Flatten(p, flattener, rgc.current.Tr.GetScale())
}
rgc.paint(rgc.fillRasterizer, rgc.current.FillColor)
}
// FillStroke first fills the paths and than strokes them
func (rgc *RasterGraphicContext) FillStroke(paths ...*Path) {
paths = append(paths, rgc.current.Path)
rgc.fillRasterizer.UseNonZeroWinding = rgc.current.FillRule == FillRuleWinding
rgc.strokeRasterizer.UseNonZeroWinding = true
flattener := Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.fillRasterizer}}
stroker := NewLineStroker(rgc.current.Cap, rgc.current.Join, Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.strokeRasterizer}})
stroker.HalfLineWidth = rgc.current.LineWidth / 2
var liner Flattener
if rgc.current.Dash != nil && len(rgc.current.Dash) > 0 {
liner = NewDashVertexConverter(rgc.current.Dash, rgc.current.DashOffset, stroker)
} else {
liner = stroker
}
demux := DemuxFlattener{Flatteners: []Flattener{flattener, liner}}
for _, p := range paths {
Flatten(p, demux, rgc.current.Tr.GetScale())
}
// Fill
rgc.paint(rgc.fillRasterizer, rgc.current.FillColor)
// Stroke
rgc.paint(rgc.strokeRasterizer, rgc.current.StrokeColor)
}

View File

@ -0,0 +1,183 @@
package drawing
import (
"image"
"image/color"
"github.com/golang/freetype/truetype"
)
// StackGraphicContext is a context that does thngs.
type StackGraphicContext struct {
current *ContextStack
}
// ContextStack is a graphic context implementation.
type ContextStack struct {
Tr Matrix
Path *Path
LineWidth float64
Dash []float64
DashOffset float64
StrokeColor color.Color
FillColor color.Color
FillRule FillRule
Cap LineCap
Join LineJoin
FontSize float64
Font *truetype.Font
Scale float64
Previous *ContextStack
}
// NewStackGraphicContext Create a new Graphic context from an image
func NewStackGraphicContext() *StackGraphicContext {
gc := &StackGraphicContext{}
gc.current = new(ContextStack)
gc.current.Tr = NewIdentityMatrix()
gc.current.Path = new(Path)
gc.current.LineWidth = 1.0
gc.current.StrokeColor = image.Black
gc.current.FillColor = image.White
gc.current.Cap = RoundCap
gc.current.FillRule = FillRuleEvenOdd
gc.current.Join = RoundJoin
gc.current.FontSize = 10
return gc
}
func (gc *StackGraphicContext) GetMatrixTransform() Matrix {
return gc.current.Tr
}
func (gc *StackGraphicContext) SetMatrixTransform(Tr Matrix) {
gc.current.Tr = Tr
}
func (gc *StackGraphicContext) ComposeMatrixTransform(Tr Matrix) {
gc.current.Tr.Compose(Tr)
}
func (gc *StackGraphicContext) Rotate(angle float64) {
gc.current.Tr.Rotate(angle)
}
func (gc *StackGraphicContext) Translate(tx, ty float64) {
gc.current.Tr.Translate(tx, ty)
}
func (gc *StackGraphicContext) Scale(sx, sy float64) {
gc.current.Tr.Scale(sx, sy)
}
func (gc *StackGraphicContext) SetStrokeColor(c color.Color) {
gc.current.StrokeColor = c
}
func (gc *StackGraphicContext) SetFillColor(c color.Color) {
gc.current.FillColor = c
}
func (gc *StackGraphicContext) SetFillRule(f FillRule) {
gc.current.FillRule = f
}
func (gc *StackGraphicContext) SetLineWidth(lineWidth float64) {
gc.current.LineWidth = lineWidth
}
func (gc *StackGraphicContext) SetLineCap(cap LineCap) {
gc.current.Cap = cap
}
func (gc *StackGraphicContext) SetLineJoin(join LineJoin) {
gc.current.Join = join
}
func (gc *StackGraphicContext) SetLineDash(dash []float64, dashOffset float64) {
gc.current.Dash = dash
gc.current.DashOffset = dashOffset
}
func (gc *StackGraphicContext) SetFontSize(fontSize float64) {
gc.current.FontSize = fontSize
}
func (gc *StackGraphicContext) GetFontSize() float64 {
return gc.current.FontSize
}
func (gc *StackGraphicContext) SetFont(f *truetype.Font) {
gc.current.Font = f
}
func (gc *StackGraphicContext) GetFont() *truetype.Font {
return gc.current.Font
}
func (gc *StackGraphicContext) BeginPath() {
gc.current.Path.Clear()
}
func (gc *StackGraphicContext) IsEmpty() bool {
return gc.current.Path.IsEmpty()
}
func (gc *StackGraphicContext) LastPoint() (float64, float64) {
return gc.current.Path.LastPoint()
}
func (gc *StackGraphicContext) MoveTo(x, y float64) {
gc.current.Path.MoveTo(x, y)
}
func (gc *StackGraphicContext) LineTo(x, y float64) {
gc.current.Path.LineTo(x, y)
}
func (gc *StackGraphicContext) QuadCurveTo(cx, cy, x, y float64) {
gc.current.Path.QuadCurveTo(cx, cy, x, y)
}
func (gc *StackGraphicContext) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {
gc.current.Path.CubicCurveTo(cx1, cy1, cx2, cy2, x, y)
}
func (gc *StackGraphicContext) ArcTo(cx, cy, rx, ry, startAngle, angle float64) {
gc.current.Path.ArcTo(cx, cy, rx, ry, startAngle, angle)
}
func (gc *StackGraphicContext) Close() {
gc.current.Path.Close()
}
func (gc *StackGraphicContext) Save() {
context := new(ContextStack)
context.FontSize = gc.current.FontSize
context.Font = gc.current.Font
context.LineWidth = gc.current.LineWidth
context.StrokeColor = gc.current.StrokeColor
context.FillColor = gc.current.FillColor
context.FillRule = gc.current.FillRule
context.Dash = gc.current.Dash
context.DashOffset = gc.current.DashOffset
context.Cap = gc.current.Cap
context.Join = gc.current.Join
context.Path = gc.current.Path.Copy()
context.Font = gc.current.Font
context.Scale = gc.current.Scale
copy(context.Tr[:], gc.current.Tr[:])
context.Previous = gc.current
gc.current = context
}
func (gc *StackGraphicContext) Restore() {
if gc.current.Previous != nil {
oldContext := gc.current
gc.current = gc.current.Previous
oldContext.Previous = nil
}
}

85
drawing/stroker.go Normal file
View File

@ -0,0 +1,85 @@
// Copyright 2010 The draw2d Authors. All rights reserved.
// created: 13/12/2010 by Laurent Le Goff
package drawing
// NewLineStroker creates a new line stroker.
func NewLineStroker(c LineCap, j LineJoin, flattener Flattener) *LineStroker {
l := new(LineStroker)
l.Flattener = flattener
l.HalfLineWidth = 0.5
l.Cap = c
l.Join = j
return l
}
// LineStroker draws the stroke portion of a line.
type LineStroker struct {
Flattener Flattener
HalfLineWidth float64
Cap LineCap
Join LineJoin
vertices []float64
rewind []float64
x, y, nx, ny float64
}
// MoveTo implements the path builder interface.
func (l *LineStroker) MoveTo(x, y float64) {
l.x, l.y = x, y
}
// LineTo implements the path builder interface.
func (l *LineStroker) LineTo(x, y float64) {
l.line(l.x, l.y, x, y)
}
// LineJoin implements the path builder interface.
func (l *LineStroker) LineJoin() {}
func (l *LineStroker) line(x1, y1, x2, y2 float64) {
dx := (x2 - x1)
dy := (y2 - y1)
d := vectorDistance(dx, dy)
if d != 0 {
nx := dy * l.HalfLineWidth / d
ny := -(dx * l.HalfLineWidth / d)
l.appendVertex(x1+nx, y1+ny, x2+nx, y2+ny, x1-nx, y1-ny, x2-nx, y2-ny)
l.x, l.y, l.nx, l.ny = x2, y2, nx, ny
}
}
// Close implements the path builder interface.
func (l *LineStroker) Close() {
if len(l.vertices) > 1 {
l.appendVertex(l.vertices[0], l.vertices[1], l.rewind[0], l.rewind[1])
}
}
// End implements the path builder interface.
func (l *LineStroker) End() {
if len(l.vertices) > 1 {
l.Flattener.MoveTo(l.vertices[0], l.vertices[1])
for i, j := 2, 3; j < len(l.vertices); i, j = i+2, j+2 {
l.Flattener.LineTo(l.vertices[i], l.vertices[j])
}
}
for i, j := len(l.rewind)-2, len(l.rewind)-1; j > 0; i, j = i-2, j-2 {
l.Flattener.LineTo(l.rewind[i], l.rewind[j])
}
if len(l.vertices) > 1 {
l.Flattener.LineTo(l.vertices[0], l.vertices[1])
}
l.Flattener.End()
// reinit vertices
l.vertices = l.vertices[0:0]
l.rewind = l.rewind[0:0]
l.x, l.y, l.nx, l.ny = 0, 0, 0, 0
}
func (l *LineStroker) appendVertex(vertices ...float64) {
s := len(vertices) / 2
l.vertices = append(l.vertices, vertices[:s]...)
l.rewind = append(l.rewind, vertices[s:]...)
}

74
drawing/text.go Normal file
View File

@ -0,0 +1,74 @@
// Copyright 2010 The draw2d Authors. All rights reserved.
// created: 13/12/2010 by Laurent Le Goff
package drawing
import (
"github.com/golang/freetype/truetype"
"golang.org/x/image/math/fixed"
)
// DrawContour draws the given closed contour at the given sub-pixel offset.
func DrawContour(path PathBuilder, ps []truetype.Point, dx, dy float64) {
if len(ps) == 0 {
return
}
startX, startY := pointToF64Point(ps[0])
path.MoveTo(startX+dx, startY+dy)
q0X, q0Y, on0 := startX, startY, true
for _, p := range ps[1:] {
qX, qY := pointToF64Point(p)
on := p.Flags&0x01 != 0
if on {
if on0 {
path.LineTo(qX+dx, qY+dy)
} else {
path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy)
}
} else {
if on0 {
// No-op.
} else {
midX := (q0X + qX) / 2
midY := (q0Y + qY) / 2
path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy)
}
}
q0X, q0Y, on0 = qX, qY, on
}
// Close the curve.
if on0 {
path.LineTo(startX+dx, startY+dy)
} else {
path.QuadCurveTo(q0X+dx, q0Y+dy, startX+dx, startY+dy)
}
}
// FontExtents contains font metric information.
type FontExtents struct {
// Ascent is the distance that the text
// extends above the baseline.
Ascent float64
// Descent is the distance that the text
// extends below the baseline. The descent
// is given as a negative value.
Descent float64
// Height is the distance from the lowest
// descending point to the highest ascending
// point.
Height float64
}
// Extents returns the FontExtents for a font.
// TODO needs to read this https://developer.apple.com/fonts/TrueType-Reference-Manual/RM02/Chap2.html#intro
func Extents(font *truetype.Font, size float64) FontExtents {
bounds := font.Bounds(fixed.Int26_6(font.FUnitsPerEm()))
scale := size / float64(font.FUnitsPerEm())
return FontExtents{
Ascent: float64(bounds.Max.Y) * scale,
Descent: float64(bounds.Min.Y) * scale,
Height: float64(bounds.Max.Y-bounds.Min.Y) * scale,
}
}

39
drawing/transformer.go Normal file
View File

@ -0,0 +1,39 @@
// Copyright 2010 The draw2d Authors. All rights reserved.
// created: 13/12/2010 by Laurent Le Goff
package drawing
// Transformer apply the Matrix transformation tr
type Transformer struct {
Tr Matrix
Flattener Flattener
}
// MoveTo implements the path builder interface.
func (t Transformer) MoveTo(x, y float64) {
u := x*t.Tr[0] + y*t.Tr[2] + t.Tr[4]
v := x*t.Tr[1] + y*t.Tr[3] + t.Tr[5]
t.Flattener.MoveTo(u, v)
}
// LineTo implements the path builder interface.
func (t Transformer) LineTo(x, y float64) {
u := x*t.Tr[0] + y*t.Tr[2] + t.Tr[4]
v := x*t.Tr[1] + y*t.Tr[3] + t.Tr[5]
t.Flattener.LineTo(u, v)
}
// LineJoin implements the path builder interface.
func (t Transformer) LineJoin() {
t.Flattener.LineJoin()
}
// Close implements the path builder interface.
func (t Transformer) Close() {
t.Flattener.Close()
}
// End implements the path builder interface.
func (t Transformer) End() {
t.Flattener.End()
}

56
drawing/util.go Normal file
View File

@ -0,0 +1,56 @@
package drawing
import (
"math"
"golang.org/x/image/math/fixed"
"github.com/golang/freetype/raster"
"github.com/golang/freetype/truetype"
)
func abs(i int) int {
if i < 0 {
return -i
}
return i
}
func distance(x1, y1, x2, y2 float64) float64 {
return vectorDistance(x2-x1, y2-y1)
}
func vectorDistance(dx, dy float64) float64 {
return float64(math.Sqrt(dx*dx + dy*dy))
}
func toFtCap(c LineCap) raster.Capper {
switch c {
case RoundCap:
return raster.RoundCapper
case ButtCap:
return raster.ButtCapper
case SquareCap:
return raster.SquareCapper
}
return raster.RoundCapper
}
func toFtJoin(j LineJoin) raster.Joiner {
switch j {
case RoundJoin:
return raster.RoundJoiner
case BevelJoin:
return raster.BevelJoiner
}
return raster.RoundJoiner
}
func pointToF64Point(p truetype.Point) (x, y float64) {
return fUnitsToFloat64(p.X), -fUnitsToFloat64(p.Y)
}
func fUnitsToFloat64(x fixed.Int26_6) float64 {
scaled := x << 2
return float64(scaled/256) + float64(scaled%256)/256.0
}

View File

@ -5,31 +5,38 @@ import (
"image/color" "image/color"
"image/png" "image/png"
"io" "io"
"math"
"golang.org/x/image/font"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
drawing "github.com/llgcode/draw2d/draw2dimg" "github.com/wcharczuk/go-chart/drawing"
) )
// PNG returns a new png/raster renderer. // PNG returns a new png/raster renderer.
func PNG(width, height int) Renderer { func PNG(width, height int) (Renderer, error) {
i := image.NewRGBA(image.Rect(0, 0, width, height)) i := image.NewRGBA(image.Rect(0, 0, width, height))
return &rasterRenderer{ gc, err := drawing.NewRasterGraphicContext(i)
i: i, if err == nil {
gc: drawing.NewGraphicContext(i), return &rasterRenderer{
i: i,
gc: gc,
}, nil
} }
return nil, err
} }
// rasterRenderer renders chart commands to a bitmap. // rasterRenderer renders chart commands to a bitmap.
type rasterRenderer struct { type rasterRenderer struct {
i *image.RGBA i *image.RGBA
gc *drawing.GraphicContext gc *drawing.RasterGraphicContext
fc *font.Drawer
font *truetype.Font
fontColor color.RGBA
fontSize float64 fontSize float64
fontColor color.RGBA
f *truetype.Font
}
// SetDPI implements the interface method.
func (rr *rasterRenderer) SetDPI(dpi float64) {
rr.gc.SetDPI(dpi)
} }
// SetStrokeColor implements the interface method. // SetStrokeColor implements the interface method.
@ -92,7 +99,7 @@ func (rr *rasterRenderer) Circle(radius float64, x, y int) {
// SetFont implements the interface method. // SetFont implements the interface method.
func (rr *rasterRenderer) SetFont(f *truetype.Font) { func (rr *rasterRenderer) SetFont(f *truetype.Font) {
rr.font = f rr.f = f
rr.gc.SetFont(f) rr.gc.SetFont(f)
} }
@ -115,25 +122,20 @@ func (rr *rasterRenderer) Text(body string, x, y int) {
rr.gc.Fill() rr.gc.Fill()
} }
// MeasureText uses the truetype font drawer to measure the width of text. // MeasureText returns the height and width in pixels of a string.
func (rr *rasterRenderer) MeasureText(body string) int { func (rr *rasterRenderer) MeasureText(body string) (width int, height int) {
if rr.fc == nil && rr.font != nil { l, t, r, b, err := rr.gc.GetStringBounds(body)
rr.fc = &font.Drawer{ if err != nil {
Face: truetype.NewFace(rr.font, &truetype.Options{ return
DPI: DefaultDPI,
Size: rr.fontSize,
}),
}
} }
if rr.fc != nil { dw := r - l
dimensions := rr.fc.MeasureString(body) dh := b - t
return dimensions.Floor() width = int(math.Ceil(dw * (4.0 / 3.0)))
} height = int(math.Ceil(dh * (4.0 / 3.0)))
return 0 return
} }
// Save implements the interface method. // Save implements the interface method.
func (rr *rasterRenderer) Save(w io.Writer) error { func (rr *rasterRenderer) Save(w io.Writer) error {
return png.Encode(w, rr.i) return png.Encode(w, rr.i)
} }

View File

@ -8,10 +8,13 @@ import (
) )
// RendererProvider is a function that returns a renderer. // RendererProvider is a function that returns a renderer.
type RendererProvider func(int, int) Renderer type RendererProvider func(int, int) (Renderer, error)
// Renderer represents the basic methods required to draw a chart. // Renderer represents the basic methods required to draw a chart.
type Renderer interface { type Renderer interface {
// SetDPI sets the DPI for the renderer.
SetDPI(dpi float64)
// SetStrokeColor sets the current stroke color. // SetStrokeColor sets the current stroke color.
SetStrokeColor(color.RGBA) SetStrokeColor(color.RGBA)
@ -56,7 +59,7 @@ type Renderer interface {
Text(body string, x, y int) Text(body string, x, y int)
// MeasureText measures text. // MeasureText measures text.
MeasureText(body string) int MeasureText(body string) (width int, height int)
// Save writes the image to the given writer. // Save writes the image to the given writer.
Save(w io.Writer) error Save(w io.Writer) error

View File

@ -102,7 +102,7 @@ func (s Style) SVG() string {
fontSizeText := "" fontSizeText := ""
if fs != 0 { if fs != 0 {
fontSizeText = "font-size:" + fmt.Sprintf("%.1f", fs) fontSizeText = "font-size:" + fmt.Sprintf("%.1fpx", fs)
} }
if !ColorIsZero(fnc) { if !ColorIsZero(fnc) {

View File

@ -2,6 +2,7 @@ package main
import ( import (
"log" "log"
"net/http"
"github.com/wcharczuk/go-chart" "github.com/wcharczuk/go-chart"
"github.com/wcharczuk/go-web" "github.com/wcharczuk/go-web"
@ -49,6 +50,7 @@ func main() {
if err != nil { if err != nil {
return rc.API().InternalError(err) return rc.API().InternalError(err)
} }
rc.Response.WriteHeader(http.StatusOK)
return nil return nil
}) })
log.Fatal(app.Start()) log.Fatal(app.Start())

View File

@ -66,3 +66,7 @@ func Slices(count int, total float64) []float64 {
} }
return values return values
} }
func flf(v float64) string {
return fmt.Sprintf("%.2f", v)
}

View File

@ -5,37 +5,44 @@ import (
"fmt" "fmt"
"image/color" "image/color"
"io" "io"
"math"
"strings" "strings"
"golang.org/x/image/font" "golang.org/x/image/font"
"github.com/ajstarks/svgo"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
) )
// SVG returns a new png/raster renderer. // SVG returns a new png/raster renderer.
func SVG(width, height int) Renderer { func SVG(width, height int) (Renderer, error) {
buffer := bytes.NewBuffer([]byte{}) buffer := bytes.NewBuffer([]byte{})
canvas := svg.New(buffer) canvas := newCanvas(buffer)
canvas.Start(width, height) canvas.Start(width, height)
return &vectorRenderer{ return &vectorRenderer{
b: buffer, b: buffer,
c: canvas, c: canvas,
s: &Style{}, s: &Style{},
p: []string{}, p: []string{},
} }, nil
} }
// vectorRenderer renders chart commands to a bitmap. // vectorRenderer renders chart commands to a bitmap.
type vectorRenderer struct { type vectorRenderer struct {
b *bytes.Buffer dpi float64
c *svg.SVG b *bytes.Buffer
s *Style c *canvas
f *truetype.Font s *Style
p []string f *truetype.Font
fc *font.Drawer p []string
fc *font.Drawer
} }
// SetDPI implements the interface method.
func (vr *vectorRenderer) SetDPI(dpi float64) {
vr.dpi = dpi
}
// SetStrokeColor implements the interface method.
func (vr *vectorRenderer) SetStrokeColor(c color.RGBA) { func (vr *vectorRenderer) SetStrokeColor(c color.RGBA) {
vr.s.StrokeColor = c vr.s.StrokeColor = c
} }
@ -130,20 +137,18 @@ func (vr *vectorRenderer) Text(body string, x, y int) {
} }
// MeasureText uses the truetype font drawer to measure the width of text. // MeasureText uses the truetype font drawer to measure the width of text.
func (vr *vectorRenderer) MeasureText(body string) int { func (vr *vectorRenderer) MeasureText(body string) (width, height int) {
if vr.fc == nil && vr.f != nil { if vr.f != nil {
vr.fc = &font.Drawer{ vr.fc = &font.Drawer{
Face: truetype.NewFace(vr.f, &truetype.Options{ Face: truetype.NewFace(vr.f, &truetype.Options{
DPI: DefaultDPI, DPI: vr.dpi,
Size: vr.s.FontSize, Size: vr.s.FontSize,
}), }),
} }
width = vr.fc.MeasureString(body).Ceil()
height = int(math.Ceil(vr.s.FontSize))
} }
if vr.fc != nil { return
dimensions := vr.fc.MeasureString(body)
return dimensions.Floor()
}
return 0
} }
func (vr *vectorRenderer) Save(w io.Writer) error { func (vr *vectorRenderer) Save(w io.Writer) error {
@ -151,3 +156,49 @@ func (vr *vectorRenderer) Save(w io.Writer) error {
_, err := w.Write(vr.b.Bytes()) _, err := w.Write(vr.b.Bytes())
return err return err
} }
func newCanvas(w io.Writer) *canvas {
return &canvas{
w: w,
}
}
type canvas struct {
w io.Writer
width int
height int
}
func (c *canvas) Start(width, height int) {
c.width = width
c.height = height
c.w.Write([]byte(fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%d" height="%d">\n`, c.width, c.height)))
}
func (c *canvas) Path(d string, style ...string) {
if len(style) > 0 {
c.w.Write([]byte(fmt.Sprintf(`<path d="%s" style="%s"/>\n`, d, style[0])))
} else {
c.w.Write([]byte(fmt.Sprintf(`<path d="%s"/>\n`, d)))
}
}
func (c *canvas) Text(x, y int, body string, style ...string) {
if len(style) > 0 {
c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" style="%s">%s</text>`, x, y, style[0], body)))
} else {
c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d">%s</text>`, x, y, body)))
}
}
func (c *canvas) Circle(x, y, r int, style ...string) {
if len(style) > 0 {
c.w.Write([]byte(fmt.Sprintf(`<circle cx="%d" cy="%d" r="%d" style="%s">`, x, y, r, style[0])))
} else {
c.w.Write([]byte(fmt.Sprintf(`<circle cx="%d" cy="%d" r="%d">`, x, y, r)))
}
}
func (c *canvas) End() {
c.w.Write([]byte("</svg>"))
}