removing 3rd party deps.
This commit is contained in:
parent
7f36c08fbf
commit
8bc8b1087c
27 changed files with 1995 additions and 54 deletions
26
chart.go
26
chart.go
|
@ -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)
|
||||||
|
|
|
@ -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
9
drawing/constants.go
Normal 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
158
drawing/curve.go
Normal 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
89
drawing/dasher.go
Normal 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
|
||||||
|
}
|
41
drawing/demux_flattener.go
Normal file
41
drawing/demux_flattener.go
Normal 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
148
drawing/drawing.go
Normal 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
90
drawing/flattener.go
Normal 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
30
drawing/free_type_path.go
Normal 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() {}
|
82
drawing/graphic_context.go
Normal file
82
drawing/graphic_context.go
Normal 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
13
drawing/image_filter.go
Normal 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
48
drawing/line.go
Normal 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
219
drawing/matrix.go
Normal 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
31
drawing/painter.go
Normal 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
186
drawing/path.go
Normal 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
|
||||||
|
}
|
282
drawing/raster_graphic_context.go
Normal file
282
drawing/raster_graphic_context.go
Normal 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)
|
||||||
|
}
|
183
drawing/stack_graphic_context.go
Normal file
183
drawing/stack_graphic_context.go
Normal 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
85
drawing/stroker.go
Normal 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
74
drawing/text.go
Normal 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
39
drawing/transformer.go
Normal 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
56
drawing/util.go
Normal 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
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
2
style.go
2
style.go
|
@ -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) {
|
||||||
|
|
|
@ -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())
|
||||||
|
|
4
util.go
4
util.go
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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>"))
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue