diff --git a/draw2d/.project b/draw2d/.project new file mode 100644 index 0000000..6a8aaf2 --- /dev/null +++ b/draw2d/.project @@ -0,0 +1,17 @@ + + + draw2d + + + + + + com.googlecode.goclipse.goBuilder + + + + + + goclipse.goNature + + diff --git a/draw2d/src/cmd/testdraw2d.go b/draw2d/src/cmd/testdraw2d.go new file mode 100644 index 0000000..b9ad1e0 --- /dev/null +++ b/draw2d/src/cmd/testdraw2d.go @@ -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) +} + +/* + +*/ +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))) +} +/* + +*/ +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) +} +/* + +*/ +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) +} +/* + +*/ +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) +} + +/* + +*/ +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) +} + + +/* + +*/ +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) +} + +/* + +*/ +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() +} diff --git a/draw2d/src/cmd/testpath.go b/draw2d/src/cmd/testpath.go new file mode 100644 index 0000000..7071341 --- /dev/null +++ b/draw2d/src/cmd/testpath.go @@ -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) +} diff --git a/draw2d/src/pkg/draw2d/arc.go b/draw2d/src/pkg/draw2d/arc.go new file mode 100644 index 0000000..7c5b97e --- /dev/null +++ b/draw2d/src/pkg/draw2d/arc.go @@ -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) + } +} diff --git a/draw2d/src/pkg/draw2d/curves.go b/draw2d/src/pkg/draw2d/curves.go new file mode 100644 index 0000000..6c385a2 --- /dev/null +++ b/draw2d/src/pkg/draw2d/curves.go @@ -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) + +} diff --git a/draw2d/src/pkg/draw2d/draw2d.go b/draw2d/src/pkg/draw2d/draw2d.go new file mode 100644 index 0000000..8355152 --- /dev/null +++ b/draw2d/src/pkg/draw2d/draw2d.go @@ -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 +} diff --git a/draw2d/src/pkg/draw2d/math.go b/draw2d/src/pkg/draw2d/math.go new file mode 100644 index 0000000..c30a234 --- /dev/null +++ b/draw2d/src/pkg/draw2d/math.go @@ -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 +} diff --git a/draw2d/src/pkg/draw2d/path.go b/draw2d/src/pkg/draw2d/path.go new file mode 100644 index 0000000..2fa3e5c --- /dev/null +++ b/draw2d/src/pkg/draw2d/path.go @@ -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 +}