diff --git a/docs/src/polygons.md b/docs/src/polygons.md index 1c944d13..ddfd4d9e 100644 --- a/docs/src/polygons.md +++ b/docs/src/polygons.md @@ -6,7 +6,7 @@ A polygon is an ordered collection of Points stored in an array. A path is a sequence of one or more straight and curved (circular arc or Bézier curve) segments. Paths can consist of subpaths. Luxor maintains a 'current path', to which you can add lines and curves until you finish with a stroke or fill instruction. -Luxor also provides a Bézier-path type, which is an array of four-point tuples, each of which is a Bézier curve section. +Luxor also provides a BezierPath type, which is an array of four-point tuples, each of which is a Bézier cubic curve section. ```@setup polytable using Luxor @@ -20,13 +20,15 @@ polygon ngon() polysmooth() poly() isi - ngonside() - prettypoly() polyperimeter() polysplit() - star() - polysmooth() polyarea() polyportion() - offsetpoly() - - polycentroid() polyremainder() -- polyfit() - - boundingbox() polysortbyangle() +- polyfit() - - boundingbox() polysortbyangle() - hyptrochoid() - - - polysortbydistance() - epitrochoid() - - - polyintersections() path getpath() pathtopoly() - - - - getpathflat() - - - - bezierpath makebezierpath() pathtobezierpaths() drawbezierpath() - - -- pathtobezierpaths() bezierpathtopoly() - - - +- pathtobezierpaths() bezierpathtopoly() brush() - - +- BezierPath() - - - - +- BezierPathSegment() - - - - """)) # find the widths of the columns @@ -600,10 +602,9 @@ getpathflat Use the `makebezierpath()` and `drawbezierpath()` functions to make and draw Bézier paths, and `pathtobezierpaths()` to convert the current path to an array of Bézier paths. -A Bézier path is a sequence of Bézier curve segments; each curve segment is defined by four points: two end points and their control points. +A BezierPath type contains a sequence of `BezierPathSegment`s; each curve segment is defined by four points: two end points and their control points. ``` -NTuple{4,Point}[ (Point(-129.904, 75.0), # start point Point(-162.38, 18.75), # ^ control point Point(-64.9519, -150.0), # v control point @@ -618,7 +619,6 @@ NTuple{4,Point}[ Point(-129.904, 75.0) ), ... - ] ``` Bézier paths are different from ordinary paths in that they don't usually contain straight line segments. However, by setting the two control points to be the same as their matching start/end points, you create straight line sections. @@ -714,7 +714,7 @@ nothing # hide ``` ![path to polygon](assets/figures/bezierpathtopoly.png) -You can convert the current path to an array of Bézier paths using the `pathtobezierpaths()` function. +You can convert the current path to an array of BezierPaths using the `pathtobezierpaths()` function. In the next example, the letter "a" is placed at the current position (set by `move()`) and then converted to an array of Bézier paths. Each Bézier path is drawn first of all in gray, then the control points of segment are drawn (in orange) showing how they affect the curvature. @@ -754,22 +754,41 @@ nothing # hide ![path to polygon](assets/figures/pathtobezierpaths.png) +### Brush strokes + +```@example +using Luxor # hide +Drawing(600, 250, "assets/figures/brush.png") # hide +origin() # hide +background("white") # hide +srand(42) # hide +sethue("black") # hide +brush(Point(-250, 0), Point(250, 0), 20, + strokes=15, + tidystart=true, + lowhandle=0.0, + twist=-5, + lowhandle=-0.5, + highhandle=0.5) +finish() # hide +nothing # hide +``` + +![brush](assets/figures/brush.png) + ```@docs bezier -bezier′ -bezier′′ beziercurvature bezierfrompoints bezierpathtopoly bezierstroke beziertopoly -brush drawbezierpath -Luxor.findbeziercontrolpoints makebezierpath pathtobezierpaths setbezierhandles shiftbezierhandles +brush ``` ## Polygon information diff --git a/docs/src/simplegraphics.md b/docs/src/simplegraphics.md index 86bb57e4..8f4bfd8e 100644 --- a/docs/src/simplegraphics.md +++ b/docs/src/simplegraphics.md @@ -922,6 +922,8 @@ boxdiagonal boxwidth boxheight intersectboundingboxes +boxtop +boxbottom ``` ## Miscellaneous diff --git a/docs/src/transforms.md b/docs/src/transforms.md index 72c2bef9..31f8d10a 100644 --- a/docs/src/transforms.md +++ b/docs/src/transforms.md @@ -73,13 +73,12 @@ nothing # hide To return home after many changes, you can use `setmatrix([1, 0, 0, 1, 0, 0])` to reset the matrix to the default. `origin()` resets the matrix then moves the origin to the center of the page. -`rescale()` is just a convenient utility function for linear interpolation, also called a "lerp". +`rescale()` is a convenient utility function for linear interpolation, also called a "lerp". ```@docs scale rotate translate -rescale ``` # Matrices and transformations diff --git a/src/BoundingBox.jl b/src/BoundingBox.jl index cca5c35b..74e01ade 100644 --- a/src/BoundingBox.jl +++ b/src/BoundingBox.jl @@ -168,10 +168,24 @@ boxdiagonal(bb::BoundingBox) = hypot(boxwidth(bb), boxheight(bb)) """ boxaspectratio(bb::BoundingBox) -Return the asepct ratio (the height divided by the width) of bounding box `bb`. +Return the aspect ratio (the height divided by the width) of bounding box `bb`. """ boxaspectratio(bb::BoundingBox) = boxheight(bb)/boxwidth(bb) +""" + boxtop(bb::BoundingBox) + +Return the top center point of bounding box `bb`. +""" +boxtop(bb::BoundingBox) = midpoint(bb.corner1, bb.corner2) - (0, boxheight(bb)/2) + +""" + boxbottom(bb::BoundingBox) + +Return the top center point of bounding box `bb`. +""" +boxbottom(bb::BoundingBox) = midpoint(bb.corner1, bb.corner2) + (0, boxheight(bb)/2) + """ convert(Point, bbox::BoundingBox) diff --git a/src/Luxor.jl b/src/Luxor.jl index 8b18ae4d..cc698a3b 100755 --- a/src/Luxor.jl +++ b/src/Luxor.jl @@ -64,7 +64,7 @@ export Drawing, currentdrawing, move, rmove, line, rule, rline, arrow, - BoundingBox, boundingbox, boxwidth, boxheight, boxdiagonal, boxaspectratio, + BoundingBox, boundingbox, boxwidth, boxheight, boxdiagonal, boxaspectratio, boxtop, boxbottom, circle, circlepath, ellipse, hypotrochoid, epitrochoid, squircle, center3pts, curve, arc, carc, arc2r, carc2r, spiral, sector, intersection2circles, diff --git a/src/bezierpath.jl b/src/bezierpath.jl index 2358c566..88b504e3 100644 --- a/src/bezierpath.jl +++ b/src/bezierpath.jl @@ -17,7 +17,7 @@ Base.next(bps::BezierPathSegment, s::Int) = bps[s], s+1 Base.done(bps::BezierPathSegment, s::Int) = (s > length(bps)) function Base.show(io::IO, bps::BezierPathSegment) - println(io, "p1 $(bps.p1) cp1 $(bps.cp1)") + println(io, "p1 $(bps.p1) cp1 $(bps.cp1)") println(io, "cp2 $(bps.cp2) p2 $(bps.p2)") end @@ -407,39 +407,6 @@ Given four points, return the Bezier curve that passes through all four points. """ bezierfrompoints(ptslist::Array{Point, 1}) = bezierfrompoints(ptslist...) -""" - bezierstroke(bps::BezierPathSegment, width=5.0) - -Create a BezierPath that replaces the single BezierPathSegment in `bps`. The -new BezierPath contains two segments which together define a shape. -""" -function bezierstroke(bps::BezierPathSegment, width=5.0) - newbezpath = BezierPath() - p1, cp1, cp2, p2 = bps - # find two points on the curve - # choose third and two thirds - ip1 = bezier(0.33, p1, cp1, cp2, p2) - ip2 = bezier(0.66, p1, cp1, cp2, p2) - # find slope of curve at those points - pt1 = bezier′(0.33, p1, cp1, cp2, p2) - pt2 = bezier′(0.66, p1, cp1, cp2, p2) - # find normals normal is 1/slope - slopeip1 = -1/(pt1.y/pt1.x) - slopeip2 = -1/(pt2.y/pt2.x) - # find perpendiculars on both sides - ipt1 = ip1 + polar(width, atan(slopeip1)) - ipt2 = ip2 + polar(width, atan(slopeip2)) - ipt3 = ip1 - polar(width, atan(slopeip1)) - ipt4 = ip2 - polar(width, atan(slopeip2)) - # make two new beziers, one on each side - result1 = bezierfrompoints(p1, ipt3, ipt4, p2) - result2 = bezierfrompoints(p1, ipt1, ipt2, p2) - push!(newbezpath, BezierPathSegment(p1, result1[2], result1[3], p2)) - push!(newbezpath, BezierPathSegment(p2, result2[3], result2[2], p1)) - - return newbezpath -end - """ bezierstroke(point1, point2, width=0.0) @@ -453,35 +420,51 @@ To draw it, use eg `drawbezierpath(..., :fill)`. """ function bezierstroke(p1::Point, p2::Point, width=0.0) bezpath = BezierPath() - # simple stroke starting ending at point if isapprox(width, 0.0) + # simple wavy stroke starting ending at point push!(bezpath, BezierPathSegment(p1, p1, p2, p2)) push!(bezpath, BezierPathSegment(p2, p2, p1, p1)) - # stroke with broad opening and closing + result = setbezierhandles.(bezpath, angles=[0.1, -0.1],handles=[0.3, 0.3]) else + # stroke with broad opening and closing + # make the paths, then give them some control power cp1 = perpendicular(p1, p2, -width) - push!(bezpath, BezierPathSegment(p1, p1, cp1, cp1)) + seg = BezierPathSegment(p1, p1, cp1, cp1) + adjustedseg = setbezierhandles(seg, angles=[0.1, -0.1], handles=[0.3, 0.3]) + push!(bezpath, adjustedseg) cp2 = perpendicular(p2, p1, width) - push!(bezpath, BezierPathSegment(cp1, cp1, cp2, cp2)) - push!(bezpath, BezierPathSegment(cp2, cp2, p2, p2)) + seg = BezierPathSegment(cp1, cp1, cp2, cp2) + adjustedseg = setbezierhandles(seg, angles=[0.1, -0.1], handles=[0.3, 0.3]) + push!(bezpath, adjustedseg) + + seg = BezierPathSegment(cp2, cp2, p2, p2) + adjustedseg = setbezierhandles(seg, angles=[0.1, -0.1], handles=[0.3, 0.3]) + push!(bezpath, adjustedseg) # TODO the segments should be collinear, perhaps? cp3 = perpendicular(p2, p1, -width) - push!(bezpath, BezierPathSegment(p2, p2, cp3, cp3)) + seg = BezierPathSegment(p2, p2, cp3, cp3) + adjustedseg = setbezierhandles(seg, angles=[0.1, -0.1], handles=[0.3, 0.3]) + push!(bezpath, adjustedseg) cp4 = perpendicular(p1, p2, width) - push!(bezpath, BezierPathSegment(cp3, cp3, cp4, cp4)) - - push!(bezpath, BezierPathSegment(cp4, cp4, p1, p1)) + seg = BezierPathSegment(cp3, cp3, cp4, cp4) + adjustedseg = setbezierhandles(seg, angles=[0.1, -0.1], handles=[0.3, 0.3]) + push!(bezpath, adjustedseg) + + seg = BezierPathSegment(cp4, cp4, p1, p1) + adjustedseg = setbezierhandles(seg, angles=[0.1, -0.1], handles=[0.3, 0.3]) + push!(bezpath, adjustedseg) + result = bezpath end - return bezpath + return result end """ setbezierhandles(bps::BezierPathSegment; - angles=[0.05, -0.1], - handles=[0.3, 0.3]) + angles = [0.05, -0.1], + handles = [0.3, 0.3]) Return a new Bezier path segment with new locations for the Bezier control points in the Bezier path segment `bps`. @@ -533,8 +516,8 @@ the values in `handles` modifies the lengths: 1 preserves the length, 0.5 halves the length of the handles, 2 doubles them. """ function shiftbezierhandles(bps::BezierPathSegment; - angles=[0.1, -0.1], - handles=[0.1, 0.1]) + angles = [0.1, -0.1], + handles = [0.1, 0.1]) p1, cp1, cp2, p2 = bps # find slope of curve at the end points spt1 = bezier′(0.0, p1, cp1, cp2, p2) @@ -558,74 +541,59 @@ end """ brush(pt1, pt2, width=10; - strokes=5, - minwidth=0.01, - maxwidth=0.03, - twist = -1, # -1 or 1 - randomopacity = true - ) + strokes=10, + minwidth=0.01, + maxwidth=0.03, + twist = -1, + lowhandle = 0.3, + highhandle = 0.7, + randomopacity = true, + tidystart = false, + action = :fill) -Draw a composite brush stroke made up of some randomized individual brush strokes. +Draw a composite brush stroke made up of some randomized individual filled +Bezier paths. + +!!! note -!!! There is a lot of randomness in this function. Results are unpredictable. """ function brush(pt1, pt2, width=10; - strokes=5, + strokes=10, minwidth=0.01, maxwidth=0.03, twist = -1, - randomopacity = true + lowhandle = 0.3, + highhandle = 0.7, + randomopacity = true, + tidystart = false, + action = :fill ) @layer begin sl = slope(pt1, pt2) n = norm(pt1, pt2) translate(pt1) rotate(sl - pi/2) - for j in linspace(-width/2, width/2, max(strokes, 2)) - shp = [O + (j, 0), O + (j, n)] - shp .+= Point(rand(-5:5), rand(-5:5)) - pbp = bezierstroke(shp[1], shp[2], rand(Bool) ? 0.0 : rand(minwidth:0.1:maxwidth)) + widthsteps = (maxwidth - minwidth)/10 + for j in 1:strokes + shiftedline = [O + (0, 0), O + (0, n)] + if tidystart + shiftedline .+= Point(rand(-width/2:width/2), 0) + else + shiftedline .+= Point(rand(-width/2:width/2), rand(-width/2:width/2)) + end + pbp = bezierstroke(shiftedline[1], + shiftedline[2], + rand(Bool) ? 0.0 : rand(minwidth:widthsteps:maxwidth) + ) for bps in pbp - nbpb = setbezierhandles(bps, - angles = [rand(minwidth:0.001:maxwidth), twist * rand(minwidth:0.001:maxwidth)], - handles = [rand(0.3:0.1:0.4, 2)...] + randomopacity ? setopacity(rand(0.3:0.1:0.9)) : setopacity(1.0) + nbpb = shiftbezierhandles(bps, + angles = [twist * rand(minwidth:widthsteps:maxwidth), -twist * rand(minwidth:widthsteps:maxwidth)], + handles = [rand(lowhandle:0.01:highhandle, 2)...] ) - randomopacity ? setopacity(rand()) : setopacity(1.0) - drawbezierpath(nbpb, :stroke, close=false) + drawbezierpath(nbpb, action, close=false) end end end end - -""" - brush(bpseg::BezierPathSegment, width=10; - strokes=5, - minwidth=0.01, - maxwidth=0.03, - twist = -1, # -1 or 1 - randomopacity = true - ) -""" -function brush(bpseg::BezierPathSegment, width=10; - strokes=5, - minwidth=0.01, - maxwidth=0.01, - twist = -1, - randomopacity = true - ) - @layer begin - # random start positions - for j in linspace(-width/2, width/2, max(strokes, 2)) - shiftedpts = [bpseg.p1 + (j, 0), bpseg.p2 + (j, 0)] - shiftedpts .+= Point(rand(-width/2:width/2), rand(-width/2:width/2)) - pbp = bezierstroke(BezierPathSegment(shiftedpts[1], bpseg.cp1, bpseg.cp2, shiftedpts[2]), - rand(Bool) ? 0.0 : rand(minwidth:0.1:maxwidth)) - npbp = shiftbezierhandles.(pbp, - angles = [rand(minwidth:0.001:maxwidth), twist * rand(minwidth:0.001:maxwidth)], - handles = [rand(0.8:0.1:1.2, 2)...]) - randomopacity ? setopacity(rand()) : setopacity(1.0) - drawbezierpath.(npbp, :stroke, close=false) - end - end -end diff --git a/src/juliagraphics.jl b/src/juliagraphics.jl index 1ee298bc..83079393 100644 --- a/src/juliagraphics.jl +++ b/src/juliagraphics.jl @@ -12,6 +12,15 @@ const purples = (darker_purple, lighter_purple) const greens = (darker_green, lighter_green) const reds = (darker_red, lighter_red) +const juliacolorscheme = [ + RGB(Luxor.darker_purple...), + RGB(Luxor.darker_red...), + RGB(Luxor.darker_red...), + RGB(Luxor.darker_green...), + RGB(Luxor.darker_green...), + RGB(Luxor.darker_purple...) + ] + """ julialogo(;action=:fill, color=true) diff --git a/test/bezierstroke-test.jl b/test/bezierstroke-test.jl index 8a31c1ef..1c6b1543 100644 --- a/test/bezierstroke-test.jl +++ b/test/bezierstroke-test.jl @@ -38,38 +38,49 @@ function testbezierstroke(fname) drawbezierpath(bpath, :stroke) for bps in bpath randomhue() - drawbezierpath(bezierstroke(bps, rand(1:15)), :fill) + drawbezierpath(setbezierhandles(bps, angles=[2rand(), 2rand()], handles=[3rand(), 2rand()]), :fill) end # close needs doing manually - if length(bpath) >= 2 - closingsegment = BezierPathSegment(bpath[end].cp2, bpath[end].p2, bpath[1].p1, bpath[1].cp2) - drawbezierpath(closingsegment, :fill) + if length(bpath) >= 1 + closingsegment = BezierPathSegment(bpath[end].cp2, bpath[end].p2, bpath[1].p1, bpath[1].cp1) + seg = setbezierhandles(closingsegment, angles=[2rand(), -2rand()], handles=[3rand(), 2rand()]) + drawbezierpath(seg, :fill) end end end end + + # Julia! fontsize(500) fontface("Times-Bold") - translate(-450, 0) + translate(-450, 100) + textpath("julia") bp = pathtobezierpaths() newpath() # clear current path + for i in 1:5 for b in bp for bps in b randomhue() - drawbezierpath(bezierstroke(bps), :fill) - sethue("black") - drawbezierpath(bezierstroke(bps), :stroke) + drawbezierpath(setbezierhandles(bps, + angles=[3rand(), 3rand()], handles=[4rand(), 4rand()]), + :fill) end # close needs doing manually - if length(b) >= 2 - closingsegment = BezierPathSegment(b[end].p2, b[end].p2, b[1].p1, b[1].p1) - drawbezierpath(bezierstroke(closingsegment, rand(5:25)), :fill) - sethue("black") - drawbezierpath(bezierstroke(closingsegment, rand(5:25)), :stroke) + if length(b) >= 1 + closingsegment = BezierPathSegment(b[end].cp2, b[end].p2, b[1].p1, b[1].cp1) + seg = setbezierhandles(closingsegment, angles=[4rand(), -4rand()], handles=[4rand(), 4rand()]) + drawbezierpath(seg, :fill) end end - @test finish() == true + + setmode("overlay") + textpath("julia") + sethue("blue") + fillpath() + +end + @test finish() == true end fname = "bezierstroke.png"