diff --git a/CHANGELOG.md b/CHANGELOG.md index f516d7a7..a5c0c09b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [v3.7.0] - forthcoming +## [v3.7.0] - 2023-02-04 LaTeX support is still under development. See https://github.com/JuliaGraphics/Cairo.jl/pull/357. @@ -18,7 +18,7 @@ See https://github.com/JuliaGraphics/Cairo.jl/pull/357. ### Changed -- added more information to doc strings +- added more information to doc strings and tutorials ### Removed diff --git a/docs/make.jl b/docs/make.jl index 4f028cb5..2713858f 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -12,7 +12,7 @@ makedocs( pages = [ "Introduction to Luxor" => "index.md", "Tutorials" => [ - "The Hello World bit" => "tutorial/helloworld.md", + "Hello World" => "tutorial/helloworld.md", "Basic path building" => "tutorial/basicpath.md", "Design a logo" => "tutorial/quickstart.md", "Geometry tutorial" => "tutorial/basictutorial.md", diff --git a/docs/src/tutorial/basicpath.md b/docs/src/tutorial/basicpath.md index fb55d5a8..c71257dd 100644 --- a/docs/src/tutorial/basicpath.md +++ b/docs/src/tutorial/basicpath.md @@ -5,11 +5,15 @@ DocTestSetup = quote ``` # Basic path construction -This tutorial covers the basics of path building in Luxor. If you're familiar with the basics of Cairo, PostScript, or similar graphics applications, you can probably glance at this and move on. And, for a lot more definitive information about how paths work, you could usefully refer to the [Cairo API documentation](https://cairographics.org/manual/cairo-Paths.html); Luxor hides the details behind friendly Julia syntax, but the mechanics are the same. +This tutorial covers the basics of drawing paths in Luxor. If you're familiar with the basics of Cairo, PostScript, Processing, HTML canvas, or similar graphics applications, you can probably glance through these tutorials and then refer to the How To sections. For more information about how paths are built, refer to the [Cairo API documentation](https://cairographics.org/manual/cairo-Paths.html); Luxor hides the details behind friendly Julia syntax, but the underlying mechanics are the same. ## How to build a path -Consider this drawing. We'll use the quick `@drawsvg ...end` macro syntax for simplicity, so the point `(0, 0)` is at the center of the drawing, and the y direction is downwards. +Consider the following drawing. (We'll use the quick `@drawsvg ...end` macro syntax for simplicity.) The point `(0, 0)` is at the center of the drawing canvas, and, as with most graphics software applications, the *y direction is downwards*. + +!!! warning + + Mathematicians and people who like making plots say that the y axis goes up the page. Most graphics software is written with the assumption that the y axis goes downwards. ```@example using Luxor @@ -20,31 +24,34 @@ move(Point(200, 0)) line(Point(250, 100)) curve(Point(150, 150), Point(0, 100), Point(-200, -200)) closepath() + strokepath() end ``` -This drawing constructs and renders a path, using the basic building blocks. +This drawing constructs and renders a path, using basic building blocks. -In Luxor, there's always a current path. At the start, just after we set the color to white, the current path is empty. The `move()` function starts the path by moving to (200, 0), ie 200 units in x (right). This sets the current point to `Point(200, 0)`. +In Luxor, there's always a current path. At the start, just after we set the color to white, the current path is empty. The `move()` function call starts the path by moving to (200, 0), ie 200 units in x (right). This sets the *current point* to `Point(200, 0)`. -The `line(Point(250, 100))` function adds a straight line from the current point down to (250, 100). The current point becomes (250, 100), and the current path now has two entries. We've reached the bottom right corner of this particular path. +The `line(Point(250, 100))` function call adds a straight line from the current point down to the point (250, 100). The current point is now set to (250, 100), and the current path now has two entries. We've reached the bottom right corner of this particular path. -The `curve()` function takes three points, and adds a cubic Bézier curve to the current path. The curve is from the current point to the third point argument, with the first and second point arguments defining the Bézier curve's control points. These influence the shape of the curve. Finally, the current point is set to the point defined by the third argument. +The `curve()` function takes three point arguments, and adds a cubic Bézier curve to the current path. The curve runs from the current point to the third point argument, with the first and second point arguments defining the Bézier curve's control points. These influence the shape of the curve. Finally, the current point is updated to the point defined by the third argument. !!! note - To learn about Bézier curves, you can read [A Primer on Bézier Curves](https://pomax.github.io/Bézierinfo/). + To learn about Bézier curves, read [A Primer on Bézier Curves](https://pomax.github.io/Bézierinfo/). -The `closepath()` function adds a straight line to the path, and joins the current point to the beginning of the path - more specifically, to the most recent point `move`d to. The current point is updated to this point. +The `closepath()` function adds a straight line to the path, joining the current point to the beginning of the path (more specifically, to the most recent point `move`d to). The current point is then updated to this point. -We could use `line()` rather than `closepath()`, but `closepath()` will make a mitred join between the last and first line segments. +We could have used `line(Point(200, 0))` rather than `closepath()`, but `closepath()` is better here because it will make a mitred join between the two line segments. -So, we've constructed a path. The final job is to decide what to do with it, unless you want to add more lines to it. `strokepath()` draws the path using a line with the current settings (width, color, etc). `fillpath()` fills the shape with the current color. +So, now we've constructed a path. The final job is to decide what to do with it. We used `strokepath()` to draw the path using a line with the current settings (width, color, etc). But an alternative is to use `fillpath()` to fill the shape with the current color. `fillstroke()` does both. To change colors and styles, see [Colors and styles](@ref). -At this point, the current path is empty again, and there is no current point. +After you've rendered the path, the current path is empty again, and there is no current point. -And that's how you draw paths in Luxor. However, you'd be right if you think it will be a bit tedious to construct every single shape like this. This is why there are so many other functions in Luxor, such as `circle()`, `ngon()`, `star()`, `rect()`, `box()`, to name just a few. +And that's how you draw paths in Luxor. + +However, you'd be right if you're thinking that constructing every single shape like this would be a lot of work. This is why there are so many other functions in Luxor, such as `circle()`, `ngon()`, `star()`, `rect()`, `box()`, etc. See [Simple graphics](@ref). ## Arcs @@ -64,49 +71,55 @@ end The `arc()` function arguments are: the center point, the radius, the start angle, and the end angle. -But you'll notice that there are two straight lines, not just one. The correct starting point for the arc isn't the same as the current point set by `move(Point(100, 200))`. So a straight line from the current point to the arc's starting point was automatically inserted. +But you'll notice that there are two straight lines, not just one. After moving down to (100, 200), the calculated start point for the arc isn't (100, 200), but (70, 0). So an additional straight line from the current point (100, 200) to the arc's starting point (70, 0) was automatically inserted into the path. Internally, circular arcs are converted to Bézier curves. ## Relative coordinates -The `move()` and `line()` accept absolute coordinates, which refer to the current origin. You might prefer to define the positions with reference to the current point. Use `rmove()` and `rline()` to do this. +The `move()` and `line()` functions require absolute coordinates, which always refer to the current origin, (0, 0). You might prefer to define the positions with reference to the current path's current point. Use `rmove()` and `rline()` to do this. This drawing draws two boxes with 120 unit sides. ```@example using Luxor @drawsvg begin -background("black") -sethue("white") + background("black") + sethue("white") -move(0, 0) -rline(Point(0, 120)) -rline(Point(0, 0)) -rline(Point(120, 0)) -rline(Point(0, -120)) -closepath() + move(0, 0) -rmove(150, 0) -rline(Point(0, 120)) -rline(Point(0, 0)) -rline(Point(120, 0)) -rline(Point(0, -120)) -closepath() + rline(Point(120, 0)) + rline(Point(0, 120)) + rline(Point(-120, 0)) + rline(Point(0, -120)) -strokepath() + closepath() + + rmove(150, 0) + + rline(Point(120, 0)) + rline(Point(0, 120)) + rline(Point(-120, 0)) + rline(Point(0, -120)) + + closepath() + + strokepath() end ``` -The current point just before the second `closepath()` runs is `Point(270.0, 0.0)`. +The drawing instructions to make the two shapes are the same, the second is just moved 150 units in x. -`rmove()` requires a current point to be "relative to", hence the first function is `move()` rather than `rmove()`. +`rmove()` requires a current point to be "relative to". This is why the first drawing function is `move()` rather than `rmove()`. + +Notice that this code draws two shapes, but there was only one `strokepath()` function call. These two shapes are in fact *subpaths*. ## Subpaths -A path can contain any number of these move-line-curve-arc-closepath sequences. Only when you do a `strokepath()` or `fillpath()` function, when the entire path is drawn, then emptied. +A path consists of one or more of these move-line-curve-arc-closepath sequences. Each is a subpath. When you call a `strokepath()` or `fillpath()` function, all the subpaths in the entire path are rendered, and then the current path is emptied. -You can create a subpath either by doing a `move()` in the middle of building a path, or with the specific `newsubpath()` function. +You can create a new subpath either by doing a `move()` or `rmove()` in the middle of building a path (before you render it), or with the specific `newsubpath()` function. An important feature of subpaths is that they can form holes in paths. @@ -133,7 +146,7 @@ fillpath() end ``` -The first subpath is counterclockwise, the second subpath is clockwise and thus forms a hole when you fill the path. +The first subpath is counterclockwise, the second subpath is clockwise and thus forms a hole when you fill the path. (See [Nonzero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule) for details.) ## Not just fill and stroke @@ -156,10 +169,12 @@ line(Point(0, 100)) line(Point(100, 100)) line(Point(100, 0)) closepath() + # purple fill sethue("purple") fillpreserve() -# current path is still active here + +# current path is still here! # cyan stroke sethue("cyan") @@ -169,7 +184,9 @@ end ## Translate, scale, rotate -Suppose you want to repeat a path in various places on the drawing. Obviously you don't want to repeat the steps in the path over and over again. +Suppose you want to repeat a path in various places on the drawing. Obviously you don't want to code the same steps over and over again. + +In this example, the `t()` function draws a triangle path. ```@example using Luxor @@ -188,9 +205,9 @@ t() end ``` -The triangle is drawn when you call the `t()` function. The coordinates are interpreted relative to the current (0, 0) position, scale, and orientation. +Inside `t()`, the coordinates are interpreted relative to the current graphics state: the current origin position (0, 0), scale (1), and rotation (0°). -To draw the triangle in another location, you can use `translate()` to move the (0, 0) to another location. +To draw the triangle in another location, you can first use `translate()` to shift the (0, 0) origin to another location. Now the `move()` and `line()` calls inside the `t()` function all refer to the new location. ```@example using Luxor @@ -206,12 +223,13 @@ end background("black") sethue("white") t() + translate(Point(150, 150)) t() end ``` -Similarly, you could use `scale()` and `rotate()` which further modify the current state. +You can also use the `scale()` and `rotate()` functions to modify the current state: ```@example using Luxor @@ -227,20 +245,33 @@ end background("black") sethue("white") t() + translate(Point(150, 150)) t() + translate(Point(30, 30)) scale(0.5) t() + translate(Point(120, 120)) rotate(π/3) t() end ``` -But, if you experiment with these three functions, you'll notice that the changes are always relative to the previous state. How do you return to a default initial state? You could undo every transformation (in the right order). But a better way is to enclose a set of changes of position, scale, and orientation in a pair of functions (`gsave()` and `grestore()`) that isolate the modifications. +As you experiment with these three functions, you'll notice that the changes are always *relative* to the previous state. So if you do `scale(0.5)` twice, the next path will be drawn a quarter of the size. + +So how do you return to a default initial state? You could of course keep a record of each transformation and apply the opposites, making sure you do this in the right order. + +But a better way is to enclose a sequence of changes of position, scale, and orientation in a pair of functions (`gsave()` and `grestore()`). The following code generates a grid of points in a nested loop. At each iteration: + +1. `gsave()` saves the current position, scale, and orientation on an internal stack. + +2. The graphics state is translated, scaled, and rotated. + +3. The `t()` function is called, and draws the triangle with the new settings. -The following code generates a grid of points in a nested loop. At each iteration, `gsave()` saves the current position, scale, and orientation, the graphics are drawn, and then `grestore()` restores the previously saved state. +4. `grestore()` throws away any changes to position, scale, and rotation, then restores the previous state that was saved with `gsave()`. ```@example using Luxor @@ -277,9 +308,9 @@ You can use `currentpoint()` to get the current point. `rulers()` is useful for drawing the current x and y axes before you start a path. -`storepath()` grabs the current path and saves it as a Path object. You can draw a stored path using `drawpath()`. +`storepath()` grabs the current path and saves it as a Path object. This feature is intended to make Luxor paths more like other Julia objects, which you can save and manipulate before drawing them. -There's another method for `line()`, which takes two points and a rendering instruction. For example: +There's another method for `line()` which takes two points and a rendering instruction. For example: ``` line(Point(0, 0), Point(100, 100), :stroke) @@ -295,7 +326,9 @@ strokepath() ## Polygonal thinking -In Luxor, a polygon is an array (a Julia Vector) of Points. You can treat it like any standard array, and then eventually draw it using the `poly()` function. It's all straight lines, no curves, so you might have to draw a lot of them to get shapes that look like curves. +In Luxor, a polygon is an array (a standard Julia vector) of Points. You can treat it like any standard Julia array, and then eventually draw it using the `poly()` function. + +It's all straight lines, no curves, so you might have to use a lot of points to get smooth curves. ```@example using Luxor @@ -311,6 +344,24 @@ using Luxor end ``` -It's probably easier to generate polygons using Julia code than it is to generate paths. But, no curves. If you need arcs and Bezier curves, stick to paths. +You might find it easier to generate polygons using Julia code than to generate paths. But, of course, there are no curves. If you need arcs and Bézier curves, stick to paths. The `poly()` function simply builds a path with straight lines, and then does the `:fill` or `:stroke` action, depending on which you provide. + +There are some Luxor functions that let you modify the points in a polygon in various ways: + +- `polymove!(pgon, pt1, pt2)` + +move all points by `pt1` -> `pt2` + +- `polyreflect!(pgon, pt1, pt2)` + +reflect all points in line between `pt1` and `pt2` + +- `polyrotate!(pgon, θ)` + +rotate all points by `θ` + +- `polyscale!(pgon, s)` + +scale all points by `s` diff --git a/docs/src/tutorial/helloworld.md b/docs/src/tutorial/helloworld.md index 59b7e383..fb6586f0 100644 --- a/docs/src/tutorial/helloworld.md +++ b/docs/src/tutorial/helloworld.md @@ -3,9 +3,9 @@ DocTestSetup = quote using Luxor, Colors end ``` -# The Hello World bit +# Hello World -Experienced Julia users and programmers fluent in other graphics systems should have no problem using Luxor by referring to the rest of the documentation. +If you're familiar with the basics of Cairo, PostScript, Processing, or similar graphics applications, you can probably glance at these tutorials, then move on to the How To sections. ## What you need @@ -27,7 +27,7 @@ $ julia then you're ready to start. -You can work in a Jupyter or Pluto notebook, or perhaps use the VSCode editor/development environment. It's also possible to work in a text editor (make sure you know how to run a file of Julia code), or, at a pinch, you could use the Julia REPL directly. +You can work in a Jupyter or Pluto notebook, or use the VSCode editor/development environment. It's also possible to work in a text editor (make sure you know how to run a file of Julia code), or, at a pinch, you could use the Julia REPL directly. Ready? Let's begin. @@ -69,7 +69,7 @@ If you're using VS-Code, the image should appear in the Plots window. If you're In this example we've used a macro, [`@png`](@ref). This macro is an easy way to make a drawing, because it saves a lot of typing. (The macro expands to enclose your drawing commands with calls to the [`Drawing()`](@ref), [`origin`](@ref), [`finish`](@ref), and [`preview`](@ref) functions.) There are also [`@svg`](@ref) and [`@pdf`](@ref) macros, which do a similar thing. -PNGs and SVGs are good because they show up in VS-Code, Jupyter, and Plutp. SVGs are usually higher quality too, but they're text-based so can become very large and difficult to load if the image is complex. PDF documents are always higher quality, and usually open up in a separate application. +PNGs and SVGs are good because they show up in VS-Code, Jupyter, and Pluto. SVGs are usually higher quality too, but they're text-based so can become very large and difficult to load if the image is complex. PDF documents are always higher quality, and usually open up in a separate application. This example illustrates a few things about Luxor drawings: diff --git a/docs/src/tutorial/quickstart.md b/docs/src/tutorial/quickstart.md index 63da5aed..feadccff 100644 --- a/docs/src/tutorial/quickstart.md +++ b/docs/src/tutorial/quickstart.md @@ -173,15 +173,9 @@ svgimage # hide The flat circles are a bit dull, so let's write a function that draws the circles as ‘particles’. -The `drawcircle()` function draws lots of circles on top of each other, but each +The `drawcircle()` function draws lots of circular disks on top of each other, but each one is drawn with a slightly smaller radius and a slightly lighter shade of the -incoming color. - -The [`rescale`](@ref) function in Luxor provides an easy way to map or adjust -values from one range to another. Here, numbers between 5 and 1 are mapped to -numbers between 0.5 and 3. - -The radius is scaled to run between `radius` and `radius/6`. +incoming color. The [`rescale`](@ref) function in Luxor provides an easy way to map or adjust values from one range to another. Here, it's used to change both the color and the size of the disks as the loop counter goes from 5 down to 1. Also, let's make them get larger as they spiral outwards, by adding `4i` to the radius when called by `drawcircle()`. @@ -212,7 +206,9 @@ end 500 500 function drawcircle(pos, radius, n) c = colors[mod1(n, end)] for i in 5:-0.1:1 + # increase color from 0.5 to 3 as i drops from 5 to 1 setcolor(rescale(i, 5, 1, 0.5, 3) .* c) + # decrease radius from radius to radius/6 diskradius = rescale(i, 5, 1, radius, radius/6) circle(pos + (i/2, i/2), diskradius, action = :fill) end @@ -288,8 +284,6 @@ svgimage # hide To tidy up, it's a good idea to move the code into functions (to avoid running too much in global scope), and do a bit of housekeeping. -The `rescale()` function takes an input value and two min/max values, and returns the input value lying between the first min/max pair adjusted to match the second min/max pair. - 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 complete final script looks like this: diff --git a/src/Path.jl b/src/Path.jl index 9fd3eafd..2bf04562 100644 --- a/src/Path.jl +++ b/src/Path.jl @@ -119,7 +119,9 @@ which is an array of PathElements. Returns the Path object. -See also getpath() and getpathflat(). +You can draw stored paths using `drawpath()`. + +See also `getpath()`, `getpathflat()`, and `textpath()`. """ function storepath() path = PathElement[] diff --git a/src/basics.jl b/src/basics.jl index d41ab317..16bdb271 100644 --- a/src/basics.jl +++ b/src/basics.jl @@ -408,7 +408,10 @@ move(pt) = move(pt.x, pt.y) """ rmove(pt) -Begin a new subpath in the current path, and add `pt` to the current path's current point. +Begin a new subpath in the current path, add `pt` to the current path's current +point, then update the current point. + +Other path-building functions are `move()`, `line()`, `curve()`, `arc()`, and `rline()`. There must be a current point before you call this function. @@ -423,7 +426,7 @@ rmove(pt) = rmove(pt.x, pt.y) Add a straight line to the current path that joins the path's current point to `pt`. The current point is then updated to `pt`. -Other path-building functions are `line()`, `curve()`, `arc()`, `rline()`, and +Other path-building functions are `move()`, `curve()`, `arc()`, `rline()`, and `rmove()`. See also `currentpoint()` and `hascurrentpoint()`. diff --git a/src/polygons.jl b/src/polygons.jl index f77c2f1b..fde952f1 100644 --- a/src/polygons.jl +++ b/src/polygons.jl @@ -1277,14 +1277,11 @@ end """ polymove!(pgon, frompoint::Point, topoint::Point) -Move (permanently) a polygon from `frompoint` to `topoints`. +Move (permanently) a polygon from `frompoint` to `topoint`. """ function polymove!(pgon, frompoint::Point, topoint::Point) d = topoint - frompoint - @inbounds for i in eachindex(pgon) - pgon[i] = Point(pgon[i].x + d.x, pgon[i].y + d.y) - end - return pgon + return pgon .+= d end """ @@ -1294,11 +1291,8 @@ end Scale (permanently) a polygon by `s`, relative to `center`. """ function polyscale!(pgon, s; - center = O) - @inbounds for i in eachindex(pgon) - pgon[i] = between(center, pgon[i], s) - end - return pgon + center = O) + return pgon .= between.(center, pgon, s) end """ @@ -1309,11 +1303,8 @@ Scale (permanently) a polygon by `sh` horizontally and `sv` vertically, relative to `center`. """ function polyscale!(pgon, sh, sv; - center = O) - @inbounds for i in eachindex(pgon) - pgon[i] = (pgon[i] - center) * (sh, sv) - end - return pgon + center = O) + return pgon .= (pgon .- center) .* Ref((sh, sv)) end """