diff --git a/graphics.go b/graphics.go index f2ce5f4..eab6881 100644 --- a/graphics.go +++ b/graphics.go @@ -67,7 +67,7 @@ func (s *Sprite) Draw(t Target) { // IMDraw is an immediate-like-mode shape drawer. // -// TODO: mode doc +// TODO: doc type IMDraw struct { points []point opts point @@ -85,7 +85,6 @@ type point struct { col NRGBA pic Vec in float64 - width float64 precision int endshape EndShape } @@ -94,8 +93,11 @@ type point struct { type EndShape int const ( - // SharpEndShape is a square end shape. - SharpEndShape EndShape = iota + // NoEndShape leaves a line point with no special end shape. + NoEndShape EndShape = iota + + // SharpEndShape is a sharp triangular end shape. + SharpEndShape // RoundEndShape is a circular end shape. RoundEndShape @@ -140,15 +142,14 @@ func (imd *IMDraw) Draw(t Target) { // the position. func (imd *IMDraw) Push(pts ...Vec) { for _, pt := range pts { - imd.pushPt(pt) + imd.pushPt(pt, imd.opts) } } -func (imd *IMDraw) pushPt(pt Vec) { - point := imd.opts - point.pos = imd.matrix.Project(pt) - point.col = imd.mask.Mul(point.col) - imd.points = append(imd.points, point) +func (imd *IMDraw) pushPt(pos Vec, pt point) { + pt.pos = imd.matrix.Project(pos) + pt.col = imd.mask.Mul(pt.col) + imd.points = append(imd.points, pt) } // Color sets the color of the next Pushed points. @@ -166,13 +167,6 @@ func (imd *IMDraw) Intensity(in float64) { imd.opts.in = in } -// Width sets the with property of the next Pushed points. -// -// Note that this property does not apply to filled shapes. -func (imd *IMDraw) Width(w float64) { - imd.opts.width = w -} - // Precision sets the curve/circle drawing precision of the next Pushed points. // // It is the number of segments per 360 degrees. @@ -207,88 +201,368 @@ func (imd *IMDraw) MakePicture(p Picture) TargetPicture { return imd.batch.MakePicture(p) } -// FillConvexPolygon takes all points Pushed into the IM's queue and fills the convex polygon formed -// by them. +// Polygon draws a polygon from the Pushed points. If the thickness is 0, the convex polygon will be +// filled. Otherwise, an outline of the specified thickness will be drawn. The outline does not have +// to be convex. // -// The polygon does not need to be exactly convex. The way it's drawn is that for each two adjacent -// points, a triangle is constructed from those two points and the first Pushed point. You can use -// this property to draw specific concave polygons. -func (imd *IMDraw) FillConvexPolygon() { +// Note, that the filled polygon does not have to be strictly convex. The way it's drawn is that a +// triangle is drawn between each two adjacent points and the first Pushed point. You can use this +// property to draw certain kinds of concave polygons. +func (imd *IMDraw) Polygon(thickness float64) { + if thickness == 0 { + imd.fillPolygon() + } else { + imd.polyline(thickness, true) + } +} + +// Circle draws a circle of the specified radius around each Pushed point. If the thickness is 0, +// the circle will be filled, otherwise a circle outline of the specified thickness will be drawn. +func (imd *IMDraw) Circle(radius, thickness float64) { + if thickness == 0 { + imd.fillEllipseArc(V(radius, radius), 0, 2*math.Pi) + } else { + imd.outlineEllipseArc(V(radius, radius), 0, 2*math.Pi, thickness, false) + } +} + +// CircleArc draws a circle arc of the specified radius around each Pushed point. If the thickness +// is 0, the arc will be filled, otherwise will be outlined. The arc starts at the low angle and +// continues to the high angle. If low high { + orientation = -1.0 + } + + switch pt.endshape { + case NoEndShape: + // nothing + case SharpEndShape: + thick := X(thickness / 2).Rotated(normalLow) + imd.pushPt(lowCenter+thick, pt) + imd.pushPt(lowCenter-thick, pt) + imd.pushPt(lowCenter-thick.Rotated(math.Pi/2*orientation), pt) + imd.fillPolygon() + thick = X(thickness / 2).Rotated(normalHigh) + imd.pushPt(highCenter+thick, pt) + imd.pushPt(highCenter-thick, pt) + imd.pushPt(highCenter+thick.Rotated(math.Pi/2*orientation), pt) + imd.fillPolygon() + case RoundEndShape: + imd.pushPt(lowCenter, pt) + imd.fillEllipseArc(V(thickness, thickness)/2, normalLow, normalLow-math.Pi*orientation) + imd.pushPt(highCenter, pt) + imd.fillEllipseArc(V(thickness, thickness)/2, normalHigh, normalHigh+math.Pi*orientation) + } + } + } +} + +func (imd *IMDraw) polyline(thickness float64, closed bool) { + points := imd.getAndClearPoints() + + // filter identical adjacent points + filtered := points[:0] + for i := 0; i < len(points); i++ { + if closed || i+1 < len(points) { + j := (i + 1) % len(points) + if points[i].pos != points[j].pos { + filtered = append(filtered, points[i]) + } + } + } + points = filtered + + if len(points) < 2 { + return + } + + // first point + j, i := 0, 1 + normal := (points[i].pos - points[j].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) + + if !closed { + switch points[j].endshape { + case NoEndShape: + // nothing + case SharpEndShape: + imd.pushPt(points[j].pos+normal, points[j]) + imd.pushPt(points[j].pos-normal, points[j]) + imd.pushPt(points[j].pos+normal.Rotated(math.Pi/2), points[j]) + imd.fillPolygon() + case RoundEndShape: + imd.pushPt(points[j].pos, points[j]) + imd.fillEllipseArc(V(thickness, thickness)/2, normal.Angle(), normal.Angle()+math.Pi) + } + } + + imd.pushPt(points[j].pos+normal, points[j]) + imd.pushPt(points[j].pos-normal, points[j]) + + // middle points + for i := 0; i < len(points); i++ { + j, k := i+1, i+2 + + closing := false + if j >= len(points) { + if !closed { + break + } + j %= len(points) + closing = true + } + if k >= len(points) { + k %= len(points) + } + + ijNormal := (points[j].pos - points[i].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) + jkNormal := (points[k].pos - points[j].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) + + orientation := 1.0 + if ijNormal.Cross(jkNormal) > 0 { + orientation = -1.0 + } + + imd.pushPt(points[j].pos-ijNormal, points[j]) + imd.pushPt(points[j].pos+ijNormal, points[j]) + imd.fillPolygon() + + switch points[j].endshape { + case NoEndShape: + // nothing + case SharpEndShape: + imd.pushPt(points[j].pos, points[j]) + imd.pushPt(points[j].pos+ijNormal.Scaled(orientation), points[j]) + imd.pushPt(points[j].pos+jkNormal.Scaled(orientation), points[j]) + imd.fillPolygon() + case RoundEndShape: + imd.pushPt(points[j].pos, points[j]) + imd.fillEllipseArc(V(thickness, thickness)/2, ijNormal.Angle(), ijNormal.Angle()-math.Pi) + imd.pushPt(points[j].pos, points[j]) + imd.fillEllipseArc(V(thickness, thickness)/2, jkNormal.Angle(), jkNormal.Angle()+math.Pi) + } + + if !closing { + imd.pushPt(points[j].pos+jkNormal, points[j]) + imd.pushPt(points[j].pos-jkNormal, points[j]) + } + } + + // last point + i, j = len(points)-2, len(points)-1 + normal = (points[j].pos - points[i].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) + + imd.pushPt(points[j].pos-normal, points[j]) + imd.pushPt(points[j].pos+normal, points[j]) + imd.fillPolygon() + + if !closed { + switch points[j].endshape { + case NoEndShape: + // nothing + case SharpEndShape: + imd.pushPt(points[j].pos+normal, points[j]) + imd.pushPt(points[j].pos-normal, points[j]) + imd.pushPt(points[j].pos+normal.Rotated(-math.Pi/2), points[j]) + imd.fillPolygon() + case RoundEndShape: + imd.pushPt(points[j].pos, points[j]) + imd.fillEllipseArc(V(thickness, thickness)/2, normal.Angle(), normal.Angle()-math.Pi) + } } }