diff --git a/CHANGELOG.md b/CHANGELOG.md index a4db18cf..a424681b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,13 @@ # Changelog -## [v4.1] - forthcoming +## [v4.1] - 2024-07-31 ### Added - triangular grids - `squirclepath()` - `rule(pt1, pt2)` +- `polysidelengths()` ### Changed diff --git a/docs/Project.toml b/docs/Project.toml index bdb50baf..2b91e89f 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,14 +1,10 @@ [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8" -Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" Luxor = "ae8d54c2-7ccd-5906-9d76-62fc9837b5bc" MathTeXEngine = "0a4f8689-d25c-4efe-a92b-7142dfc1aa53" [compat] Colors = "0.12" Documenter = "1" -DocumenterTools = "0.1" -Images = "0.26.1" MathTeXEngine = "0.5, 0.6" diff --git a/docs/src/assets/figures/minifb-pi-1.png b/docs/src/assets/figures/minifb-pi-1.png new file mode 100644 index 00000000..cc359104 Binary files /dev/null and b/docs/src/assets/figures/minifb-pi-1.png differ diff --git a/docs/src/assets/figures/minifb-pi.png b/docs/src/assets/figures/minifb-pi.png new file mode 100644 index 00000000..e66f7098 Binary files /dev/null and b/docs/src/assets/figures/minifb-pi.png differ diff --git a/docs/src/assets/figures/playing-pixels-1.png b/docs/src/assets/figures/playing-pixels-1.png new file mode 100644 index 00000000..3f621640 Binary files /dev/null and b/docs/src/assets/figures/playing-pixels-1.png differ diff --git a/docs/src/assets/figures/playing-pixels-2.png b/docs/src/assets/figures/playing-pixels-2.png new file mode 100644 index 00000000..957f9150 Binary files /dev/null and b/docs/src/assets/figures/playing-pixels-2.png differ diff --git a/docs/src/assets/figures/playing-pixels-3.png b/docs/src/assets/figures/playing-pixels-3.png new file mode 100644 index 00000000..38a62764 Binary files /dev/null and b/docs/src/assets/figures/playing-pixels-3.png differ diff --git a/docs/src/assets/figures/playing-pixels-4.png b/docs/src/assets/figures/playing-pixels-4.png new file mode 100644 index 00000000..7841b76f Binary files /dev/null and b/docs/src/assets/figures/playing-pixels-4.png differ diff --git a/docs/src/assets/figures/playing-pixels-5.png b/docs/src/assets/figures/playing-pixels-5.png new file mode 100644 index 00000000..c5e365b4 Binary files /dev/null and b/docs/src/assets/figures/playing-pixels-5.png differ diff --git a/docs/src/howto/createdrawings.md b/docs/src/howto/createdrawings.md index 151f32fc..517f418d 100644 --- a/docs/src/howto/createdrawings.md +++ b/docs/src/howto/createdrawings.md @@ -6,7 +6,7 @@ To create a drawing, and optionally specify the filename, type, and dimensions, To finish a drawing and close the file, use [`finish`](@ref). -Ff the drawing doesn't appear automatically in your notebook or editing environment, you can type [`preview`](@ref) to see it. +If the drawing doesn't appear automatically in your notebook or editing environment, you can type [`preview`](@ref) to see it. ![jupyter](../assets/figures/jupyter.png) @@ -18,6 +18,20 @@ If you're using VS Code, then PNG and SVG drawings should automatically appear i SVGs are text-based, and can get quite big. Up to a certain size, SVGs will preview as easily and quickly as PNGs. As they get larger, though, they'll take longer, and it won't be long before they'll take longer to preview than to create in the first place. Very large drawings in SVG format might not display at all. +In Julia, the result of the most recently evaluated expression is usually displayed in a suitable form for the current environment. If the most recent expression was a Luxor drawing, it will probably be displayed somewhere, unless you're working in the REPL. So this code: + +```julia +Drawing(100, 100, :png) +origin() +sethue("green") +circle(O, 10, :fill) +finish() +preview() +println("finished") +``` + +won't display a drawing with a circle - the most recent result is returned by `println()`, not `preview()`. + ## Quick drawings with macros The [`@draw`](@ref), [`@svg`](@ref), [`@drawsvg`](@ref), [`@png`](@ref), and [`@pdf`](@ref) macros are designed to let you quickly create graphics without having to provide the usual boiler-plate functions. @@ -96,7 +110,7 @@ Drawing(width, height, surfacetype, [filename]) lets you supply `surfacetype` as a symbol (`:svg`, `:png`, `:image`, or `:rec`). This creates a new drawing of the given surface type and stores the image only in memory if no `filename` is supplied. -The [`@draw`](@ref) macro (equivalent to `Drawing(..., :png)` creates a PNG drawing in-memory (not saved in a file). You should see it displayed if you're working in a suitable environment (VSCode, Jupyter, Pluto). +The [`@draw`](@ref) macro (equivalent to `Drawing(..., :png)`) creates a PNG drawing in-memory (not saved in a file). You should see it displayed if you're working in a suitable environment (VSCode, Jupyter, Pluto). The SVG equivalent of `@draw` is [`@drawsvg`](@ref). @@ -184,7 +198,11 @@ end ### Using Jupyter notebooks (IJulia and Interact) -Currently, you should use an in-memory SVG drawing to display graphics if you're using Interact.jl. This example provides an HSB color widget. +This example uses Interact.jl to provide an HSB color widget. + +!!! note + + Interact.jl may be obsolete by the time you read this. ```julia using Interact, Colors, Luxor @@ -235,7 +253,7 @@ finish() As a standard Julia array, `A` will be shown as an image in your notebook or editor if you're using Images.jl. !!! note - + In this example the array uses Julian "column-major" array addressing. Luxor functions use the `Point` type on a Cartesian coordinate system, where the origin is (by default) at the top left of the drawing. Locations along the x-axis correspond to the column indices of the array, locations along the y-axis correspond to the rows of the array (until you use the `origin()` function, for example). You can also create a pixel array on a PNG drawing when saved as a file: diff --git a/docs/src/howto/polygons.md b/docs/src/howto/polygons.md index d2e3667c..abecf1e5 100644 --- a/docs/src/howto/polygons.md +++ b/docs/src/howto/polygons.md @@ -10,35 +10,35 @@ You can store a path in a Path type, which contains path elements. Luxor also provides a BezierPath type, which is an array of four-point tuples, each of which is a Bézier cubic curve section. -|create |convert |draw |info |edit | -|:--- |:--- |:--- |:--- |:--- | -| *polygons* | | | | | -|[`ngon`](@ref) |[`polysmooth`](@ref) |[`poly`](@ref) |[`isinside`](@ref) |[`simplify`](@ref) | -|[`ngonside`](@ref) |[`polytopath`](@ref) |[`prettypoly`](@ref) |[`polyperimeter`](@ref) |[`polysplit`](@ref) | -|[`star`](@ref) |[`polyintersect`](@ref) |[`polysmooth`](@ref) |[`polyarea`](@ref) |[`polyportion`](@ref) | -|[`polycross`](@ref) | | |[`polycentroid`](@ref) |[`polyremainder`](@ref) | -|[`offsetpoly`](@ref) | | |[`BoundingBox`](@ref) |[`polysortbyangle`](@ref) | -|[`hypotrochoid`](@ref) | | |[`ispolyclockwise`](@ref)|[`polysortbydistance`](@ref)| -|[`epitrochoid`](@ref) | | |[`ispolyconvex`](@ref) |[`polyclip`](@ref) | -|[`polyrotate!`](@ref) | | |[`ispointonpoly`](@ref) |[`polymove!`](@ref) | -|[`polyfit`](@ref) | | | |[`polyscale!`](@ref) | -|[`polyhull`](@ref) | | | |[`polyreflect!`](@ref) | -|[`polysuper`](@ref) | | | |[`polysample`](@ref) | -|[`polybspline`](@ref) | | | |[`polytriangulate`](@ref) | -| | | | |[`insertvertices!`](@ref) | -| | | | |[`polymorph`](@ref) | -| | | | | | -| | | | | | -| *paths* | | | | | -|[`storepath`](@ref) | | | | | -|[`getpath`](@ref) |[`pathtopoly`](@ref) |[`drawpath`](@ref) |[`pathlength`](@ref) |[`pathsample`](@ref) | -|[`getpathflat`](@ref) | | | | | -| *Bezier paths* | | | | | -|[`makebezierpath`](@ref) |[`pathtobezierpaths`](@ref)|[`drawbezierpath`](@ref) | |[`trimbezier`](@ref) | -|[`pathtobezierpaths`](@ref) |[`bezierpathtopoly`](@ref) |[`brush`](@ref) | |[`splitbezier`](@ref) | -|`BezierPath` |[`bezierpathtopath`](@ref) |[`bezigon`](@ref) | | | -|`BezierPathSegment` | | | | | -|[`beziersegmentangles`](@ref) | | | | | +|create |convert |draw |info |edit | +|:--- |:--- |:--- |:--- |:--- | +| *polygons* | | | | | +|[`ngon`](@ref) |[`polysmooth`](@ref) |[`poly`](@ref) |[`isinside`](@ref) |[`simplify`](@ref) | +|[`ngonside`](@ref) |[`polytopath`](@ref) |[`prettypoly`](@ref) |[`polyperimeter`](@ref) |[`polysplit`](@ref) | +|[`star`](@ref) |[`polyintersect`](@ref) |[`polysmooth`](@ref) |[`polyarea`](@ref) |[`polyportion`](@ref) | +|[`polycross`](@ref) | | |[`polycentroid`](@ref) |[`polyremainder`](@ref) | +|[`offsetpoly`](@ref) | | |[`BoundingBox`](@ref) |[`polysortbyangle`](@ref) | +|[`hypotrochoid`](@ref) | | |[`ispolyclockwise`](@ref) |[`polysortbydistance`](@ref)| +|[`epitrochoid`](@ref) | | |[`ispolyconvex`](@ref) |[`polyclip`](@ref) | +|[`polyrotate!`](@ref) | | |[`ispointonpoly`](@ref) |[`polymove!`](@ref) | +|[`polyfit`](@ref) | | |[`polysidelengths`](@ref) |[`polyscale!`](@ref) | +|[`polyhull`](@ref) | | | |[`polyreflect!`](@ref) | +|[`polysuper`](@ref) | | | |[`polysample`](@ref) | +|[`polybspline`](@ref) | | | |[`polytriangulate`](@ref) | +| | | | |[`insertvertices!`](@ref) | +| | | | |[`polymorph`](@ref) | +| | | | | | +| | | | | | +| *paths* | | | | | +|[`storepath`](@ref) | | | | | +|[`getpath`](@ref) |[`pathtopoly`](@ref) |[`drawpath`](@ref) |[`pathlength`](@ref) |[`pathsample`](@ref) | +|[`getpathflat`](@ref) | | | | | +| *Bezier paths* | | | | | +|[`makebezierpath`](@ref) |[`pathtobezierpaths`](@ref)|[`drawbezierpath`](@ref) | |[`trimbezier`](@ref) | +|[`pathtobezierpaths`](@ref) |[`bezierpathtopoly`](@ref) |[`brush`](@ref) | |[`splitbezier`](@ref) | +|`BezierPath` |[`bezierpathtopath`](@ref) |[`bezigon`](@ref) | | | +|`BezierPathSegment` | | | | | +|[`beziersegmentangles`](@ref) | | | | | ## Regular polygons ("ngons") @@ -1327,9 +1327,9 @@ nothing # hide rather than open, ie whether that last segment joining the end point to the first is used for calculations. -### Polygon side lengths +### Polygon sides -`polydistances` returns an array of the accumulated side lengths of a polygon. +`polydistances` returns an array of the accumulated side lengths of a polygon. `polysidelengths()` returns an array of the side lengths without summing them. ```julia-repl julia> p = ngon(O, 100, 7, 0, vertices=true); @@ -1343,9 +1343,22 @@ julia> polydistances(p) 433.8837391175581 520.6604869410697 607.4372347645814 + +julia> polysidelengths(p) +7-element Vector{Float64}: + 86.77674782351163 + 86.77674782351163 + 86.77674782351161 + 86.77674782351161 + 86.77674782351163 + 86.77674782351161 + 86.77674782351166 + +julia> sum(polysidelengths(p)) +607.4372347645814 ``` -It's used by [`polyportion`](@ref) and [`polyremainder`](@ref), and you can pre-calculate and pass them to these functions via keyword arguments for performance. By default the result includes the final closing segment (`closed=true`). +`polydistances()` is used by [`polyportion`](@ref) and [`polyremainder`](@ref), and you can pre-calculate and pass them to these functions via keyword arguments for performance. By default the result includes the final closing segment (`closed=true`). These functions also make use of the [`nearestindex`](@ref), which returns a tuple of: the index of the nearest value in an array of distances to a given value; and the excess value. diff --git a/docs/src/howto/simplegraphics.md b/docs/src/howto/simplegraphics.md index 96adb845..8828efee 100644 --- a/docs/src/howto/simplegraphics.md +++ b/docs/src/howto/simplegraphics.md @@ -8,9 +8,9 @@ DocTestSetup = quote In Luxor, there are different ways of working with graphical items: -- Draw them immediately. Create lines and curves to build a **path** on the drawing. When you paint the path, the graphics are ‘fixed’, and you move on to the next. +- Draw them immediately. Add points and control points defining curves to build a **path** on the drawing. When you paint the path, the graphics are drawn, and ‘fixed’, and you move on to the next. -- Construct arrays of points - **polygons** - which you can draw at some later point. Watch out for a `vertices=true` option, which returns coordinate data rather than adding shapes to the current path. +- Construct arrays of points - **polygons** - which you can draw at some later stage. Watch out for a `vertices=true` option, which returns coordinate data rather than adding shapes to the current path. Polygons are copied to paths when they're drawn. - You can combine these two approaches: create a path from lines and curves (and jumps), then store the path, ready for drawing later on. @@ -155,7 +155,7 @@ The [`squircle`](@ref) function makes nicer shapes. ## Triangles, pentagons, and regular polygons -For regular polygons, pentagons, and so on, see the section on [Polygons and paths](@ref). If you like drawing hexagons, you could also read [Hexagonal grids](@ref). +For regular polygons, pentagons, and so on, see the section on [Polygons and paths](@ref). If you like drawing hexagons, you could also read [Hexagonal grids](@ref). Eqilateral Triangular grids are also provided ([EquilateralTriangleGrid](@ref)). ## Circles and ellipses @@ -333,7 +333,7 @@ nothing # hide ![ellipse in quadrilateral](../assets/figures/ellipseinquad.png) -[`circlepath`](@ref) constructs a circular path from Bézier curves, which allows you to use circles as paths. +[`circlepath`](@ref) constructs a circular path from Bézier curves. ```@example using Luxor # hide @@ -547,7 +547,7 @@ These last two functions can return 0, 1, or 2 points (since there are often two ```@example using Luxor # hide @drawsvg begin # hide - background("grey10") + background("antiquewhite") sethue("rebeccapurple") setline(3) circle(Point(0, 0), 200, :stroke) @@ -645,7 +645,22 @@ nothing # hide ![ruling lines clipped to bounding boxes](../assets/figures/rulebbox.png) -Another method of `rule()` draws a line through two points. This example rules lines passing through each pair of adjacent points of the edge of a squircle curve. +Another method of `rule()` draws a line through two points. + +```@example +using Luxor, Colors # hide + +@drawsvg begin + background("antiquewhite") + sethue("black") + pt1 = Point(-100, -50) + pt2 = Point(200, 120) + circle.((pt1, pt2), 10, :fill) + rule(pt1, pt2) +end 800 500 +``` + +This next example rules lines passing through each pair of adjacent points of the edge of a squircle curve. ```@example using Luxor, Colors diff --git a/docs/src/howto/tables-grids.md b/docs/src/howto/tables-grids.md index 5a77473f..d206d6e2 100644 --- a/docs/src/howto/tables-grids.md +++ b/docs/src/howto/tables-grids.md @@ -5,7 +5,7 @@ end ``` # Tables and grids -You often want to position graphics at regular locations on the drawing. The positions can be provided by: +You often want to position graphics at regularly-spaced locations on the drawing. These positions can be provided by: - `Tiler`: a rectangular grid iterator which you specify by enclosing area, and the number of rows and columns - `Partition`: a rectangular grid iterator which you specify by enclosing area, and the width and height of each cell @@ -14,9 +14,7 @@ You often want to position graphics at regular locations on the drawing. The pos These are types which act as iterators. Their job is to provide you with centerpoints; you'll probably want to use these in combination with the cell's widths and heights. -There are functions to make hexagonal grids. See [Hexagonal grids](@ref). - -There is [EquilateralTriangleGrid](@ref), a grid iterator which generates the vertices for a rectangular grid of equilateral triangles. +There are also functions to make hexagonal grids ([Hexagonal](@ref) grids and [EquilateralTriangleGrid](@ref) grids. ## Tiles and partitions diff --git a/docs/src/tutorial/basicpath.md b/docs/src/tutorial/basicpath.md index 1445b167..dc1cd171 100644 --- a/docs/src/tutorial/basicpath.md +++ b/docs/src/tutorial/basicpath.md @@ -5,7 +5,7 @@ end ``` # Basic path construction -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. +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 much 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 @@ -83,15 +83,15 @@ We could have used `line(Point(200, 0))` rather than `closepath()`, but `closepa So, now we've constructed and finished a path - but now we must decide what to do with it. Above, 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). -After you've rendered the path by stroking or filling it, the current path is empty again. +After you've rendered the path by stroking or filling it, the current path is empty again, ready for the next set of instructions. 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). +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()`, `rule`, `crescent`, `squircle`, etc. See [Simple graphics](@ref) for a few of the more basic ones. ## Arcs -There are `arc()` and `carc()` (counterclockwise arc) functions that provide the ability to add circular arcs to the current path, just as `curve()` adds a Bézier curve. However, these need careful handling. Consider this drawing: +There are `arc()` and `carc()` (counterclockwise arc) functions that provide the ability to add circular arcs to the current path, just as `curve()` adds a Bézier curve. However, these need careful handling, because Luxor will insert lines sometimes that you didn't expect. Consider this drawing: ```@example using Luxor @@ -107,9 +107,9 @@ 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. After moving down to (100, 200), the calculated start point for the arc isn't (100, 200), but (70, 0). So a straight line is drawn from the current point (100, 200) back up to the arc's starting point (70, 0) - this was automatically added to the path, even though you didn't specify a line. +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 extra straight line had to be inserted to go from the current point (100, 200) back up to the arc's starting point (70, 0) - this was automatically added to the path, even though you didn't specify it. -Internally, circular arcs are converted to Bézier curves. +Internally, circular arcs are converted to Bézier curves by the Cairo engine. ## Relative coordinates @@ -125,7 +125,7 @@ using Luxor move(0, 0) - rline(Point(120, 0)) + rline(Point(120, 0)) rline(Point(0, 120)) rline(Point(-120, 0)) rline(Point(0, -120)) @@ -134,7 +134,7 @@ using Luxor rmove(150, 0) - rline(Point(120, 0)) + rline(Point(120, 0)) rline(Point(0, 120)) rline(Point(-120, 0)) rline(Point(0, -120)) @@ -145,15 +145,15 @@ using Luxor end ``` -The drawing instructions to make the two shapes are the same, the second is just moved 150 units in x. +The drawing instructions to make the two shapes are the same, the second set are applied shifted 150 units in x. `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*. +Notice that this code draws two shapes, but there was only one `strokepath()` function call. These two shapes are in fact *subpaths*. A path can contain a number of separate shapes. ## Subpaths -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. +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 in the same way, and then the current path is emptied. 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. @@ -220,9 +220,9 @@ end ## Translate, scale, rotate -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. +Suppose you want to repeat a path in various places on the drawing. Obviously you don't want to write the same code over and over again. -In this example, the `t()` function draws a triangle path. +In this example, the `t()` function draws a triangle path relative to the current (0, 0) point. ```@example using Luxor @@ -241,9 +241,9 @@ end end ``` -Inside `t()`, the coordinates are interpreted relative to the current graphics state: the current origin position (0, 0), scale (1), and rotation (0°). +Inside `t()`, the coordinates, and other settings, are interpreted relative to the current graphics state: the current origin position (0, 0), scale (1), and rotation (0°), and so on. -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. +To draw the triangle in another location, you first use `translate()` to shift the (0, 0) origin to another location. Now the `move()` and `line()` calls inside the `t()` function all automatically refer to the new location. ```@example using Luxor @@ -340,11 +340,13 @@ end As an alternative to `gsave()` and `grestore()` you can use the `@layer begin ... end` macro, which does the same thing. +The other way to manipulate the graphics state (position, translation, rotation, scale) is to learn about the current transformation matrix. See [Transforms and matrices](@ref) for more. + ## Useful tools 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. +`rulers()` is useful for drawing the current x and y axes before you start a path. (For obvious reasons it's not useful while you're in the middle of drawing your path, becaause it applies its own set of paths.) `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. @@ -364,7 +366,7 @@ strokepath() ## Polygonal thinking -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. +In Luxor, a polygon is an array (a standard Julia vector) of Points. You can treat it like any standard Julia array. When you want to draw it (or use it for clipping), you use the `poly()` function to make a path. It's all straight lines, no curves, so you might have to use a lot of points to get smooth curves. @@ -382,11 +384,11 @@ using Luxor end ``` -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. +You might find it easier to generate your graphics using polygons than to generate paths by issuing lots of path functions. 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: +There are some Luxor functions that let you modify all the points in a polygon in various ways: - `polymove!(pgon, pt1, pt2)` diff --git a/docs/src/tutorial/helloworld.md b/docs/src/tutorial/helloworld.md index 9efae043..90f12dd0 100644 --- a/docs/src/tutorial/helloworld.md +++ b/docs/src/tutorial/helloworld.md @@ -11,7 +11,7 @@ If you're familiar with the basics of Cairo, PostScript, Processing, or similar If you've already downloaded Julia, and have added the Luxor package successfully (using `] add Luxor`): -```plain +```julia-repl $ julia _ _ _ _(_)_ | Documentation: https://docs.julialang.org @@ -27,13 +27,13 @@ $ julia then you're ready to start. -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. +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, although the graphics will disappoint. Ready? Let's begin. ## First steps -We'll have to load just one package for this tutorial: +We'll load just one package for this tutorial: ```julia using Luxor @@ -63,13 +63,19 @@ What happened? Can you see this image somewhere? ![point example](../assets/figures/tutorial-hello-world.png) -If you're using VS-Code and run the file in the Julia REPL, the image should appear in the Plots window. The preview will not be shown if there is any executed code following the macro. If you're working in a Jupyter or Pluto notebook, the image should appear below or above the code. If you're using Julia in a terminal or text editor, the image should have opened up in some other application, or, at the very least, it should have been saved in your current working directory (as `luxor-drawing-(time stamp).png`). If nothing happened, or if something bad happened, we've got some set-up or installation issues probably unrelated to Luxor... +If you're using VS-Code and run the file in the Julia REPL, the image should appear in the Plots window. The preview will not be shown if there is any executed code following the macro. + +If you're working in a Jupyter or Pluto notebook, the image should appear below or above the code. If you're using Julia in a terminal or text editor, the image should have opened up in some other application, or, at the very least, it should have been saved in your current working directory (as `luxor-drawing-(time stamp).png`). + +If you're working in the REPL or an editor, you should see the text-only representation: `Luxor drawing: (type = :png, width = 100.0, height = 100.0, location = in memory)`. + +If nothing happened, or if something bad happened, we've got some set-up or installation issues probably unrelated to Luxor... !!! note 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), [`background`](@ref), [`finish`](@ref), and [`preview`](@ref) functions.) There are also [`@svg`](@ref), [`@pdf`](@ref), [`@draw`](@ref), and [`@drawsvg`](@ref) macros, which do similar things. -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. +PNGs and SVGs are good because they show up in VS-Code, Jupyter, and Pluto. Lines in SVG graphics are usually drawn in higher quality, but the SVG contents of the files are readable text, and can become very large and difficult to load if the image is complex. PDF documents are always high quality, like SVGs, and usually open up in a separate application. This example illustrates a few things about Luxor drawings: @@ -114,4 +120,3 @@ finish() The x-coordinates usually run from left to right, the y-coordinates from top to bottom. So here, `Point(0, 250)` is a point at the left/right center, but at the bottom of the drawing. The `rulers()` function draws CAD-style x and y rulers at the origin. - diff --git a/docs/src/tutorial/pixels.md b/docs/src/tutorial/pixels.md index 723116fa..aa2a2818 100644 --- a/docs/src/tutorial/pixels.md +++ b/docs/src/tutorial/pixels.md @@ -1,24 +1,24 @@ ```@meta DocTestSetup = quote - using Luxor, Colors, Images + using Luxor, Colors end ``` # Playing with pixels As well as working with PNG, SVG, and PDF drawings, Luxor lets you play directly with pixels, and combine these freely with vector graphics and text. -This section is a quick walkthrough of some functions you can use to control pixels. You'll need the Images.jl package as well as Luxor.jl. +This section is a quick walkthrough of some functions you can use to control pixels. You'll probably need the Images.jl package as well as Luxor.jl. -When you create a new Luxor drawing with [`Drawing()`](@ref), you can choose to use the contents of an existing Julia array as the drawing surface, rather than a PNG or SVG. +When you create a new Luxor drawing with [`Drawing()`](@ref), you can choose to use the contents of an existing Julia array as the drawing surface, rather than ask for a new PNG or SVG. -```@example blocks -using Luxor, Colors, Images +```@julia +using Luxor, Colors A = zeros(ARGB32, 400, 800) Drawing(A) nothing # hide ``` -The array `A` should be a matrix where each element is an ARGB32 value. ARGB32 is a way of fitting four integers (using 8 bits for alpha, 8 bits for Red, 8 for Green, and 8 for Blue) into a 32-bit number between 0 and 4,294,967,296 - a 32 bit unsigned integer. The ARGB32 type is provided by the Images.jl package (or ColorTypes.jl). +The array `A` should be a matrix where each element is an *ARGB32& value. ARGB32 is a way of fitting four small integers (using 8 bits for alpha, 8 bits for Red, 8 for Green, and 8 for Blue) into a 32-bit number between 0 and 4,294,967,296 - each element is a 32 bit unsigned integer that represents a colored pixel. The ARGB32 type is provided by the Images.jl package (or ColorTypes.jl). You can set and get the values of pixels by treating the drawing's array like a standard Julia array. So we can inspect pixels like this: @@ -41,15 +41,17 @@ A[300:350, 50:450] .= colorant"blue"; [A[rand(1:(400 * 800))] = RGB(rand(), rand(), rand()) for i in 1:800]; ``` -Because this is an array rather than a PNG/SVG, we could either use Images.jl to display it in a notebook or code editor such as VS-Code. +Because this is an array rather than a PNG/SVG, we must either use Images.jl to display it in a notebook or code editor such as VS-Code. -```@example blocks +```julia blocks A ``` +![playing with pixels tutorial 1](../assets/figures/playing-pixels-1.png) + Or, to display it in Luxor, start a new drawing, and use [`placeimage()`](@ref) to position the array on the drawing: -```@example +```julia using Luxor, Colors # hide A = zeros(ARGB32, 400, 800) Drawing(A) @@ -66,9 +68,11 @@ finish() preview() ``` +![playing with pixels tutorial 2](../assets/figures/playing-pixels-2.png) + Here's some code that sets the HSV color of each pixel to the value of some arbitrary maths function operating on complex numbers: -```@example +```julia using Luxor, Colors, Images A = zeros(ARGB32, 400, 800) Drawing(A) @@ -91,13 +95,15 @@ finish() A ``` +![playing with pixels tutorial 3](../assets/figures/playing-pixels-3.png) + ## Rows and columns, height and width -A quick note about the two coordinate systems at work here. +A note about the two coordinate systems at work here. -Locations in arrays and images are typically specified with row and column values, or perhaps just a single index value. Because arrays are column-major in Julia, the address `A[10, 200]` is "row 10, column 200 of A". +Locations in arrays and images are typically specified with row and column values, or perhaps just a single index value. Because arrays are column-major in Julia, the address `A[10, 200]` is "row 10, column 200 of A", with a single index `A[79610]`. -Locations on plots and Luxor drawings are typically specified as Cartesian x and y coordinates. So `Point(10, 200)` would identify a point 10 units along the x-axis and 200 along the y-axis from the origin, In Luxor, like most computer graphics systems, the y-axis points vertically down the drawing. +Locations on plots and Luxor drawings are typically specified as Cartesian x and y coordinates. So `Point(10, 200)` would identify a point 10 units along the x-axis and 200 along the y-axis from the origin, In Luxor, like most computer graphics systems, the y-axis points vertically *down* the drawing. The origin point of an array or image is the first pixel - usually drawn at the top left. In Luxor, the origin point (0/0) of a drawing (if created by `Drawing()`) is also at the top left, until you move it, or use the `origin()` function. (The `@draw` macros also set the origin at the centre of the drawing, for your convenience.) @@ -134,13 +140,15 @@ text("f(z) = (z + 3)^3 / ((z + 2im) * (z - 2im)^2)", A ``` +![playing with pixels tutorial 4](../assets/figures/playing-pixels-4.png) + A disadvantage is that `BoundingBox()` functions don't work, because they're not yet aware of transformation matrices. ## Array as image An alternative way of using pixel arrays is to add them to a PNG or SVG drawing using [`placeimage`](@ref). -For example, create a new drawing, and this time add the array A to the drawing. You can use it like any other image, such as clipping it by the Julia logo: +For example, create a new drawing, and this time place the array A on the drawing. You can use it like any other image, such as clipping it by the Julia logo: ```julia # with A defined as above @@ -158,44 +166,4 @@ finish() preview() ``` -```@setup final_example -using Luxor, Colors -A = zeros(ARGB32, 400, 800) -w, h = 800, 400 -Drawing(A) -f(z) = (z + 3)^3 / ((z + 2im) * (z - 2im)^2) -function pixelcolor(r, c; - rows=100, - cols=100) - z = rescale(r, 1, rows, -2π, 2π) + rescale(c, 2π, cols, -2π, 2π) * im - n = f(z) - h = 360rescale(angle(n), 0, 2π) - s = abs(sin(π / 2 * real(f(z)))) # * (sin(π * imag(f(z))))) - v = abs(sin(2π * real(f(z)))) - return HSV(h, s, v) -end -for r in 1:size(A, 1), c in 1:size(A, 2) - A[r, c] = pixelcolor(r, c, rows=400, cols=800) -end -transform([0 1 1 0 h/2 w/2]) -fontsize(18) -sethue("white") -text("f(z) = (z + 3)^3 / ((z + 2im) * (z - 2im)^2)", - O + (0, h / 2 - 20), halign=:center) -finish() - -d = Drawing(800, 400, :png) -origin() -placeimage(A, O - (w / 2, h / 2), alpha=0.4) -julialogo(centered=true, action=:clip) -placeimage(A, O - (w / 2, h / 2)) -clipreset() -julialogo(centered=true, action=:path) -sethue("white") -strokepath() -finish() -``` - -```@example final_example -d # hide -``` +![playing with pixels tutorial 5](../assets/figures/playing-pixels-5.png) diff --git a/docs/src/tutorial/quickstart.md b/docs/src/tutorial/quickstart.md index c79760e0..de8056f0 100644 --- a/docs/src/tutorial/quickstart.md +++ b/docs/src/tutorial/quickstart.md @@ -8,11 +8,11 @@ end ## Logo beginnings The new (and currently fictitious) organization JuliaFission -has just asked you to design a new logo for them. They're -something to do with atoms and particles, perhaps? So we'll +has just asked you to design a new logo for them. They’re +something to do with atoms and particles, perhaps? So we’ll design a new logo for them using some basic shapes; perhaps colored circles in some kind of spiral formation would look -suitably "atomic". +suitably “atomic”. Let's try out some ideas. @@ -50,7 +50,7 @@ This short piece of code does the following things: - finishes the drawing and displays it on the screen -In case you're wondering, the units are *points* (as in font sizes), and there are 72 points in an inch, just over 3 per millimeter. The y-axis points down the page. If you want to be reminded of where the x and y axes are, use the [`rulers`](@ref) function. +In case you’re wondering, the units are *points* (as in font sizes), and there are 72 points in an inch, just over 3 per millimeter. The y-axis points down the page. If you want to be reminded of where the x and y axes are, use the [`rulers`](@ref) function. The `action=:fill` at the end of [`circle`](@ref) uses one of a set of symbols that let you use the shape you've created in different ways. There's the `:stroke` action, which draws around the edges but doesn't fill the shape with color. You might also meet the `:fillstroke`, `:fillpreserve`, `:strokepreserve`, `:clip`, and `:path` actions. @@ -86,9 +86,9 @@ svgimage # hide Notice the "." after `circle`. This broadcasts the `circle()` function over the `corners`, thus drawing a 10-unit red-filled circle at every point. -The arguments to `ngon` are usually centerpoint, radius, and the number of sides. Try changing the third argument from 3 (triangle) to 4 (square) or 31 (traikontagon?). +The arguments to `ngon` are centerpoint, radius, and the number of sides. Try changing the third argument from 3 (triangle) to 4 (square) or 31 (traikontagon?). -To create a spiral of circles, we want to repeat this "draw a circle at each vertex of a triangle" procedure more than once. A simple loop will do: we'll rotate the current drawing context by `i * ` 5° (`deg2rad(5)` radians) each time (so 5°, 10°, 15°, 20°, 25°, and 30°), and at the same time increase the size of the polygon by multiples of 10: +To create a spiral of circles, we want to repeat this “draw a circle at each vertex of a triangle” procedure more than once. A simple loop will do: we’ll rotate the current drawing context by `i * ` 5° (`deg2rad(5)` radians) each time (so 5°, 10°, 15°, 20°, 25°, and 30°), and at the same time increase the size of the polygon by multiples of 10: ```@setup example_3 using Luxor diff --git a/docs/src/tutorial/simple-animation.md b/docs/src/tutorial/simple-animation.md index 3acd0109..fb7af9e0 100644 --- a/docs/src/tutorial/simple-animation.md +++ b/docs/src/tutorial/simple-animation.md @@ -196,3 +196,44 @@ function frame(scene, framenumber) end end ``` + +## Live graphics + +Although Luxor is designed primarily for creating static graphics, it's possible to run it with an external buffer to display moving graphics. This example finds an approximation to π, updating the window continually with the latest estimate: + +```julia +using Luxor +using MiniFB +include(dirname(pathof(Luxor)) * "/play.jl") + +function run() + within_circle = 0 + total_points = 0 + @play 400 400 begin + pt = Point(rand(), rand()) + total_points += 1 + d = distance(O, pt) + if d <= 1 + randomhue() + circle(200pt, 1, :fill) + within_circle += 1 + end + pi_estimate = 4.0within_circle / total_points + # show estimate + sethue("black") + box(boxtopleft(), boxmiddleright(), :fill) + sethue("white") + fontsize(30) + text(string(pi_estimate), boxtopleft() + (10, 50), halign=:left) + fontsize(20) + text(string(total_points), boxtopleft() + (10, 100), halign=:left) + sleep(0.05) + end +end + +run() +``` + +![pi live drawing](../assets/figures/minifb-pi.png) ![a few moments later...](../assets/figures/minifb-pi-1.png) + +For more information, see [Interactive graphics and Threads](@ref). \ No newline at end of file diff --git a/src/Luxor.jl b/src/Luxor.jl index 79e31973..6a614159 100644 --- a/src/Luxor.jl +++ b/src/Luxor.jl @@ -117,7 +117,7 @@ export Drawing, poly, simplify, polycentroid, polysortbyangle, polyhull, polysortbydistance, offsetpoly, polyfit, currentpoint, polybspline, hascurrentpoint, getworldposition, anglethreepoints, polyperimeter, polydistances, polyportion, - polyremainder, nearestindex, polyarea, polysample, + polyremainder, nearestindex, polyarea, polysample, polysidelengths, insertvertices!, polymove!, polyscale!, polyrotate!, polyreflect!, @polar, polar, strokepreserve, fillpreserve, gsave, grestore, @layer, scale, rotate, translate, diff --git a/src/hexagons.jl b/src/hexagons.jl index b1fd493b..313146a0 100644 --- a/src/hexagons.jl +++ b/src/hexagons.jl @@ -34,6 +34,19 @@ h = HexagonOffsetOddR(q, 0, 100) poly(hextile(h), :fill) ``` +This code draws a 5 by 5 hexagonal grid: + +```julia +@draw begin + for q in -2:2 # horizontal + for r in -2:2 # vertical + pgon = hextile(HexagonOffsetOddR(q, r, 40)) + poly(pgon, :stroke, close = true) + end + end +end +``` + Functions: - `hextile(hex::Hexagon)` - return the six vertices diff --git a/src/polygons.jl b/src/polygons.jl index 484dfc88..ad400e46 100644 --- a/src/polygons.jl +++ b/src/polygons.jl @@ -1728,3 +1728,41 @@ end polymorph(pgon1::Vector{Point}, pgon2::Array{Vector{Point}}, k; kwargs...) = begin polymorph([pgon1], pgon2, k; kwargs...) end + +""" + polysidelengths(p::Vector{Point}; closed = true) + +Return an array containing the lengths of each "side" of the polygon `p`. + +If `closed = false`, the side joining the last point to the first is not included. + +```julia +polysidelengths(ngon(O, 100, 4)) +4-element Vector{Float64}: + 141.42135623730948 + 141.4213562373095 + 141.4213562373095 + 141.42135623730954 + +polysidelengths(ngon(O, 100, 4), closed=false) +3-element Vector{Float64}: + 141.42135623730948 + 141.4213562373095 + 141.4213562373095 +``` +""" +function polysidelengths(p::Vector{Point}; closed = true) + l = length(p) + r = Float64[] + sizehint!(r, l) + t = 0.0 + @inbounds for i in 1:(l - 1) + t = distance(p[i], p[i + 1]) + push!(r, t) + end + if closed + t = distance(p[end], p[1]) + push!(r, t) + end + return r +end \ No newline at end of file diff --git a/test/polysidelengths-test.jl b/test/polysidelengths-test.jl new file mode 100644 index 00000000..9a0ef6ef --- /dev/null +++ b/test/polysidelengths-test.jl @@ -0,0 +1,37 @@ +#!/usr/bin/env julia + +using Luxor, Test, Random + +Random.seed!(42) + +function main() + w, h = 600, 600 + + fname = "polysidelengths-test.png" + Drawing(w, h, fname) + background("white") + origin() + setline(0.5) + + pg = ngon(O, 100, 4, vertices = true) + + r = polysidelengths(pg) + @test all(isapprox(141.42135623), r) + @test length(r) == 4 + + r = polysidelengths(pg, closed = false) + @test all(isapprox(141.42135623), r) + @test length(r) == 3 + + for S in 3:10 + D = rand(9:173) + pg = ngonside(rand(BoundingBox()), D, S, vertices = true) + @show sum(polysidelengths(pg)), S * D + @test isapprox(sum(polysidelengths(pg)), S * D) + end + + @test finish() == true + println("...finished test: output in $(fname)") +end + +main() \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 05aa9725..e6f8cbd8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -47,6 +47,7 @@ function run_all_tests() include("polysplit.jl") include("polysplit-2.jl") include("polysmooth-tests.jl") + include("polysidelengths-test.jl") include("pretty-poly-test.jl") include("simplify-polygons.jl") include("spirals.jl")