This commit is contained in:
legoff.laurent 2010-11-23 15:59:21 +00:00
parent 5b82e83e21
commit acce541041
8 changed files with 1416 additions and 0 deletions

17
draw2d/.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>draw2d</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.googlecode.goclipse.goBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>goclipse.goNature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,412 @@
package main
import (
"fmt"
"log"
"os"
"bufio"
"time"
"math"
"image"
"image/png"
"draw2d"
)
const (
w, h = 256, 256
)
var (
lastTime int64
folder = "../../test_results/"
)
func initGc(w, h int) (image.Image, *draw2d.GraphicContext) {
i := image.NewRGBA(w, h)
gc := draw2d.NewGraphicContext(i)
lastTime = time.Nanoseconds()
gc.SetStrokeColor(image.Black)
gc.SetFillColor(image.White)
// fill the background
//gc.Clear()
return i, gc
}
func saveToPngFile(TestName string, m image.Image) {
dt := time.Nanoseconds() - lastTime
fmt.Printf("%s during: %f ms\n", TestName, float(dt)*10e-6)
filePath := folder + TestName + ".png"
f, err := os.Open(filePath, os.O_CREAT|os.O_WRONLY, 0600)
if err != nil {
log.Println(err)
os.Exit(1)
}
defer f.Close()
b := bufio.NewWriter(f)
err = png.Encode(b, m)
if err != nil {
log.Println(err)
os.Exit(1)
}
err = b.Flush()
if err != nil {
log.Println(err)
os.Exit(1)
}
fmt.Printf("Wrote %s OK.\n", filePath)
}
/*
<img src="../test_results/TestPath.png"/>
*/
func TestPath() {
i, gc := initGc(w, h)
gc.MoveTo(10.0, 10.0)
gc.LineTo(100.0, 10.0)
gc.LineTo(100.0, 100.0)
gc.LineTo(10.0, 100.0)
gc.LineTo(10.0, 10.0)
gc.FillStroke()
saveToPngFile("TestPath", i)
}
func cos(f float) float {
return float(math.Cos(float64(f)))
}
func sin(f float) float {
return float(math.Sin(float64(f)))
}
/*
<img src="../test_results/TestDrawArc.png"/>
*/
func TestDrawArc() {
i, gc := initGc(w, h)
// draw an arc
xc, yc := 128.0, 128.0
radiusX, radiusY := 100.0, 100.0
startAngle := 45.0 * (math.Pi / 180.0) /* angles are specified */
angle := 135 * (math.Pi / 180.0) /* in radians */
gc.SetLineWidth(10)
gc.SetLineCap(draw2d.ButtCap)
gc.SetStrokeColor(image.Black)
gc.ArcTo(xc, yc, radiusX, radiusY, startAngle, angle)
gc.Stroke()
// fill a circle
gc.SetStrokeColor(image.RGBAColor{255, 0x33, 0x33, 0x80})
gc.SetFillColor(image.RGBAColor{255, 0x33, 0x33, 0x80})
gc.SetLineWidth(6)
gc.MoveTo(xc, yc)
gc.LineTo(xc+cos(startAngle)*radiusX, yc+sin(startAngle)*radiusY)
gc.MoveTo(xc, yc)
gc.LineTo(xc-radiusX, yc)
gc.Stroke()
gc.ArcTo(xc, yc, 10.0, 10.0, 0, 2*math.Pi)
gc.Fill()
saveToPngFile("TestDrawArc", i)
}
/*
<img src="../test_results/TestDrawArc.png"/>
*/
func TestDrawArcNegative() {
i, gc := initGc(w, h)
// draw an arc
xc, yc := 128.0, 128.0
radiusX, radiusY := 100.0, 100.0
startAngle := 45.0 * (math.Pi / 180.0) /* angles are specified */
angle := -225 * (math.Pi / 180.0) /* in radians */
gc.SetLineWidth(10)
gc.SetLineCap(draw2d.ButtCap)
gc.SetStrokeColor(image.Black)
gc.ArcTo(xc, yc, radiusX, radiusY, startAngle, angle)
gc.Stroke()
// fill a circle
gc.SetStrokeColor(image.RGBAColor{255, 0x33, 0x33, 0x80})
gc.SetFillColor(image.RGBAColor{255, 0x33, 0x33, 0x80})
gc.SetLineWidth(6)
gc.MoveTo(xc, yc)
gc.LineTo(xc+cos(startAngle)*radiusX, yc+sin(startAngle)*radiusY)
gc.MoveTo(xc, yc)
gc.LineTo(xc-radiusX, yc)
gc.Stroke()
gc.ArcTo(xc, yc, 10.0, 10.0, 0, 2*math.Pi)
gc.Fill()
saveToPngFile("TestDrawArcNegative", i)
}
func TestCurveRectangle() {
i, gc := initGc(w, h)
/* a custom shape that could be wrapped in a function */
x0, y0 := 25.6, 25.6 /* parameters like cairo_rectangle */
rect_width, rect_height := 204.8, 204.8
radius := 102.4 /* and an approximate curvature radius */
x1 := x0 + rect_width
y1 := y0 + rect_height
if rect_width/2 < radius {
if rect_height/2 < radius {
gc.MoveTo(x0, (y0+y1)/2)
gc.CubicCurveTo(x0, y0, x0, y0, (x0+x1)/2, y0)
gc.CubicCurveTo(x1, y0, x1, y0, x1, (y0+y1)/2)
gc.CubicCurveTo(x1, y1, x1, y1, (x1+x0)/2, y1)
gc.CubicCurveTo(x0, y1, x0, y1, x0, (y0+y1)/2)
} else {
gc.MoveTo(x0, y0+radius)
gc.CubicCurveTo(x0, y0, x0, y0, (x0+x1)/2, y0)
gc.CubicCurveTo(x1, y0, x1, y0, x1, y0+radius)
gc.LineTo(x1, y1-radius)
gc.CubicCurveTo(x1, y1, x1, y1, (x1+x0)/2, y1)
gc.CubicCurveTo(x0, y1, x0, y1, x0, y1-radius)
}
} else {
if rect_height/2 < radius {
gc.MoveTo(x0, (y0+y1)/2)
gc.CubicCurveTo(x0, y0, x0, y0, x0+radius, y0)
gc.LineTo(x1-radius, y0)
gc.CubicCurveTo(x1, y0, x1, y0, x1, (y0+y1)/2)
gc.CubicCurveTo(x1, y1, x1, y1, x1-radius, y1)
gc.LineTo(x0+radius, y1)
gc.CubicCurveTo(x0, y1, x0, y1, x0, (y0+y1)/2)
} else {
gc.MoveTo(x0, y0+radius)
gc.CubicCurveTo(x0, y0, x0, y0, x0+radius, y0)
gc.LineTo(x1-radius, y0)
gc.CubicCurveTo(x1, y0, x1, y0, x1, y0+radius)
gc.LineTo(x1, y1-radius)
gc.CubicCurveTo(x1, y1, x1, y1, x1-radius, y1)
gc.LineTo(x0+radius, y1)
gc.CubicCurveTo(x0, y1, x0, y1, x0, y1-radius)
}
}
gc.Close()
gc.SetFillColor(image.RGBAColor{0x80, 0x80, 0xFF, 0xFF})
gc.SetStrokeColor(image.RGBAColor{0x80, 0, 0, 0x80})
gc.SetLineWidth(10.0)
gc.FillStroke()
saveToPngFile("TestCurveRectangle", i)
}
/*
<img src="../test_results/TestDrawCubicCurve.png"/>
*/
func TestDrawCubicCurve() {
i, gc := initGc(w, h)
// draw a cubic curve
x, y := 25.6, 128.0
x1, y1 := 102.4, 230.4
x2, y2 := 153.6, 25.6
x3, y3 := 230.4, 128.0
gc.SetFillColor(image.RGBAColor{0xAA, 0xAA, 0xAA, 0xFF})
gc.SetLineWidth(10)
gc.MoveTo(x, y)
gc.CubicCurveTo(x1, y1, x2, y2, x3, y3)
gc.Stroke()
gc.SetStrokeColor(image.RGBAColor{0xFF, 0x33, 0x33, 0x88})
gc.SetLineWidth(6)
// draw segment of curve
gc.MoveTo(x, y)
gc.LineTo(x1, y1)
gc.LineTo(x2, y2)
gc.LineTo(x3, y3)
gc.Stroke()
saveToPngFile("TestDrawCubicCurve", i)
}
/*
<img src="../test_results/TestDash.png"/>
*/
func TestDash() {
i, gc := initGc(w, h)
gc.SetLineDash([]float{50, 10, 10, 10}, -50.0)
gc.SetLineCap(draw2d.ButtCap)
gc.SetLineJoin(draw2d.BevelJoin)
gc.SetLineWidth(10)
gc.MoveTo(128.0, 25.6)
gc.LineTo(128.0, 25.6)
gc.LineTo(230.4, 230.4)
gc.RLineTo(-102.4, 0.0)
gc.CubicCurveTo(51.2, 230.4, 51.2, 128.0, 128.0, 128.0)
gc.Stroke()
gc.SetLineDash(nil, 0.0)
saveToPngFile("TestDash", i)
}
/*
<img src="../test_results/TestFillStroke.png"/>
*/
func TestFillStroke() {
i, gc := initGc(w, h)
gc.MoveTo(128.0, 25.6)
gc.LineTo(230.4, 230.4)
gc.RLineTo(-102.4, 0.0)
gc.CubicCurveTo(51.2, 230.4, 51.2, 128.0, 128.0, 128.0)
gc.Close()
gc.MoveTo(64.0, 25.6)
gc.RLineTo(51.2, 51.2)
gc.RLineTo(-51.2, 51.2)
gc.RLineTo(-51.2, -51.2)
gc.Close()
gc.SetLineWidth(10.0)
gc.SetFillColor(image.RGBAColor{0, 0, 0xFF, 0xFF})
gc.SetStrokeColor(image.Black)
gc.FillStroke()
saveToPngFile("TestFillStroke", i)
}
/*
<img src="../test_results/TestFillStyle.png"/>
*/
func TestFillStyle() {
i, gc := initGc(w, h)
gc.SetLineWidth(6)
gc.Rect(12, 12, 244, 70)
wheel1 := new(draw2d.Path)
wheel1.ArcTo(64, 64, 40, 40, 0, 2*math.Pi)
wheel2 := new(draw2d.Path)
wheel2.ArcTo(192, 64, 40, 40, 0, 2*math.Pi)
gc.SetFillRule(draw2d.FillRuleEvenOdd)
gc.SetFillColor(image.RGBAColor{0, 0xB2, 0, 0xFF})
gc.SetStrokeColor(image.Black)
gc.FillStroke(wheel1, wheel2)
gc.Rect(12, 140, 244, 198)
wheel1 = new(draw2d.Path)
wheel1.ArcTo(64, 192, 40, 40, 0, 2*math.Pi)
wheel2 = new(draw2d.Path)
wheel2.ArcTo(192, 192, 40, 40, 0, -2*math.Pi)
gc.SetFillRule(draw2d.FillRuleWinding)
gc.SetFillColor(image.RGBAColor{0, 0, 0xE5, 0xFF})
gc.FillStroke(wheel1, wheel2)
saveToPngFile("TestFillStyle", i)
}
func TestMultiSegmentCaps() {
i, gc := initGc(w, h)
gc.MoveTo(50.0, 75.0)
gc.LineTo(200.0, 75.0)
gc.MoveTo(50.0, 125.0)
gc.LineTo(200.0, 125.0)
gc.MoveTo(50.0, 175.0)
gc.LineTo(200.0, 175.0)
gc.SetLineWidth(30.0)
gc.SetLineCap(draw2d.RoundCap)
gc.Stroke()
saveToPngFile("TestMultiSegmentCaps", i)
}
func TestRoundRectangle() {
i, gc := initGc(w, h)
/* a custom shape that could be wrapped in a function */
x, y := 25.6, 25.6
width, height := 204.8, 204.8
aspect := 1.0 /* aspect ratio */
corner_radius := height / 10.0 /* and corner curvature radius */
radius := corner_radius / aspect
degrees := math.Pi / 180.0
gc.ArcTo(x+width-radius, y+radius, radius, radius, -90*degrees, 90*degrees)
gc.ArcTo(x+width-radius, y+height-radius, radius, radius, 0*degrees, 90*degrees)
gc.ArcTo(x+radius, y+height-radius, radius, radius, 90*degrees, 90*degrees)
gc.ArcTo(x+radius, y+radius, radius, radius, 180*degrees, 90*degrees)
gc.Close()
gc.SetFillColor(image.RGBAColor{0x80, 0x80, 0xFF, 0xFF})
gc.SetStrokeColor(image.RGBAColor{0x80, 0, 0, 0x80})
gc.SetLineWidth(10.0)
gc.FillStroke()
saveToPngFile("TestRoundRectangle", i)
}
func TestLineCap() {
i, gc := initGc(w, h)
gc.SetLineWidth(30.0)
gc.SetLineCap(draw2d.ButtCap)
gc.MoveTo(64.0, 50.0)
gc.LineTo(64.0, 200.0)
gc.Stroke()
gc.SetLineCap(draw2d.RoundCap)
gc.MoveTo(128.0, 50.0)
gc.LineTo(128.0, 200.0)
gc.Stroke()
gc.SetLineCap(draw2d.SquareCap)
gc.MoveTo(192.0, 50.0)
gc.LineTo(192.0, 200.0)
gc.Stroke()
/* draw helping lines */
gc.SetStrokeColor(image.RGBAColor{0xFF, 0x33, 0x33, 0xFF})
gc.SetLineWidth(2.56)
gc.MoveTo(64.0, 50.0)
gc.LineTo(64.0, 200.0)
gc.MoveTo(128.0, 50.0)
gc.LineTo(128.0, 200.0)
gc.MoveTo(192.0, 50.0)
gc.LineTo(192.0, 200.0)
gc.Stroke()
saveToPngFile("TestLineCap", i)
}
func TestLineJoin() {
i, gc := initGc(w, h)
gc.SetLineWidth(40.96)
gc.MoveTo(76.8, 84.48)
gc.RLineTo(51.2, -51.2)
gc.RLineTo(51.2, 51.2)
gc.SetLineJoin(draw2d.MiterJoin) /* default */
gc.Stroke()
gc.MoveTo(76.8, 161.28)
gc.RLineTo(51.2, -51.2)
gc.RLineTo(51.2, 51.2)
gc.SetLineJoin(draw2d.BevelJoin)
gc.Stroke()
gc.MoveTo(76.8, 238.08)
gc.RLineTo(51.2, -51.2)
gc.RLineTo(51.2, 51.2)
gc.SetLineJoin(draw2d.RoundJoin)
gc.Stroke()
saveToPngFile("TestLineJoin", i)
}
func main() {
TestPath()
TestDrawArc()
TestDrawArcNegative()
TestDrawCubicCurve()
TestCurveRectangle()
TestDash()
TestFillStroke()
TestFillStyle()
TestMultiSegmentCaps()
TestRoundRectangle()
TestLineCap()
TestLineJoin()
}

View File

@ -0,0 +1,17 @@
package main
import (
"draw2d"
"fmt"
)
func main() {
path := new(draw2d.Path)
path.MoveTo(2.0, 3.0)
path.LineTo(2.0, 3.0)
path.QuadCurveTo(2.0, 3.0, 10, 20)
path.CubicCurveTo(2.0, 3.0, 10, 20, 13, 23)
path.Rect(2.0, 3.0, 100, 200)
path.ArcTo(2.0, 3.0, 100, 200, 200, 300)
fmt.Printf("%v\n", path)
}

View File

@ -0,0 +1,31 @@
package draw2d
func arc(t LineTracer, x, y, rx, ry, start, angle, scale float) {
end := start + angle
clockWise := true
if angle < 0 {
clockWise = false
}
ra := (fabs(rx) + fabs(ry)) / 2
da := acos(ra/(ra+0.125/scale)) * 2
//normalize
if !clockWise {
da = -da
}
angle = start + da
var curX, curY float
for {
if (angle < end-da/4) != clockWise {
curX = x + cos(end)*rx
curY = y + sin(end)*ry
t.LineTo(curX, curY)
break
}
curX = x + cos(angle)*rx
curY = y + sin(angle)*ry
angle += da
t.LineTo(curX, curY)
}
}

View File

@ -0,0 +1,339 @@
package draw2d
import (
"math"
)
var (
CurveRecursionLimit = 32
CurveCollinearityEpsilon = 1e-30
CurveAngleToleranceEpsilon = 0.01
)
/*
The function has the following parameters:
approximationScale :
Eventually determines the approximation accuracy. In practice we need to transform points from the World coordinate system to the Screen one.
It always has some scaling coefficient.
The curves are usually processed in the World coordinates, while the approximation accuracy should be eventually in pixels.
Usually it looks as follows:
curved.approximationScale(transform.scale());
where transform is the affine matrix that includes all the transformations, including viewport and zoom.
angleTolerance :
You set it in radians.
The less this value is the more accurate will be the approximation at sharp turns.
But 0 means that we don't consider angle conditions at all.
cuspLimit :
An angle in radians.
If 0, only the real cusps will have bevel cuts.
If more than 0, it will restrict the sharpness.
The more this value is the less sharp turns will be cut.
Typically it should not exceed 10-15 degrees.
*/
func cubicBezier(v LineTracer, x1, y1, x2, y2, x3, y3, x4, y4, approximationScale, angleTolerance, cuspLimit float) {
cuspLimit = computeCuspLimit(cuspLimit)
distanceToleranceSquare := 0.5 / approximationScale
distanceToleranceSquare = distanceToleranceSquare * distanceToleranceSquare
recursiveCubicBezier(v, x1, y1, x2, y2, x3, y3, x4, y4, 0, distanceToleranceSquare, angleTolerance, cuspLimit)
v.LineTo(x4, y4)
}
/*
* see cubicBezier comments for approximationScale and angleTolerance definition
*/
func quadraticBezier(v LineTracer, x1, y1, x2, y2, x3, y3, approximationScale, angleTolerance float) {
distanceToleranceSquare := 0.5 / approximationScale
distanceToleranceSquare = distanceToleranceSquare * distanceToleranceSquare
recursiveQuadraticBezierBezier(v, x1, y1, x2, y2, x3, y3, 0, distanceToleranceSquare, angleTolerance)
v.LineTo(x3, y3)
}
func computeCuspLimit(v float) (r float) {
if v == 0.0 {
r = 0.0
} else {
r = math.Pi - v
}
return
}
/**
* http://www.antigrain.com/research/adaptive_bezier/index.html
*/
func recursiveQuadraticBezierBezier(v LineTracer, x1, y1, x2, y2, x3, y3 float, level int, distanceToleranceSquare, angleTolerance float) {
if level > CurveRecursionLimit {
return
}
// Calculate all the mid-points of the line segments
//----------------------
x12 := (x1 + x2) / 2
y12 := (y1 + y2) / 2
x23 := (x2 + x3) / 2
y23 := (y2 + y3) / 2
x123 := (x12 + x23) / 2
y123 := (y12 + y23) / 2
dx := x3 - x1
dy := y3 - y1
d := fabs(((x2-x3)*dy - (y2-y3)*dx))
if d > CurveCollinearityEpsilon {
// Regular case
//-----------------
if d*d <= distanceToleranceSquare*(dx*dx+dy*dy) {
// If the curvature doesn't exceed the distanceTolerance value
// we tend to finish subdivisions.
//----------------------
if angleTolerance < CurveAngleToleranceEpsilon {
v.LineTo(x123, y123)
return
}
// Angle & Cusp Condition
//----------------------
da := fabs(atan2(y3-y2, x3-x2) - atan2(y2-y1, x2-x1))
if da >= math.Pi {
da = 2*math.Pi - da
}
if da < angleTolerance {
// Finally we can stop the recursion
//----------------------
v.LineTo(x123, y123)
return
}
}
} else {
// Collinear case
//------------------
da := dx*dx + dy*dy
if da == 0 {
d = squareDistance(x1, y1, x2, y2)
} else {
d = ((x2-x1)*dx + (y2-y1)*dy) / da
if d > 0 && d < 1 {
// Simple collinear case, 1---2---3
// We can leave just two endpoints
return
}
if d <= 0 {
d = squareDistance(x2, y2, x1, y1)
} else if d >= 1 {
d = squareDistance(x2, y2, x3, y3)
} else {
d = squareDistance(x2, y2, x1+d*dx, y1+d*dy)
}
}
if d < distanceToleranceSquare {
v.LineTo(x2, y2)
return
}
}
// Continue subdivision
//----------------------
recursiveQuadraticBezierBezier(v, x1, y1, x12, y12, x123, y123, level+1, distanceToleranceSquare, angleTolerance)
recursiveQuadraticBezierBezier(v, x123, y123, x23, y23, x3, y3, level+1, distanceToleranceSquare, angleTolerance)
}
/**
* http://www.antigrain.com/research/adaptive_bezier/index.html
*/
func recursiveCubicBezier(v LineTracer, x1, y1, x2, y2, x3, y3, x4, y4 float, level int, distanceToleranceSquare, angleTolerance, cuspLimit float) {
if level > CurveRecursionLimit {
return
}
// Calculate all the mid-points of the line segments
//----------------------
x12 := (x1 + x2) / 2
y12 := (y1 + y2) / 2
x23 := (x2 + x3) / 2
y23 := (y2 + y3) / 2
x34 := (x3 + x4) / 2
y34 := (y3 + y4) / 2
x123 := (x12 + x23) / 2
y123 := (y12 + y23) / 2
x234 := (x23 + x34) / 2
y234 := (y23 + y34) / 2
x1234 := (x123 + x234) / 2
y1234 := (y123 + y234) / 2
// Try to approximate the full cubic curve by a single straight line
//------------------
dx := x4 - x1
dy := y4 - y1
d2 := fabs(((x2-x4)*dy - (y2-y4)*dx))
d3 := fabs(((x3-x4)*dy - (y3-y4)*dx))
switch {
case d2 <= CurveCollinearityEpsilon && d3 <= CurveCollinearityEpsilon:
// All collinear OR p1==p4
//----------------------
k := dx*dx + dy*dy
if k == 0 {
d2 = squareDistance(x1, y1, x2, y2)
d3 = squareDistance(x4, y4, x3, y3)
} else {
k = 1 / k
da1 := x2 - x1
da2 := y2 - y1
d2 = k * (da1*dx + da2*dy)
da1 = x3 - x1
da2 = y3 - y1
d3 = k * (da1*dx + da2*dy)
if d2 > 0 && d2 < 1 && d3 > 0 && d3 < 1 {
// Simple collinear case, 1---2---3---4
// We can leave just two endpoints
return
}
if d2 <= 0 {
d2 = squareDistance(x2, y2, x1, y1)
} else if d2 >= 1 {
d2 = squareDistance(x2, y2, x4, y4)
} else {
d2 = squareDistance(x2, y2, x1+d2*dx, y1+d2*dy)
}
if d3 <= 0 {
d3 = squareDistance(x3, y3, x1, y1)
} else if d3 >= 1 {
d3 = squareDistance(x3, y3, x4, y4)
} else {
d3 = squareDistance(x3, y3, x1+d3*dx, y1+d3*dy)
}
}
if d2 > d3 {
if d2 < distanceToleranceSquare {
v.LineTo(x2, y2)
return
}
} else {
if d3 < distanceToleranceSquare {
v.LineTo(x3, y3)
return
}
}
break
case d2 <= CurveCollinearityEpsilon && d3 > CurveCollinearityEpsilon:
// p1,p2,p4 are collinear, p3 is significant
//----------------------
if d3*d3 <= distanceToleranceSquare*(dx*dx+dy*dy) {
if angleTolerance < CurveAngleToleranceEpsilon {
v.LineTo(x23, y23)
return
}
// Angle Condition
//----------------------
da1 := fabs(atan2(y4-y3, x4-x3) - atan2(y3-y2, x3-x2))
if da1 >= math.Pi {
da1 = 2*math.Pi - da1
}
if da1 < angleTolerance {
v.LineTo(x2, y2)
v.LineTo(x3, y3)
return
}
if cuspLimit != 0.0 {
if da1 > cuspLimit {
v.LineTo(x3, y3)
return
}
}
}
break
case d2 > CurveCollinearityEpsilon && d3 <= CurveCollinearityEpsilon:
// p1,p3,p4 are collinear, p2 is significant
//----------------------
if d2*d2 <= distanceToleranceSquare*(dx*dx+dy*dy) {
if angleTolerance < CurveAngleToleranceEpsilon {
v.LineTo(x23, y23)
return
}
// Angle Condition
//----------------------
da1 := fabs(atan2(y3-y2, x3-x2) - atan2(y2-y1, x2-x1))
if da1 >= math.Pi {
da1 = 2*math.Pi - da1
}
if da1 < angleTolerance {
v.LineTo(x2, y2)
v.LineTo(x3, y3)
return
}
if cuspLimit != 0.0 {
if da1 > cuspLimit {
v.LineTo(x2, y2)
return
}
}
}
break
case d2 > CurveCollinearityEpsilon && d3 > CurveCollinearityEpsilon:
// Regular case
//-----------------
if (d2+d3)*(d2+d3) <= distanceToleranceSquare*(dx*dx+dy*dy) {
// If the curvature doesn't exceed the distanceTolerance value
// we tend to finish subdivisions.
//----------------------
if angleTolerance < CurveAngleToleranceEpsilon {
v.LineTo(x23, y23)
return
}
// Angle & Cusp Condition
//----------------------
k := atan2(y3-y2, x3-x2)
da1 := fabs(k - atan2(y2-y1, x2-x1))
da2 := fabs(atan2(y4-y3, x4-x3) - k)
if da1 >= math.Pi {
da1 = 2*math.Pi - da1
}
if da2 >= math.Pi {
da2 = 2*math.Pi - da2
}
if da1+da2 < angleTolerance {
// Finally we can stop the recursion
//----------------------
v.LineTo(x23, y23)
return
}
if cuspLimit != 0.0 {
if da1 > cuspLimit {
v.LineTo(x2, y2)
return
}
if da2 > cuspLimit {
v.LineTo(x3, y3)
return
}
}
}
break
}
// Continue subdivision
//----------------------
recursiveCubicBezier(v, x1, y1, x12, y12, x123, y123, x1234, y1234, level+1, distanceToleranceSquare, angleTolerance, cuspLimit)
recursiveCubicBezier(v, x1234, y1234, x234, y234, x34, y34, x4, y4, level+1, distanceToleranceSquare, angleTolerance, cuspLimit)
}

View File

@ -0,0 +1,337 @@
package draw2d
import (
"exp/draw"
"image"
"freetype-go.googlecode.com/hg/freetype/raster"
)
type FillRule int
const (
FillRuleEvenOdd FillRule = iota
FillRuleWinding
)
type Cap int
const (
RoundCap Cap = iota
ButtCap
SquareCap
)
type Join int
const (
BevelJoin Join = iota
RoundJoin
MiterJoin
)
type GraphicContext struct {
PaintedImage *image.RGBA
rasterizer *raster.Rasterizer
current *contextStack
}
type contextStack struct {
path *Path
lineWidth float
dash []float
dashOffset float
strokeColor image.Color
fillColor image.Color
fillRule FillRule
cap Cap
join Join
previous *contextStack
}
/**
* Create a new Graphic context from an image
*/
func NewGraphicContext(pi *image.RGBA) *GraphicContext {
gc := new(GraphicContext)
gc.PaintedImage = pi
width, height := gc.PaintedImage.Bounds().Dx(), gc.PaintedImage.Bounds().Dy()
gc.rasterizer = raster.NewRasterizer(width, height)
gc.current = new(contextStack)
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.path = new(Path)
return gc
}
func (gc *GraphicContext) Clear() {
width, height := gc.PaintedImage.Bounds().Dx(), gc.PaintedImage.Bounds().Dy()
gc.ClearRect(0, 0, width, height)
}
func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) {
imageColor := image.NewColorImage(gc.current.fillColor)
draw.Draw(gc.PaintedImage, gc.PaintedImage.Bounds(), imageColor, image.ZP)
}
func (gc *GraphicContext) SetStrokeColor(c image.Color) {
gc.current.strokeColor = c
}
func (gc *GraphicContext) SetFillColor(c image.Color) {
gc.current.fillColor = c
}
func (gc *GraphicContext) SetFillRule(f FillRule) {
gc.current.fillRule = f
}
func (gc *GraphicContext) SetLineWidth(lineWidth float) {
gc.current.lineWidth = lineWidth
}
func (gc *GraphicContext) SetLineCap(cap Cap) {
gc.current.cap = cap
}
func (gc *GraphicContext) SetLineJoin(join Join) {
gc.current.join = join
}
func (gc *GraphicContext) SetLineDash(dash []float, dashOffset float) {
gc.current.dash = dash
gc.current.dashOffset = dashOffset
}
func (gc *GraphicContext) Save() {
context := new(contextStack)
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.previous = gc.current
gc.current = context
}
func (gc *GraphicContext) Restore() {
if gc.current.previous != nil {
oldContext := gc.current
gc.current = gc.current.previous
oldContext.previous = nil
}
}
func (gc *GraphicContext) BeginPath() {
gc.current.path = new(Path)
}
func (gc *GraphicContext) MoveTo(x, y float) {
gc.current.path.MoveTo(x, y)
}
func (gc *GraphicContext) RMoveTo(dx, dy float) {
gc.current.path.RMoveTo(dx, dy)
}
func (gc *GraphicContext) LineTo(x, y float) {
gc.current.path.LineTo(x, y)
}
func (gc *GraphicContext) RLineTo(dx, dy float) {
gc.current.path.RLineTo(dx, dy)
}
func (gc *GraphicContext) Rect(x1, y1, x2, y2 float) {
gc.current.path.Rect(x1, y1, x2, y2)
}
func (gc *GraphicContext) RRect(dx1, dy1, dx2, dy2 float) {
gc.current.path.RRect(dx1, dy1, dx2, dy2)
}
func (gc *GraphicContext) QuadCurveTo(cx, cy, x, y float) {
gc.current.path.QuadCurveTo(cx, cy, x, y)
}
func (gc *GraphicContext) RQuadCurveTo(dcx, dcy, dx, dy float) {
gc.current.path.RQuadCurveTo(dcx, dcy, dx, dy)
}
func (gc *GraphicContext) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float) {
gc.current.path.CubicCurveTo(cx1, cy1, cx2, cy2, x, y)
}
func (gc *GraphicContext) RCubicCurveTo(dcx1, dcy1, dcx2, dcy2, dx, dy float) {
gc.current.path.RCubicCurveTo(dcx1, dcy1, dcx2, dcy2, dx, dy)
}
func (gc *GraphicContext) ArcTo(cx, cy, rx, ry, startAngle, angle float) {
gc.current.path.ArcTo(cx, cy, rx, ry, startAngle, angle)
}
func (gc *GraphicContext) RArcTo(dcx, dcy, rx, ry, startAngle, angle float) {
gc.current.path.RArcTo(dcx, dcy, rx, ry, startAngle, angle)
}
func (gc *GraphicContext) Close() {
gc.current.path.Close()
}
func (gc *GraphicContext) paint(color image.Color) {
painter := raster.NewRGBAPainter(gc.PaintedImage)
painter.SetColor(color)
gc.rasterizer.Rasterize(painter)
gc.rasterizer.Clear()
gc.current.path = new(Path)
}
func (gc *GraphicContext) Stroke(paths ...*Path) {
paths = append(paths, gc.current.path)
rasterPath := tracePath(gc.current.dash, gc.current.dashOffset, paths...)
gc.rasterizer.UseNonZeroWinding = true
gc.rasterizer.AddStroke(*rasterPath, raster.Fix32(gc.current.lineWidth*256), gc.current.cap.capper(), gc.current.join.joiner())
gc.paint(gc.current.strokeColor)
}
func (gc *GraphicContext) Fill(paths ...*Path) {
paths = append(paths, gc.current.path)
rasterPath := tracePath(nil, 0, paths...)
gc.rasterizer.UseNonZeroWinding = gc.current.fillRule.fillRule()
gc.rasterizer.AddPath(*rasterPath)
gc.paint(gc.current.fillColor)
}
func (gc *GraphicContext) FillStroke(paths ...*Path) {
paths = append(paths, gc.current.path)
rasterPath := tracePath(nil, 0, paths...)
gc.rasterizer.UseNonZeroWinding = gc.current.fillRule.fillRule()
gc.rasterizer.AddPath(*rasterPath)
gc.paint(gc.current.fillColor)
if gc.current.dash != nil {
rasterPath = tracePath(gc.current.dash, gc.current.dashOffset, paths...)
}
gc.rasterizer.UseNonZeroWinding = true
gc.rasterizer.AddStroke(*rasterPath, raster.Fix32(gc.current.lineWidth*256), gc.current.cap.capper(), gc.current.join.joiner())
gc.paint(gc.current.strokeColor)
}
func (f FillRule) fillRule() bool {
switch f {
case FillRuleEvenOdd:
return false
case FillRuleWinding:
return true
}
return false
}
func (c Cap) capper() raster.Capper {
switch c {
case RoundCap:
return raster.RoundCapper
case ButtCap:
return raster.ButtCapper
case SquareCap:
return raster.SquareCapper
}
return raster.RoundCapper
}
func (j Join) joiner() raster.Joiner {
switch j {
case RoundJoin:
return raster.RoundJoiner
case BevelJoin:
return raster.BevelJoiner
}
return raster.RoundJoiner
}
type PathAdapter struct {
path *raster.Path
x, y, distance float
dash []float
currentDash int
dashOffset float
}
func tracePath(dash []float, dashOffset float, paths ...*Path) *raster.Path {
var adapter PathAdapter
if dash != nil && len(dash) > 0 {
adapter.dash = dash
} else {
adapter.dash = nil
}
adapter.currentDash = 0
adapter.dashOffset = dashOffset
adapter.path = new(raster.Path)
for _, path := range paths {
path.TraceLine(&adapter)
}
return adapter.path
}
func floatToPoint(x, y float) raster.Point {
return raster.Point{raster.Fix32(x * 256), raster.Fix32(y * 256)}
}
func (p *PathAdapter) MoveTo(x, y float) {
p.path.Start(floatToPoint(x, y))
p.x, p.y = x, y
p.distance = p.dashOffset
p.currentDash = 0
}
func (p *PathAdapter) LineTo(x, y float) {
if p.dash != nil {
rest := p.dash[p.currentDash] - p.distance
for rest < 0 {
p.distance = p.distance - p.dash[p.currentDash]
p.currentDash = (p.currentDash + 1) % len(p.dash)
rest = p.dash[p.currentDash] - p.distance
}
d := distance(p.x, p.y, x, y)
for d >= rest {
k := rest / d
lx := p.x + k*(x-p.x)
ly := p.y + k*(y-p.y)
if p.currentDash%2 == 0 {
// line
p.path.Add1(floatToPoint(lx, ly))
} else {
// gap
p.path.Start(floatToPoint(lx, ly))
}
d = d - rest
p.x, p.y = lx, ly
p.currentDash = (p.currentDash + 1) % len(p.dash)
rest = p.dash[p.currentDash]
}
p.distance = d
if p.currentDash%2 == 0 {
p.path.Add1(floatToPoint(x, y))
} else {
p.path.Start(floatToPoint(x, y))
}
if p.distance >= p.dash[p.currentDash] {
p.distance = p.distance - p.dash[p.currentDash]
p.currentDash = (p.currentDash + 1) % len(p.dash)
}
} else {
p.path.Add1(floatToPoint(x, y))
}
p.x, p.y = x, y
}

View File

@ -0,0 +1,41 @@
package draw2d
import (
"math"
)
func fabs(x float) float {
switch {
case x < 0:
return -x
case x == 0:
return 0 // return correctly fabs(-0)
}
return x
}
func cos(f float) float {
return float(math.Cos(float64(f)))
}
func sin(f float) float {
return float(math.Sin(float64(f)))
}
func acos(f float) float {
return float(math.Acos(float64(f)))
}
func atan2(x, y float) float {
return float(math.Atan2(float64(x), float64(y)))
}
func distance(x1, y1, x2, y2 float) float {
dx := x2 - x1
dy := y2 - y1
return float(math.Sqrt(float64(dx*dx + dy*dy)))
}
func squareDistance(x1, y1, x2, y2 float) float {
dx := x2 - x1
dy := y2 - y1
return dx*dx + dy*dy
}

View File

@ -0,0 +1,222 @@
package draw2d
import (
"fmt"
"math"
)
type PathCmd int
const (
MoveTo PathCmd = iota
LineTo
QuadCurveTo
CubicCurveTo
ArcTo
Close
)
type Path struct {
commands []PathCmd
vertices []float
x, y float
}
type LineTracer interface {
MoveTo(x, y float)
LineTo(x, y float)
}
func (p *Path) appendToPath(cmd PathCmd, vertices ...float) {
p.commands = append(p.commands, cmd)
p.vertices = append(p.vertices, vertices...)
}
func (src *Path) Copy() (dest *Path) {
dest = new(Path)
dest.commands = make([]PathCmd, len(src.commands))
copy(dest.commands, src.commands)
dest.vertices = make([]float, len(src.vertices))
copy(dest.vertices, src.vertices)
return dest
}
func (p *Path) LastPoint() (x, y float) {
return p.x, p.y
}
func (p *Path) Close() *Path {
p.appendToPath(Close)
return p
}
func (p *Path) MoveTo(x, y float) *Path {
p.appendToPath(MoveTo, x, y)
p.x = x
p.y = y
return p
}
func (p *Path) RMoveTo(dx, dy float) *Path {
x, y := p.LastPoint()
p.MoveTo(x+dx, y+dy)
return p
}
func (p *Path) LineTo(x, y float) *Path {
p.appendToPath(LineTo, x, y)
p.x = x
p.y = y
return p
}
func (p *Path) RLineTo(dx, dy float) *Path {
x, y := p.LastPoint()
p.LineTo(x+dx, y+dy)
return p
}
func (p *Path) Rect(x1, y1, x2, y2 float) *Path {
w, h := x2-x1, y2-y1
if len(p.commands) > 0 {
p.LineTo(x1, y1)
} else {
p.MoveTo(x1, y1)
}
p.LineTo(x1+w, y1)
p.LineTo(x1+w, y1+h)
p.LineTo(x1, y1+h)
p.LineTo(x1, y1)
return p
}
func (p *Path) RRect(dx1, dy1, dx2, dy2 float) *Path {
x, y := p.LastPoint()
p.Rect(x+dx1, y+dy1, x+dx2, y+dy2)
return p
}
func (p *Path) QuadCurveTo(cx, cy, x, y float) *Path {
p.appendToPath(QuadCurveTo, cx, cy, x, y)
p.x = x
p.y = y
return p
}
func (p *Path) RQuadCurveTo(dcx, dcy, dx, dy float) *Path {
x, y := p.LastPoint()
p.RQuadCurveTo(x+dcx, y+dcy, x+dx, y+dy)
return p
}
func (p *Path) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float) *Path {
p.appendToPath(CubicCurveTo, cx1, cy1, cx2, cy2, x, y)
p.x = x
p.y = y
return p
}
func (p *Path) RCubicCurveTo(dcx1, dcy1, dcx2, dcy2, dx, dy float) *Path {
x, y := p.LastPoint()
p.RCubicCurveTo(x+dcx1, y+dcy1, x+dcx2, y+dcy2, x+dx, y+dy)
return p
}
func (p *Path) ArcTo(cx, cy, rx, ry, startAngle, angle float) *Path {
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 + cos(startAngle)*rx
startY := cy + sin(startAngle)*ry
if len(p.commands) > 0 {
p.LineTo(startX, startY)
} else {
p.MoveTo(startX, startY)
}
p.appendToPath(ArcTo, cx, cy, rx, ry, startAngle, angle)
p.x = cx + cos(endAngle)*rx
p.y = cy + sin(endAngle)*ry
return p
}
func (p *Path) RArcTo(dcx, dcy, rx, ry, startAngle, angle float) *Path {
x, y := p.LastPoint()
p.RArcTo(x+dcx, y+dcy, rx, ry, startAngle, angle)
return p
}
func (p *Path) TraceLine(tracer LineTracer) {
j := 0
x, y := 0.0, 0.0
firstX, firstY := x, y
if len(p.commands) > 0 {
if p.commands[0] == MoveTo {
firstX, firstY = p.vertices[0], p.vertices[1]
}
}
for _, cmd := range p.commands {
switch cmd {
case MoveTo:
tracer.MoveTo(p.vertices[j], p.vertices[j+1])
x, y = p.vertices[j], p.vertices[j+1]
firstX, firstY = x, y
j = j + 2
case LineTo:
tracer.LineTo(p.vertices[j], p.vertices[j+1])
x, y = p.vertices[j], p.vertices[j+1]
j = j + 2
case QuadCurveTo:
quadraticBezier(tracer, x, y, p.vertices[j], p.vertices[j+1], p.vertices[j+2], p.vertices[j+3], 1.0, 0.0)
x, y = p.vertices[j+2], p.vertices[j+3]
j = j + 4
case CubicCurveTo:
cubicBezier(tracer, x, y, p.vertices[j], p.vertices[j+1], p.vertices[j+2], p.vertices[j+3], p.vertices[j+4], p.vertices[j+5], 1.0, 0.0, 0.0)
x, y = p.vertices[j+4], p.vertices[j+5]
j = j + 6
case ArcTo:
arc(tracer, p.vertices[j], p.vertices[j+1], p.vertices[j+2], p.vertices[j+3], p.vertices[j+4], p.vertices[j+5], 1)
j = j + 6
case Close:
tracer.LineTo(firstX, firstY)
x, y = firstX, firstY
}
}
}
func (p *Path) String() string {
s := ""
j := 0
for _, cmd := range p.commands {
switch cmd {
case MoveTo:
s += fmt.Sprintf("MoveTo: %f, %f\n", p.vertices[j], p.vertices[j+1])
j = j + 2
case LineTo:
s += fmt.Sprintf("LineTo: %f, %f\n", p.vertices[j], p.vertices[j+1])
j = j + 2
case QuadCurveTo:
s += fmt.Sprintf("QuadCurveTo: %f, %f, %f, %f\n", p.vertices[j], p.vertices[j+1], p.vertices[j+2], p.vertices[j+3])
j = j + 4
case CubicCurveTo:
s += fmt.Sprintf("CubicCurveTo: %f, %f, %f, %f, %f, %f\n", p.vertices[j], p.vertices[j+1], p.vertices[j+2], p.vertices[j+3], p.vertices[j+4], p.vertices[j+5])
j = j + 6
case ArcTo:
s += fmt.Sprintf("ArcTo: %f, %f, %f, %f, %f, %f\n", p.vertices[j], p.vertices[j+1], p.vertices[j+2], p.vertices[j+3], p.vertices[j+4], p.vertices[j+5])
j = j + 6
case Close:
s += "Close\n"
}
}
return s
}