From 8e1a7d4990c6a43f4d61b48311359cf16facb6c1 Mon Sep 17 00:00:00 2001 From: cormullion Date: Thu, 7 Oct 2021 17:54:02 +0100 Subject: [PATCH] more action defaults --- CHANGELOG.md | 5 +- docs/src/example/moreexamples.md | 88 ++++++++++----- docs/src/explanation/basics.md | 42 +++++--- docs/src/explanation/luxorcairo.md | 2 +- docs/src/howto/colors-styles.md | 4 +- docs/src/howto/tables-grids.md | 8 +- docs/src/howto/text.md | 11 +- docs/src/tutorial/basictutorial.md | 8 +- docs/src/tutorial/quickstart.md | 16 ++- src/BoundingBox.jl | 6 +- src/Boxmaptile.jl | 6 +- src/Luxor.jl | 2 +- src/Table.jl | 15 ++- src/bezierpath.jl | 8 +- src/curves.jl | 23 +++- src/polygons.jl | 6 +- src/shapes.jl | 4 +- src/text.jl | 167 ++++++++++++++++++++++++++--- src/tiles-grids.jl | 17 +-- test/action-testing.jl | 9 ++ 20 files changed, 350 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 540a420a..65ca6e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,15 @@ # Changelog -## [v2.16.0] - forthcoming 2021-10-?? +## [v2.16.0] - forthcoming 2021-10-07 ### Added ### Changed -- many functions can now accept `action=` keyword arguments +- many functions can now accept `action=` keyword arguments as well as positional ones - offsetpoly(... function) algorithm altered (it still sucks, though :)) - `include_first` kwarg added to `polysample()` +- texttrack() rewritten so that the alignment works ### Removed diff --git a/docs/src/example/moreexamples.md b/docs/src/example/moreexamples.md index 952849c1..018bc312 100644 --- a/docs/src/example/moreexamples.md +++ b/docs/src/example/moreexamples.md @@ -83,7 +83,10 @@ where some of the characters—eg "F", "+", "-", and "t"—issue turtle control ## Strange -It's usually better to draw fractals and similar images using pixels and image processing tools. But just for fun it's an interesting experiment to render a strange attractor image using vector drawing rather than placing pixels. This version uses about 600,000 circular dots (which is why it's better to target PNG rather than SVG or PDF for this example!). +It's usually better to draw fractals and similar images +using pixels and image processing tools. But just for fun +it's an interesting experiment to render a strange attractor +image using vector drawing rather than placing pixels. ```@example using Luxor, Colors @@ -91,39 +94,72 @@ function strange(dotsize, w=800.0) xmin = -2.0; xmax = 2.0; ymin= -2.0; ymax = 2.0 Drawing(w, w, "../assets/figures/strange-vector.png") origin() - background("white") - xinc = w/(xmax - xmin) - yinc = w/(ymax - ymin) + background("grey5") + xinc = w / (xmax - xmin) + yinc = w / (ymax - ymin) # control parameters - a = 2.24; b = 0.43; c = -0.65; d = -2.43; e1 = 1.0 + a = 2.24 + b = 0.43 + c = -0.65 + d = -2.43 + e1 = 1.0 x = y = z = 0.0 - wover2 = w/2 - for j in 1:w - for i in 1:w - xx = sin(a * y) - z * cos(b * x) + wover2 = w / 2 - 50 # margin + for j = 1:w + for i = 1:w + xx = sin(a * y) - z * cos(b * x) yy = z * sin(c * x) - cos(d * y) zz = e1 * sin(x) - x = xx; y = yy; z = zz - if xx < xmax && xx > xmin && yy < ymax && yy > ymin - xpos = rescale(xx, xmin, xmax, -wover2, wover2) # scale to range - ypos = rescale(yy, ymin, ymax, -wover2, wover2) # scale to range - rcolor = rescale(xx, -1, 1, 0.0, .7) - gcolor = rescale(yy, -1, 1, 0.2, .5) - bcolor = rescale(zz, -1, 1, 0.3, .8) - setcolor(convert(Colors.HSV, Colors.RGB(rcolor, gcolor, bcolor))) - circle(Point(xpos, ypos), dotsize, :fill) + x = xx + y = yy + z = zz + if xx < xmax && xx > xmin + if yy < ymax && yy > ymin + xpos = rescale( + xx, + xmin, + xmax, + -wover2, + wover2, + ) # scale to range + ypos = rescale( + yy, + ymin, + ymax, + -wover2, + wover2, + ) # scale to range + rcolor = rescale(xx, -1, 1, 0.0, 0.6) + gcolor = rescale(yy, -1, 1, 0.2, 0.5) + bcolor = rescale(zz, -1, 1, 0.6, 0.9) + setcolor(rcolor, gcolor, bcolor) + move(Point(xpos, ypos)) + line(Point(xpos + dotsize, ypos)) + line(Point( + xpos + dotsize, + ypos + dotsize, + )) + line(Point(xpos, ypos + dotsize)) + fillpath() + end end end end finish() end -strange(.3, 800) +strange(.5, 800) nothing # hide ``` ![strange attractor in vectors](../assets/figures/strange-vector.png) +This example generates about 650,000 paths, which is why +it’s better to target PNG rather than SVG or PDF for this +example. Also for speed, the “dots” are actually simple +paths, which are slightly quicker to draw than circles or +polygons. + ## More animations [![strange attractor in vectors](../assets/figures/animation-screengrab.jpg)](https://youtu.be/1FA2FgJU6dM) @@ -171,9 +207,9 @@ end function draw_scarab_body() @layer begin - squircle(Point(0, -25), 26, 75, :path) - squircle(Point(0, 0), 50, 70, :path) - squircle(Point(0, 40), 65, 90, :path) + squircle(Point(0, -25), 26, 75, action=:path) + squircle(Point(0, 0), 50, 70, action=:path) + squircle(Point(0, 40), 65, 90, action=:path) end end @@ -187,20 +223,20 @@ function draw() height= 240 sethue("black") - squircle(O, width, height-5, rt=0.4, :fill) + squircle(O, width, height-5, rt=0.4, action=:fill) set_gold_blend() - squircle(O, width, height-5, rt=0.4, :path) + squircle(O, width, height-5, rt=0.4, action=:path) translate(0, 50) draw_scarab_legs(O) strokepath() draw_scarab_body() fillpath() - # julia dots === Ra + # julia dots === Ra egyptian sun deity @layer begin translate(0, -190) - circle(O, 48, :fill) + circle(O, 48, action=:fill) juliacircles(20) end diff --git a/docs/src/explanation/basics.md b/docs/src/explanation/basics.md index 78b04b28..f7c66988 100644 --- a/docs/src/explanation/basics.md +++ b/docs/src/explanation/basics.md @@ -6,13 +6,24 @@ DocTestSetup = quote # The drawing model -The underlying drawing model is that you build paths, and these are filled and/or stroked, using the current *graphics state*, which specifies colors, line thicknesses, scale, orientation, opacity, and so on. - -You can modify the current graphics state by transforming/rotating/scaling it, setting color and style parameters, and so on. - -Many of the drawing functions have an *action* argument, supplied either as a symbol argument (eg `:fill`) or as a keyword argument (eg `action=:fill`). This action can be `:none`, `:fill`, `:stroke`, `:fillstroke`, `:fillpreserve`, `:strokepreserve`, `:clip`, or `:path`. The default is `:none`, which is usually equivalent to `:path`, ie. add to the current path but otherwise do nothing. - -Subsequent graphics use the new state, but the graphics you've already drawn are unchanged. +The underlying drawing model is that you build paths, and +these are filled and/or stroked, using the current *graphics +state*, which specifies colors, line thicknesses, scale, +orientation, opacity, and so on. + +You can modify the current graphics state by +transforming/rotating/scaling it, setting color and style +parameters, and so on. Subsequent graphics use the new +state, but the graphics you've already drawn are unchanged. + +Many of the drawing functions have an *action* argument, +supplied either as a symbol argument (eg `:fill`) or as a +keyword argument (eg `action=:fill`). This action determines +what happens to the current path. It can be `:none`, +`:fill`, `:stroke`, `:fillstroke`, `:fillpreserve`, +`:strokepreserve`, `:clip`, or `:path`. The default is +`:none`, which is usually equivalent to `:path`, ie. add to +the current path but otherwise do nothing. The main Julia data types you'll encounter in Luxor are: @@ -33,9 +44,12 @@ The main Julia data types you'll encounter in Luxor are: ## Points and coordinates -You specify points on the drawing surface using `Point(x, y)`. (A few older functions accept separate x and y values). +You specify points on the drawing surface using `Point(x, y)`. -The default _origin_ (ie the `x = 0, y = 0` point) is at the top left corner: the x axis runs left to right across the page, and the y axis runs top to bottom down the page, so Y coordinates increase downwards. +The default _origin_ (ie the `x = 0, y = 0` point) is at the +top left corner: the x axis runs left to right across the +page, and the y axis runs top to bottom down the page, so Y +coordinates increase downwards. By default, `Point(0, 100)` is below `Point(0, 0)`. @@ -124,7 +138,9 @@ julia> Point.(plist) Point(14.2, 15.4) ``` -You can use the letter **O** as a shortcut to refer to the current Origin, `Point(0, 0)`. +You can use the letter **O** as a shortcut to refer to the +current Origin, `Point(0, 0)`. Most coding fonts clearly +show the difference between the letter `O` and the digit `0`. ```@example using Luxor # hide @@ -251,8 +267,8 @@ You can use an environment such as a Jupyter or Pluto notebook or the Juno or VS Luxor can create new SVG images, either in a file or in memory, and can also place existing SVG images on a drawing. See [Placing images](@ref) for more. It's also possible to -obtain the source of an SVG drawing as a string. For example, -this code draws the Julia logo using SVG code: +obtain the source of the current SVG drawing as a string. For example, +this code draws the Julia logo using SVG code and stores the SVG in `s`: ```julia Drawing(500, 500, :svg) @@ -262,7 +278,7 @@ finish() s = svgstring() ``` -You can now examine the SVG code in `s` programmatically: +You can now examine the SVG elements in `s` programmatically: ```julia eachmatch(r"rgb\(.*?\)", s) |> collect diff --git a/docs/src/explanation/luxorcairo.md b/docs/src/explanation/luxorcairo.md index 23a9ba85..2ab8f6a4 100644 --- a/docs/src/explanation/luxorcairo.md +++ b/docs/src/explanation/luxorcairo.md @@ -62,7 +62,7 @@ documentation](https://cairographics.org/documentation/). If a Cairo function isn't yet supported in Cairo.jl or Luxor.jl, a temporary workround is to add a direct call to the Cairo library in your Luxor script. -For example, the Cairo function to return the current line width ([cairo_get_line_width](https://cairographics.org/manual/cairo-cairo-t.html#cairo-get-line-width)) isn't yet available in Julia, but you can easily add it with code like this (or better): +For example, the Cairo function to return the current line width ([`cairo_get_line_width`](https://cairographics.org/manual/cairo-cairo-t.html#cairo-get-line-width)) isn't yet available in Julia, but you can easily add it with code like this (or better): ```julia using Luxor diff --git a/docs/src/howto/colors-styles.md b/docs/src/howto/colors-styles.md index 904003be..22f8aa61 100644 --- a/docs/src/howto/colors-styles.md +++ b/docs/src/howto/colors-styles.md @@ -43,7 +43,7 @@ gamma = 2.2 for n in 1:length(cols) col = cols[n][1] r, g, b = sethue(col) - box(table[n], table.colwidths[1], table.rowheights[1], :fill) + box(table[n], table.colwidths[1], table.rowheights[1], action=:fill) luminance = 0.2126 * r^gamma + 0.7152 * g^gamma + 0.0722 * b^gamma (luminance > 0.5^gamma) ? sethue("black") : sethue("white") text(string(cols[n][1]), table[n], halign=:center, valign=:middle) @@ -78,7 +78,7 @@ for l in 1:3 textcentred(["butt", "square", "round"][l], 80l, 80) setlinejoin(["round", "miter", "bevel"][l]) textcentred(["round", "miter", "bevel"][l], 80l, 120) - poly(ngon(Point(80l, 0), 20, 3, 0, vertices=true), :strokepreserve, close=false) + poly(ngon(Point(80l, 0), 20, 3, 0, vertices=true), action=:strokepreserve, close=false) sethue("white") setline(1) strokepath() diff --git a/docs/src/howto/tables-grids.md b/docs/src/howto/tables-grids.md index 63d83b95..97716ecc 100644 --- a/docs/src/howto/tables-grids.md +++ b/docs/src/howto/tables-grids.md @@ -32,14 +32,14 @@ fontsize(20) # hide tiles = Tiler(800, 500, 4, 5, margin=5) for (pos, n) in tiles randomhue() - box(pos, tiles.tilewidth, tiles.tileheight, :fill) + box(pos, tiles.tilewidth, tiles.tileheight, action=:fill) if n % 3 == 0 gsave() translate(pos) subtiles = Tiler(tiles.tilewidth, tiles.tileheight, 4, 4, margin=5) for (pos1, n1) in subtiles randomhue() - box(pos1, subtiles.tilewidth, subtiles.tileheight, :fill) + box(pos1, subtiles.tilewidth, subtiles.tileheight, action=:fill) end grestore() end @@ -79,7 +79,7 @@ Unlike a `Tiler`, the `Table` iterator lets you have columns with different widt ‘width’ -> ‘height’, ‘row’ -> ‘column’. This flavour of consistency can sometimes be confusing if you’re expecting other kinds of consistency, such as ‘x before - y’ or ‘column major’...) + y’ or ‘column major’.) Tables don't store data, of course, but are designed to help you draw tabular data. @@ -153,7 +153,7 @@ end setopacity(0.5) sethue("thistle") -circle.(t[3, :], 20, :fill) # row 3, every column +circle.(t[3, :], 20, action=:fill) # row 3, every column finish() # hide nothing # hide diff --git a/docs/src/howto/text.md b/docs/src/howto/text.md index b046eda4..0f2769a8 100644 --- a/docs/src/howto/text.md +++ b/docs/src/howto/text.md @@ -462,13 +462,18 @@ nothing # hide ## Text tracking -Use [`texttrack`](@ref) to track or letter-space text, i.e. vary the spacing between every letter. ("Kerning" is when you do this for just a pair of letters.) The units are 1/1000 em, so the actual distance of "50 units of tracking" varies depending on the current font size. +Use [`texttrack`](@ref) to track or letter-space text, i.e. +vary the spacing between every letter. ("Kerning" is when +you do this for just a pair of letters.) -But really, don’t track text unless you have to. +The tracking units depend on the current font size. In a +12‑point font, 1 em equals 12 points. A point is about +0.35mm, so a 1000 units of tracking for 12 point text +produces about 4.2mm of space between each character. ```@example using Luxor # hide -Drawing(600, 600, "../assets/figures/texttrack.svg") # hide +Drawing(600, 400, "../assets/figures/texttrack.svg") # hide origin() # hide background("white") # hide sethue("black") # hide diff --git a/docs/src/tutorial/basictutorial.md b/docs/src/tutorial/basictutorial.md index 23bfae3a..43246067 100644 --- a/docs/src/tutorial/basictutorial.md +++ b/docs/src/tutorial/basictutorial.md @@ -11,9 +11,8 @@ Experienced Julia users and programmers fluent in other languages and graphics s If you've already downloaded Julia, and have added the Luxor package successfully (using `] add Luxor`): -```@raw html - -
+```
+$ julia
                    _
        _       _ _(_)_     |  Documentation: https://docs.julialang.org
       (_)     | (_) (_)    |
@@ -23,10 +22,7 @@ If you've already downloaded Julia, and have added the Luxor package successfull
      _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
     |__/                   |
 
-$ julia
 (v1.6) pkg> add Luxor
-
-
``` then you're ready to start. diff --git a/docs/src/tutorial/quickstart.md b/docs/src/tutorial/quickstart.md index 170ffb48..89b17fa2 100644 --- a/docs/src/tutorial/quickstart.md +++ b/docs/src/tutorial/quickstart.md @@ -70,7 +70,7 @@ svgimage = @drawsvg begin setcolor("red") corners = ngon(Point(0, 0), 80, 3, vertices=true) -circle.(corners, 10, :fill) +circle.(corners, 10, action=:fill) end 500 500 ``` @@ -80,7 +80,7 @@ Drawing(500, 500, "my-drawing.svg") origin() setcolor("red") corners = ngon(Point(0, 0), 80, 3, vertices=true) -circle.(corners, 10, :fill) +circle.(corners, 10, action=:fill) finish() preview() ``` @@ -289,7 +289,7 @@ To tidy up, it's a good idea to move the code into functions (to avoid running t Also, a background for the icon would look good. [`squircle`](@ref) is useful for drawing shapes that occupy the space between pointy dull rectangles and space-inefficient circles. -The final script looks like this: +The complete final script looks like this: ```@setup example_7 using Luxor, Colors @@ -353,7 +353,7 @@ svgimage # hide So, did the JuliaFission organization like their logo? Who knows? - they may still be debating how accurate the -representation of an atom it should be... But if not, we can +representation of an atom should be... But if not, we can always recycle some of these ideas for future projects. 😃 ## Extra credit @@ -364,4 +364,10 @@ Try refactoring your code so that you can automatically run through various para ### 2. Remember the random values -Using random numbers is a great way to find new patterns and shapes; but unless you know what values were used, you can't easily reproduce them. It's frustrating to produce something really good but not know what values were used to make it. So modify the code so that the random numbers are remembered, and drawn on the screen (you can use the `text(str, position)` function), +Using random numbers is a great way to find new patterns and +shapes; but unless you know what values were used, you can't +easily reproduce them. It's frustrating to produce something +really good but not know what values were used to make it. +So modify the code so that the random numbers are +remembered, and drawn on the screen (you can use the +`text(str, position)` function), diff --git a/src/BoundingBox.jl b/src/BoundingBox.jl index fd4827e6..18ce4f7f 100644 --- a/src/BoundingBox.jl +++ b/src/BoundingBox.jl @@ -211,6 +211,8 @@ end box(bbox::BoundingBox; action = :none, vertices = false) + box(bbox::BoundingBox, action::Symbol; + vertices=false) Define a box using the bounds in `bbox`. @@ -231,10 +233,6 @@ function box(bbox::BoundingBox; end end -""" - box(bbox::BoundingBox, action::Symbol; - vertices=false) -""" box(bbox::BoundingBox, action::Symbol) = box(bbox, action=action, vertices=false) diff --git a/src/Boxmaptile.jl b/src/Boxmaptile.jl index f5ecbd76..3a724d24 100644 --- a/src/Boxmaptile.jl +++ b/src/Boxmaptile.jl @@ -149,13 +149,14 @@ end """ box(tile::BoxmapTile, action::Symbol=:none; vertices=false) + box(tile::BoxmapTile, action=:none, vertices=false) Use a Boxmaptile to make or draw a rectangular box. Use `vertices=true` to obtain the coordinates. Create boxmaps using `boxmap()`. """ -function box(tile::BoxmapTile, action::Symbol=:none; vertices=false) +function box(tile::BoxmapTile, action::Symbol; vertices=false) if vertices return [Point(tile.x, tile.y + tile.h), Point(tile.x, tile.y), @@ -165,6 +166,9 @@ function box(tile::BoxmapTile, action::Symbol=:none; vertices=false) rect(tile.x, tile.y, tile.w, tile.h, action) end +box(tile::BoxmapTile; action=:none, vertices=false) = + box(tile, action, vertices=vertices) + """ BoundingBox(tile::BoxmapTile) diff --git a/src/Luxor.jl b/src/Luxor.jl index 1ce79c4a..a1e7a2f2 100644 --- a/src/Luxor.jl +++ b/src/Luxor.jl @@ -153,7 +153,7 @@ export Drawing, textoutlines, textcurve, textcentred, textcentered, textright, textcurvecentred, textcurvecentered, get_fontsize, textwrap, textlines, splittext, textbox, - texttrack, + texttrack, textplace, setcolor, setopacity, sethue, setgrey, setgray, randomhue, randomcolor, @setcolor_str, diff --git a/src/Table.jl b/src/Table.jl index 1c1accb6..af869bf3 100644 --- a/src/Table.jl +++ b/src/Table.jl @@ -253,21 +253,26 @@ Base.eachindex(t::Table) = 1:length(t) # box extensions """ - box(t::Table, r::T, c::T, action::Symbol=:none) where T <: Integer + box(t::Table, r::Integer, c::Integer, action::Symbol) + box(t::Table, r::Integer, c::Integer; action=:none) Draw a box in table `t` at row `r` and column `c`. """ -function box(t::Table, r::T, c::T, action::Symbol=:none; kwargs...) where T <: Integer +function box(t::Table, r::Integer, c::Integer, action::Symbol; kwargs...) cellw, cellh = t.colwidths[c], t.rowheights[r] box(t[r, c], cellw, cellh, action; kwargs...) end +box(t::Table, r::Integer, c::Integer; action=:none, reversepath=false, vertices=false) = + box(t, r, c, action, reversepath=reversepath, vertices=vertices) + """ box(t::Table, cellnumber::Int, action::Symbol=:none; vertices=false) + box(t::Table, cellnumber::Int; action=:none, vertices=false) -Draw box `cellnumber` in table `t`. +Make box around cell `cellnumber` in table `t`. """ -function box(t::Table, cellnumber::Int, action::Symbol=:none; vertices=false) +function box(t::Table, cellnumber::Int, action::Symbol; vertices=false) r = div(cellnumber - 1, t.ncols) + 1 c = mod1(cellnumber, t.ncols) @@ -275,6 +280,8 @@ function box(t::Table, cellnumber::Int, action::Symbol=:none; vertices=false) box(t[r, c], cellw, cellh, action; vertices=vertices) end +box(t::Table, cellnumber::Int; action=:none, vertices=false) = box(t, cellnumber, action, vertices=vertices) + """ highlightcells(t::Table, cellnumbers, action::Symbol=:stroke; color::Colorant=colorant"red", diff --git a/src/bezierpath.jl b/src/bezierpath.jl index bdd1e041..8ee884da 100644 --- a/src/bezierpath.jl +++ b/src/bezierpath.jl @@ -177,11 +177,14 @@ end """ drawbezierpath(bezierpath::BezierPath, action=:none; close=true) + drawbezierpath(bezierpath::BezierPath; + action=:none, + close=true) Draw the Bézier path, and apply the action, such as `:none`, `:stroke`, `:fill`, etc. By default the path is closed. """ -function drawbezierpath(bezierpath::BezierPath, action=:none; close=true) +function drawbezierpath(bezierpath::BezierPath, action; close=true) move(bezierpath[1].p1) for i in 1:length(bezierpath) - 1 c = bezierpath[i] @@ -194,6 +197,9 @@ function drawbezierpath(bezierpath::BezierPath, action=:none; close=true) do_action(action) end +drawbezierpath(bezierpath; action=:none, close=true) = + drawbezierpath(bezierpath, action; close=close) + """ drawbezierpath(bps::BezierPathSegment, action=:none; close=false) diff --git a/src/curves.jl b/src/curves.jl index d24d5552..04c23936 100644 --- a/src/curves.jl +++ b/src/curves.jl @@ -122,6 +122,8 @@ ellipse(c::Point, w::Real, h::Real, action::Symbol) = ellipse(c, w, h, action=ac squircle(center::Point, hradius, vradius; action=:none, rt = 0.5, stepby = pi/40, vertices=false) + squircle(center::Point, hradius, vradius, action; + rt = 0.5, stepby = pi/40, vertices=false) Make a squircle or superellipse (basically a rectangle with rounded corners). Specify the center position, horizontal radius (distance from center to a side), @@ -467,8 +469,14 @@ curve(pt1, pt2, pt3) = curve(pt1.x, pt1.y, pt2.x, pt2.y, pt3.x, pt3.y) action=:none, reversepath=false, kappa = 0.5522847498307936) + circlepath(center::Point, radius, action; + reversepath=false, + kappa = 0.5522847498307936) -Draw a circle using Bézier curves. +Draw a circle using Bézier curves. One benefit of using this +rather than `circle()` is that you can use the `reversepath` +option to draw the circle clockwise rather than +`circle`'s counterclockwise. The magic value, `kappa`, is `4.0 * (sqrt(2.0) - 1.0) / 3.0`. """ @@ -612,6 +620,10 @@ ellipse(focus1::Point, focus2::Point, pt::Point, action::Symbol; stepby=0.01, period=0.0, vertices=false) + hypotrochoid(R, r, d, action; + stepby=0.01, + period=0.0, + vertices=false) Make a hypotrochoid with short line segments. (Like a Spirograph.) The curve is traced by a point attached to a circle of radius `r` rolling around the inside of a fixed circle of @@ -671,6 +683,10 @@ hypotrochoid(R, r, d, action::Symbol; stepby=0.01, period=0, vertices=false) + epitrochoid(R, r, d, action; + stepby=0.01, + period=0, + vertices=false) Make a epitrochoid with short line segments. (Like a Spirograph.) The curve is traced by a point attached to a circle of radius `r` rolling around the outside of a fixed circle of @@ -729,6 +745,11 @@ epitrochoid(R, r, d, action::Symbol; period = 4pi, vertices = false, log =false) + spiral(a, b, action; + stepby = 0.01, + period = 4pi, + vertices = false, + log =false) Make a spiral. The two primary parameters `a` and `b` determine the start radius, and the tightness. diff --git a/src/polygons.jl b/src/polygons.jl index 475dc538..bfe4e9d8 100644 --- a/src/polygons.jl +++ b/src/polygons.jl @@ -462,6 +462,7 @@ end """ polysmooth(points, radius, action=:action; debug=false) + polysmooth(points, radius; action=:none, debug=false) Make a closed path from the `points` and round the corners by making them arcs with the given radius. Execute the action when finished. @@ -472,7 +473,7 @@ possible (as large as the shortest side allows). The `debug` option also draws the construction circles at each corner. """ -function polysmooth(points::Array{Point, 1}, radius, action=:action; debug=false) +function polysmooth(points::Array{Point, 1}, radius, action::Symbol; debug=false) temppath = Tuple[] l = length(points) if l < 3 @@ -501,6 +502,9 @@ function polysmooth(points::Array{Point, 1}, radius, action=:action; debug=false do_action(action) end +polysmooth(points::Array{Point, 1}, radius; action=:none, debug=false) = + polysmooth(points, radius, action; debug=debug) + """ offsetpoly(plist::Array{Point, 1}, d) where T<:Number diff --git a/src/shapes.jl b/src/shapes.jl index 4135d034..6e197b9a 100644 --- a/src/shapes.jl +++ b/src/shapes.jl @@ -70,8 +70,8 @@ rect(cornerpoint::Point, w::Real, h::Real, action::Symbol; vertices = vertices) """ - box(cornerpoint1, cornerpoint2; action=:none, vertices=false) - box(cornerpoint1, cornerpoint2, action; vertices=false) + box(cornerpoint1, cornerpoint2; action=:none, vertices=false, reversepath=false) + box(cornerpoint1, cornerpoint2, action; vertices=false, reversepath=false) Create a box (rectangle) between two points and do an action. diff --git a/src/text.jl b/src/text.jl index 5434067b..d9ebb456 100644 --- a/src/text.jl +++ b/src/text.jl @@ -684,7 +684,12 @@ textwrap(s::T where T<:AbstractString, width::Real, pos::Point; kwargs...) = kwargs...) """ - texttrack(txt, pos, tracking, fontsize=12; + texttrack(txt, pos, tracking; + action=:fill, + halign=:left, + valign=:baseline, + startnewpath=true) + texttrack(txt, pos, tracking, fontsize; action=:fill, halign=:left, valign=:baseline, @@ -693,27 +698,161 @@ textwrap(s::T where T<:AbstractString, width::Real, pos::Point; kwargs...) = Place the text in `txt` at `pos`, left-justified, and letter space ('track') the text using the value in `tracking`. -The tracking units depend on the current font size! 1 is -1/1000 em. In a 6‑point font, 1 em equals 6 points; in a -10‑point font, 1 em equals 10 points. +The tracking units depend on the current font size. In a +12‑point font, 1 em equals 12 points. A point is about +0.35mm, 1em is about 4.2mm, and a 1000 units of tracking are +about 4.2mm. So a tracking value of 1000 for a 12 point font +places about 4mm between each character. -A value of -50 would tighten the letter spacing noticeably. -A value of 50 would make the text more open. +A negative value tightens the letter spacing noticeably. The text drawing action applied to each character defaults to `textoutlines(... :fill)`. + +If `startnewpath` is true, each character is acted on +separately. To clip and track text, specify the clip action +and avoid resetting the clipping path for each character. + +```julia + newpath() + texttrack(t, O + (0, 80), 200, action=:clip, startnewpath=false) + ... + clipreset() +``` + +TODO Is it possible to fix strings with combining characters such as "\u0308"? """ -function texttrack(txt, pos, tracking, fontsize=12; +function texttrack(txt, pos, tracking, fsize; action=:fill, halign=:left, valign=:baseline, startnewpath=true) - te = textextents(txt) - for i in txt - glyph = string(i) - glyph_x_bearing, glyph_y_bearing, glyph_width, glyph_height, glyph_x_advance, glyph_y_advance = textextents(glyph) - textoutlines(glyph, pos, action, halign=halign, valign=valign, startnewpath=startnewpath) - x = glyph_x_advance + (tracking/1000) * fontsize - pos += (x, 0) + + advances = [] + emspacewidth = textextents(" ")[5] + for c in txt + xbearing, ybearing, textwidth, textheight, xadvance, yadvance = textextents(string(c)) + if c == ' ' + textwidth = emspacewidth + end + push!(advances, xadvance) + end + + # adjust start position for alignment + # first, horizontal alignment - 1, 2, 3 + halignment = findfirst(isequal(halign), [:left, :center, :right, :centre]) + + # if unspecified or wrong, default to left, also treat UK spelling centre as center + if halignment == nothing + halignment = 1 + elseif halignment == 4 + halignment = 2 + end + + # calculate width of the final tracked string + # need this to do alignment + total_textwidth = sum(advances) + length(advances) * ((tracking/1000) * fsize) + textpointx = pos.x - [0, total_textwidth/2, total_textwidth][halignment] + # next vertical alignment + valignment = findfirst(isequal(valign), [:top, :middle, :baseline, :bottom]) + # if unspecified or wrong, default to baseline + if valignment == nothing + valignment = 3 + end + ybearing, textheight = textextents(txt)[[2, 4]] + textpointy = pos.y - [ybearing, ybearing/2, 0, textheight + ybearing][valignment] + + # this is the first point of the text string + newpos = Point(textpointx, textpointy) + + # if clipping, clip the entire path, not individual characters + if action == :clip + _action = :path + else + _action = action + end + + # draw the text + for (n, c) in enumerate(txt) + textoutlines(string(c), newpos, _action, halign=:left, startnewpath=startnewpath) + # calculate new position based on precalculated widths plus the tracking + newpos = Point(newpos.x + advances[n] + ((tracking/1000) * fsize), newpos.y) + end + if action == :clip + do_action(:clip) + end +end + +texttrack(txt, pos, tracking; + action=:fill, + halign=:left, + valign=:baseline, + startnewpath=true) = texttrack(txt, pos, tracking, get_fontsize(); + action=action, + halign=halign, + valign=valign, + startnewpath=startnewpath) + +""" + textplace(txt::AbstractString, pos::Point, params::Vector) + +A low-level function that places text characters one by one +according to the parameters in `params`. First character +uses first tuple, second character the second, and so on. +Return next text position. + +A tuple of parameters is: + +```julia +(face = "TimesRoman", size = 12, kern = 0, shift = 0, advance = true) +``` + +where + +- face is fontface "string" # sticky + +- size is fontsize # pts # sticky + +- kern amount (pixels) shifted to the right # resets after each char + +- shift = baseline shifted vertically # resets after each char + +- advance - whether to advance # resets after each char + +Font face and size parameters are sticky, and stay set until +reset. Kern/shift/advance are reset for each character. + +## Example + +Draw the Hogwarts Express Platform number 9 and 3/4 + +```julia +txtpos = textplace("93—4", O, [ + (size=120, face="Bodoni-Poster", ), # format for 9 + (size=60, kern = 10, shift = 60, advance=false,), # format for 3 + ( kern = 0, shift = 25, advance=false,), # format for - + ( kern = 10, shift = -20, advance=true), # format for 4 + ]) +``` +""" +function textplace(txt::AbstractString, pos::Point, params::Vector) + @layer begin + emspacewidth = textextents(" ")[5] + textpos = Point(pos.x, pos.y) + defaultparams = (face = "", size = 12, kern=0, shift=0, advance=true) + for (n, c) in enumerate(txt) + defaultparams = merge(defaultparams, (kern=0, shift=0, advance=true,)) + if n <= length(params) + defaultparams = merge(defaultparams, params[n]) + end + fontface(defaultparams.face) + fontsize(defaultparams.size) + xbearing, ybearing, textwidth, textheight, xadvance, yadvance = textextents(string(c)) + textoutlines(string(c), Point(textpos.x + defaultparams.kern, textpos.y - defaultparams.shift), :fill, halign=:left) + if defaultparams.advance == true + textpos += (xadvance + defaultparams.advance, 0) + end + end end + return textpos end diff --git a/src/tiles-grids.jl b/src/tiles-grids.jl index 80112bb3..f6b25f42 100644 --- a/src/tiles-grids.jl +++ b/src/tiles-grids.jl @@ -348,17 +348,22 @@ function Base.getindex(pt::Partition, i::Int) end """ - box(tiles::Tiler, n::T where T <: Integer, action::Symbol=:none; - vertices=false) + box(tiles::Tiler, n::Integer; action=:none, vertices=false, reversepath=false) + box(tiles::Tiler, n::Integer, action::Symbol=:none; vertices=false, reversepath=false) Draw a box in tile `n` of tiles `tiles`. """ -function box(tiles::Tiler, n::T where T <: Integer, action::Symbol=:none; - vertices=false) +function box(tiles::Tiler, n::Integer; + action=:none, + vertices=false, + reversepath=false) tilew, tileh = tiles.tilewidth, tiles.tileheight if vertices == true || action == :none - box(first(tiles[n]), tilew, tileh, vertices=true) + box(first(tiles[n]), tilew, tileh, action=:none, vertices=true, reversepath=reversepath) else - box(first(tiles[n]), tilew, tileh, action) + box(first(tiles[n]), tilew, tileh, action=action, vertices=false, reversepath=reversepath) end end + +box(tiles::Tiler, n::Integer, action::Symbol; vertices=false, reversepath=false) = + box(tiles, n, action=action, vertices=vertices, reversepath=reversepath) diff --git a/test/action-testing.jl b/test/action-testing.jl index d3376b31..dd111e4e 100644 --- a/test/action-testing.jl +++ b/test/action-testing.jl @@ -25,6 +25,15 @@ expressions = [ :(box(BoundingBox(box([O, Point(24, 20), Point(20, 30)])), :fill)), :(box(BoundingBox(box([O, Point(24, 20), Point(20, 30)])), action=:fill)), +:(begin + t = Table(3, 3) + pts = box(t, 2, 1, action=:stroke, vertices=false, reversepath=true) + pts = box(t, 2, 1, :stroke, vertices=false, reversepath=true) + t = Tiler(50, 50, 3, 3) + pts = box(t, 5, action=:stroke, vertices=false, reversepath=true) + pts = box(t, 5, :stroke, vertices=false, reversepath=true) +end), + :(box(Point(-30, -50), Point(20, 50), :fill)), :(box(Point(-25, -20), Point(23, 20), action=:fill)),