diff --git a/CHANGELOG.md b/CHANGELOG.md index f144c82c..f516d7a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,12 @@ See https://github.com/JuliaGraphics/Cairo.jl/pull/357. - `getcolor()` gets current color -- multiply Point by 3×3 matrix using `*` +- multiply Point by 3×3 matrix using `*` ### Changed +- added more information to doc strings + ### Removed ### Deprecated diff --git a/docs/src/explanation/basics.md b/docs/src/explanation/basics.md index 86a51b18..db9e7b22 100644 --- a/docs/src/explanation/basics.md +++ b/docs/src/explanation/basics.md @@ -220,7 +220,19 @@ nothing # hide [`gsave`](@ref) saves a copy of the current graphics settings (current axis rotation, position, scale, line and text settings, color, and so on). When the next [`grestore`](@ref) is called, all changes you've made to the graphics settings will be discarded, and the previous settings are restored, so things return to how they were when you last used [`gsave`](@ref). [`gsave`](@ref) and [`grestore`](@ref) should always be balanced in pairs, enclosing the functions. -The `@layer` macro is a synonym for a [`gsave`](@ref)...[`grestore`](@ref) pair. +```julia +@svg begin + circle(Point(0, 0), 100, action = :stroke) + gsave() + sethue("red") + rule(Point(0, 0)) + rule(Point(0, 0), pi/2) + grestore() + circle(Point(0, 0), 200, action = :stroke) +end +``` + +The `@layer` macro is a shorter synonym for a [`gsave`](@ref)...[`grestore`](@ref) pair. ```julia @svg begin diff --git a/docs/src/explanation/transforms.md b/docs/src/explanation/transforms.md index aa370969..421a366e 100644 --- a/docs/src/explanation/transforms.md +++ b/docs/src/explanation/transforms.md @@ -165,7 +165,7 @@ Line thicknesses are not scaled by default. For example, with a current line thi ## Matrices -In Luxor, there's always a *current matrix* that determines how coordinates are interpreted in the current workspace. It's a six element array: +In Luxor, there's always a *current matrix* that determines how coordinates are interpreted in the current workspace. In Cairo, it's a six element array: ```math \begin{bmatrix} @@ -174,7 +174,7 @@ In Luxor, there's always a *current matrix* that determines how coordinates are \end{bmatrix} ``` -which is usually handled in Julia/Cairo/Luxor as a simple vector (array): +and Luxor/Cairo matrix functions accept and return simple 6-element vectors: ```julia julia> getmatrix() @@ -187,6 +187,10 @@ julia> getmatrix() 0.0 ``` +!!! note + + You can convert between the 6-element and 3x3 versions of a transformation matrix using the functions [`cairotojuliamatrix`](@ref) and [`juliatocairomatrix`](@ref). + [`transform(a)`](@ref) transforms the current workspace by ‘multiplying’ the current matrix with matrix `a`. For example, `transform([1, 0, xskew, 1, 50, 0])` skews the current matrix by `xskew` radians and moves it 50 in x and 0 in y. ```@example @@ -224,8 +228,6 @@ Other functions include [`getmatrix`](@ref), Use the [`getscale`](@ref), [`gettranslation`](@ref), and [`getrotation`](@ref) functions to find the current values of the current matrix. These can also find the values of arbitrary 3x3 matrices. -You can convert between the 6-element and 3x3 versions of a transformation matrix using the functions [`cairotojuliamatrix`](@ref) -and [`juliatocairomatrix`](@ref). ## World position diff --git a/docs/src/howto/createdrawings.md b/docs/src/howto/createdrawings.md index 4cfc5f40..d886c765 100644 --- a/docs/src/howto/createdrawings.md +++ b/docs/src/howto/createdrawings.md @@ -4,13 +4,13 @@ In Luxor you always work with a current drawing, so the first thing to do is to To create a drawing, and optionally specify the filename, type, and dimensions, use the [`Drawing`](@ref) constructor function. -To finish a drawing and close the file, use [`finish`](@ref), and, if the drawing doesn't appear in your notebook, you can launch an external application to view it using [`preview`](@ref). +To finish a drawing and close the file, use [`finish`](@ref). -To finish a drawing and close the file, use [`finish`](@ref), and, to launch an external application to view it, use [`preview`](@ref). +Ff 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) -If you're using VS Code, then PNG and SVG drawings should appear in the Plots pane, if it's enabled. In a Pluto notebook, output appears above the cell. In a notebook environment, output appears in the next notebook cell. +If you're using VS Code, then PNG and SVG drawings should automatically appear in the Plots pane, if it's enabled. In a Pluto notebook, output appears above the cell. In a notebook environment, output appears in the next notebook cell. ![juno](../assets/figures/juno.png) @@ -20,19 +20,19 @@ If you're using VS Code, then PNG and SVG drawings should appear in the Plots pa ## Quick drawings with macros -The [`@draw`](@ref), [`@svg`](@ref), [`@png`](@ref), and [`@pdf`](@ref) macros are designed to let you quickly create graphics without having to provide the usual boiler-plate functions. +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. !!! note - The macros are shortcuts, designed to make it quick and easy to get started. You can save a few keystrokes and some time, but, for full control over all parameters, use [`Drawing`](@ref). + The macros are shortcuts, designed to make it quick and easy to get started. You can save keystrokes and time, but, for full control over all parameters, use [`Drawing`](@ref). -For example, the Julia code: +For example, this: ```julia @svg circle(Point(0, 0), 20, action = :stroke) 50 50 ``` -expands to +is equivalent to this: ```julia Drawing(50, 50, "luxor-drawing-(timestamp).svg") @@ -44,7 +44,7 @@ finish() preview() ``` -They're just short-cuts. You can omit the width and height (thus defaulting to 600 by 600, except for `@imagematrix`), and you don't have to specify a filename (you'll get time-stamped files in the current working directory). For multiple lines, use either: +You can omit the width and height (thus defaulting to 600 by 600, except for `@imagematrix`), and you don't have to specify a filename (you'll get time-stamped files in the current working directory). For multiple lines, use either: ```julia @svg begin @@ -86,7 +86,7 @@ preview() ### In-memory drawings -You can choose to store drawings in memory. The advantage is that in-memory drawings are quicker, and the results can be passed as Julia data. Also, it's useful in some environments to not have to worry about writing files. +You can choose to store drawings in memory rather than use files. The advantage is that in-memory drawings are quicker, and the results can be passed as Julia data. Also, it's useful in some restricted environments to not have to worry about writing files. This syntax for the [`Drawing`](@ref) function: @@ -100,9 +100,9 @@ The [`@draw`](@ref) macro (equivalent to `Drawing(..., :png)` creates a PNG draw The SVG equivalent of `@draw` is [`@drawsvg`](@ref). -Use [`svgstring()`](@ref) to extract the SVG source for a finished SVG drawing. +Use [`svgstring()`](@ref) to extract the SVG drawing's source as text. -If you want to generate SVG without making a drawing, use `@savesvg` instead of `@drawsvg`. +If you want to generate SVG code without making a drawing, use `@savesvg` instead of `@drawsvg`. ### Concatenating SVG drawings @@ -246,4 +246,4 @@ See the [Drawings as image matrices](@ref) section for more information. ## Recordings -The `:rec` option for `Drawing()` creates a recording surface in memory. You can then use `snapshot(filename, ...)` to copy the drawing into a file. +The `:rec` option for `Drawing()` creates a recording surface in memory. You can then use `snapshot(filename, ...)` to copy the drawing into a file. See [Snapshots](@ref). diff --git a/docs/src/tutorial/basicpath.md b/docs/src/tutorial/basicpath.md index e4391cc2..fb55d5a8 100644 --- a/docs/src/tutorial/basicpath.md +++ b/docs/src/tutorial/basicpath.md @@ -44,7 +44,7 @@ So, we've constructed a path. The final job is to decide what to do with it, unl At this point, 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 all shapes 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 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. ## Arcs @@ -167,11 +167,115 @@ strokepath() 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. + +```@example +using Luxor +function t() + move(Point(100, 0)) + line(Point(0, -100)) + line(Point(-100, 0)) + closepath() + strokepath() +end + +@drawsvg begin +background("black") +sethue("white") +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. + +To draw the triangle in another location, you can use `translate()` to move the (0, 0) to another location. + +```@example +using Luxor +function t() + move(Point(100, 0)) + line(Point(0, -100)) + line(Point(-100, 0)) + closepath() + strokepath() +end + +@drawsvg begin +background("black") +sethue("white") +t() +translate(Point(150, 150)) +t() +end +``` + +Similarly, you could use `scale()` and `rotate()` which further modify the current state. + +```@example +using Luxor +function t() + move(Point(100, 0)) + line(Point(0, -100)) + line(Point(-100, 0)) + closepath() + strokepath() +end + +@drawsvg begin +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. + +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. + +```@example +using Luxor + +function t() + move(Point(100, 0)) + line(Point(0, -100)) + line(Point(-100, 0)) + closepath() + strokepath() +end + +@drawsvg begin + background("black") + sethue("white") + for x in -250:20:250, y in -250:20:250 + gsave() + translate(Point(x, y)) + scale(0.1) + rotate(rand() * 2π) + t() + grestore() + end +end +``` + +!!! note + + As an alternative to `gsave()` and `grestore()` you can use the `@layer begin ... end` macro, which does the same thing. + ## Useful tools You can use `currentpoint()` to get the current point. -`rulers()` is useful for displaying the current x and y axes. +`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()`. @@ -189,9 +293,9 @@ line(Point(100, 100)) strokepath() ``` -## The straight line alternative +## Polygonal thinking -If you just want to draw straight lines, you might also like to know about Luxor polygons. A polygon is just an array (a Julia Vector) of points, and you can draw it (as many times as you like) using functions such as `poly()`. +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. ```@example using Luxor @@ -207,4 +311,6 @@ using Luxor end ``` -Curves are nice, but eventually they'll have to be converted to line segments... +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. + +The `poly()` function simply builds a path with straight lines, and then does the `:fill` or `:stroke` action, depending on which you provide. diff --git a/docs/src/tutorial/basictutorial.md b/docs/src/tutorial/basictutorial.md index 675013b4..7f3b67bf 100644 --- a/docs/src/tutorial/basictutorial.md +++ b/docs/src/tutorial/basictutorial.md @@ -476,7 +476,7 @@ Notice that this function doesn't define anything about what color it is, or whe @png begin setopacity(0.7) for θ in range(0, step=π/6, length=12) - @layer begin + gsave() rotate(θ) translate(0, -150) egg(50, :path) @@ -486,7 +486,7 @@ Notice that this function doesn't define anything about what color it is, or whe randomhue() strokepath() - end + grestore() end end 800 800 "eggstravaganza.png" ``` @@ -527,7 +527,7 @@ background("white") origin() setopacity(0.7) for θ in range(0, step=π/6, length=12) - @layer begin + gsave() rotate(θ) translate(0, -150) egg(50, :path) @@ -537,7 +537,7 @@ for θ in range(0, step=π/6, length=12) randomhue() strokepath() - end + grestore() end finish() ``` @@ -545,7 +545,7 @@ finish() The loop runs 12 times, with `theta` increasing from 0 upwards in steps of π/6. But before each egg is drawn, the entire drawing environment is rotated by `theta` radians and then shifted along the y-axis away from the origin by -150 units (the y-axis values usually increase downwards, so, before any rotation takes place, a shift of -150 looks like an upwards shift). The [`randomhue`](@ref) function does what you expect, and the `egg` function is passed the `:fill` action and the radius. -Notice that the four drawing instructions are encased in a `@layer begin...end` shell. Any change made to the drawing environment inside this shell is discarded after the `end`. This allows us to make temporary changes to the scale and rotation, etc. and discard them easily once the shapes have been drawn. +Notice that the four drawing instructions are encased in a `gsave()`/`grestore()` pair. Any change made to the drawing environment inside this pair is discarded after the `grestore()`. This allows us to make temporary changes to the scale and rotation, etc. and discard them easily once the shapes have been drawn. Rotations and angles are typically specified in radians. The positive x-axis (a line from the origin increasing in x) starts off heading due east from the origin, and the y-axis due south, and positive angles are clockwise (ie from the positive x-axis towards the positive y-axis). So the second egg in the previous example was drawn after the axes were rotated by π/6 radians clockwise. @@ -736,13 +736,13 @@ using Luxor, Colors sethue("gold") eg(:fill) eg(:clip) - @layer begin + gsave() for i in 360:-4:1 sethue(Colors.HSV(i, 1.0, 0.8)) rotate(π/30) ngon(O, i, 5, 0, action = :stroke) end - end + grestore() clipreset() sethue("red") eg(:stroke) @@ -787,13 +787,13 @@ eg(a) = egg(150, a) sethue("gold") eg(:fill) eg(:clip) -@layer begin +gsave() for i in 360:-4:1 sethue(Colors.HSV(i, 1.0, 0.8)) rotate(π/30) ngon(O, i, 5, 0, action = :stroke) end -end +grestore() clipreset() sethue("red") eg(:stroke) diff --git a/src/basics.jl b/src/basics.jl index 3fc77221..d41ab317 100644 --- a/src/basics.jl +++ b/src/basics.jl @@ -1,14 +1,14 @@ """ origin() -Reset the current matrix, and then set the 0/0 origin to the center of the drawing +Reset the current position, scale, and orientation, and then set the 0/0 origin to the center of the drawing (otherwise it will stay at the top left corner, the default). You can refer to the 0/0 point as `O`. (O = `Point(0, 0)`), """ function origin() setmatrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]) - Cairo.translate(_get_current_cr(), _current_width()/2.0, _current_height()/2.0) + Cairo.translate(_get_current_cr(), _current_width() / 2.0, _current_height() / 2.0) end function origin(x, y) @@ -19,7 +19,7 @@ end """ origin(pt:Point) -Reset the current matrix, then move the `0/0` position to `pt`. +Reset the current position, scale, and orientation, then move the `0/0` position to `pt`. """ function origin(pt) setmatrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]) @@ -59,11 +59,10 @@ julia> rescale(15, (0, 100), (1000, 0)) 850.0 ``` - """ -rescale(x, from_min, from_max, to_min=0.0, to_max=1.0) = +rescale(x, from_min, from_max, to_min = 0.0, to_max = 1.0) = ((x - from_min) / (from_max - from_min)) * (to_max - to_min) + to_min -rescale(x, from::NTuple{2, Number}, to::NTuple{2, Number}) = +rescale(x, from::NTuple{2,Number}, to::NTuple{2,Number}) = ((x - from[1]) / (from[2] - from[1])) * (to[2] - to[1]) + to[1] """ @@ -76,8 +75,9 @@ Examples: background("antiquewhite") background(1, 0.0, 1.0) background(1, 0.0, 1.0, .5) + background(Luxor.Colors.RGB(0, 1, 0)) -If Colors.jl is installed: +If Colors.jl has been imported: background(RGB(0, 1, 0)) background(RGBA(0, 1, 0)) @@ -106,7 +106,7 @@ function background(col::Colors.Colorant) return (r, g, b, a) end -function background(col::T) where T <: AbstractString +function background(col::T) where {T<:AbstractString} return background(parse(Colorant, col)) end @@ -135,26 +135,27 @@ setantialias(n) = Cairo.set_antialias(_get_current_cr(), n) """ newpath() -Create a new path, after clearing the current path. After -this there's no path and no current point. +Discard the current path's contents. After this, the current path is empty, and there's +no current point. """ newpath() = Cairo.new_path(_get_current_cr()) """ newsubpath() -Start a new subpath, keeping the current path. After this there's no current point. +Start a new subpath in the current path. After this, there's no current point. """ newsubpath() = Cairo.new_sub_path(_get_current_cr()) """ closepath() -Close the current path. This is Cairo's `close_path()` function. +Draw a line from the current point to the first point of the current subpath. +This is Cairo's `close_path()` function. """ closepath() = Cairo.close_path(_get_current_cr()) -abstract type LDispatcher end +abstract type LDispatcher end # Packages which want to override strokepath/fillpath/strokepreserve/fillpreserve # can subtype an empty struct from this abstract type and dispatch on them . # Make sure Luxor.DISPATCHER[1] is set to an instance of the struct you want to @@ -169,7 +170,7 @@ const DISPATCHER = Array{LDispatcher}([DefaultLuxor()]) strokepath() Stroke the current path with the current line width, line join, line cap, dash, -and stroke scaling settings. The current path is then cleared. +and stroke scaling settings. The current path is then emptied. """ strokepath() = strokepath(DISPATCHER[1]) strokepath(::DefaultLuxor) = _get_current_strokescale() ? Cairo.stroke_transformed(_get_current_cr()) : Cairo.stroke(_get_current_cr()) @@ -178,7 +179,7 @@ strokepath(::LDispatcher) = strokepath(DefaultLuxor()) """ fillpath() -Fill the current path according to the current settings. The current path is then cleared. +Fill the current path according to the current settings. The current path is then emptied. """ fillpath() = fillpath(DISPATCHER[1]) fillpath(::DefaultLuxor) = Cairo.fill(_get_current_cr()) @@ -200,7 +201,7 @@ Stroke the current path with current line width, line join, line cap, dash, and stroke scaling settings, but then keep the path current. """ strokepreserve() = strokepreserve(DISPATCHER[1]) -strokepreserve(::DefaultLuxor) = _get_current_strokescale() ? Cairo.stroke_preserve_transformed(_get_current_cr()) : Cairo.stroke_preserve(_get_current_cr()) +strokepreserve(::DefaultLuxor) = _get_current_strokescale() ? Cairo.stroke_preserve_transformed(_get_current_cr()) : Cairo.stroke_preserve(_get_current_cr()) strokepreserve(::LDispatcher) = strokepreserve(DefaultLuxor()) """ @@ -209,13 +210,14 @@ strokepreserve(::LDispatcher) = strokepreserve(DefaultLuxor()) Fill the current path with current settings, but then keep the path current. """ fillpreserve() = fillpreserve(DISPATCHER[1]) -fillpreserve(::DefaultLuxor) = Cairo.fill_preserve(_get_current_cr()) +fillpreserve(::DefaultLuxor) = Cairo.fill_preserve(_get_current_cr()) fillpreserve(::LDispatcher) = fillpreserve(DefaultLuxor()) """ fillstroke() Fill and stroke the current path. +After this, the current path is empty, and there is no current point. """ function fillstroke() fillpreserve() @@ -254,8 +256,9 @@ end Establish a new clipping region by intersecting the current clipping region with the current path and then clearing the current path. -An existing clipping region is enforced through and after a `gsave()`-`grestore()` block, but a clipping region set inside -a `gsave()`-`grestore()` block is lost after `grestore()`. [?] +An existing clipping region is enforced through and after a +`gsave()`-`grestore()` block, but a clipping region set inside a +`gsave()`-`grestore()` block is lost after `grestore()`. [?] """ clip() = clip(DISPATCHER[1]) clip(::DefaultLuxor) = Cairo.clip(_get_current_cr()) @@ -282,6 +285,8 @@ clipreset() = Cairo.reset_clip(_get_current_cr()) setline(n) Set the line width, in points. + +Use `getline()` to get the current value. """ setline(n) = Cairo.set_line_width(_get_current_cr(), n) @@ -289,6 +294,8 @@ setline(n) = Cairo.set_line_width(_get_current_cr(), n) getline() Get the current line width, in points. + +Use `setline()` to set the value. """ function getline() ccall((:cairo_get_line_width, Cairo.libcairo), @@ -301,7 +308,7 @@ end Set the line ends. `s` can be "butt" or `:butt` (the default), "square" or `:square`, or "round" or `:round`. """ -function setlinecap(str::String="butt") +function setlinecap(str::String = "butt") if str == "round" Cairo.set_line_cap(_get_current_cr(), Cairo.CAIRO_LINE_CAP_ROUND) elseif str == "square" @@ -326,9 +333,11 @@ end setlinejoin("round") setlinejoin("bevel") -Set the line join style, or how to render the junction of two lines when stroking. +Set the way line segments are joined when the path is stroked. + +The default joining style is "mitered". """ -function setlinejoin(str="miter") +function setlinejoin(str = "miter") if str == "round" Cairo.set_line_join(_get_current_cr(), Cairo.CAIRO_LINE_JOIN_ROUND) elseif str == "bevel" @@ -341,7 +350,7 @@ end """ setdash("dot") -Set the dash pattern to one of: "solid", "dotted", "dot", "dotdashed", +Set the dash pattern of lines to one of: "solid", "dotted", "dot", "dotdashed", "longdashed", "shortdashed", "dash", "dashed", "dotdotdashed", "dotdotdotdashed". @@ -354,18 +363,20 @@ end """ setdash(dashes::Vector, offset=0.0) -Set the dash pattern to the values in `dashes`. The first number is the length of the ink, the second the gap, and so on. +Set the dash pattern for lines to the values in `dashes`. The first number is the length +of the inked portion, the second the space, and so on. -The `offset` specifies an offset into the pattern at which the stroke begins. So an offset of 10 means that the stroke starts at `dashes[1] + 10` into the pattern. +The `offset` specifies an offset into the pattern at which the stroke begins. So +an offset of 10 means that the stroke starts at `dashes[1] + 10` into the +pattern. Or use `setdash("dot")` etc. """ -function setdash(dashes::Vector, offset=0.0) +function setdash(dashes::Vector, offset = 0.0) # no negative dashes Cairo.set_dash(_get_current_cr(), abs.(Float64.(dashes)), offset) end - """ setstrokescale() @@ -373,7 +384,6 @@ Return the current stroke scaling setting. """ setstrokescale() = _get_current_strokescale() - """ setstrokescale(state::Bool) @@ -381,53 +391,68 @@ Enable/disable stroke scaling for the current drawing. """ setstrokescale(state::Bool) = _set_current_strokescale(state) - """ move(pt) -Move to a point. +Begin a new subpath in the current path, and set the current path's current point +to `pt`, without drawing anything. + +Other path-building functions are `line()`, `curve()`, `arc()`, `rline()`, and +`rmove()`. + +`hascurrentpoint()` returns true if there is a current point. """ -move(x, y) = Cairo.move_to(_get_current_cr(), x, y) -move(pt) = move(pt.x, pt.y) +move(x, y) = Cairo.move_to(_get_current_cr(), x, y) +move(pt) = move(pt.x, pt.y) """ rmove(pt) -Move relative to current position by the `pt`'s x and y: +Begin a new subpath in the current path, and add `pt` to the current path's current point. + +There must be a current point before you call this function. + +See also `currentpoint()` and `hascurrentpoint()`. """ -rmove(x, y) = Cairo.rel_move_to(_get_current_cr(), x, y) -rmove(pt) = rmove(pt.x, pt.y) +rmove(x, y) = Cairo.rel_move_to(_get_current_cr(), x, y) +rmove(pt) = rmove(pt.x, pt.y) """ - line(pt) + line(pt::Point) + +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`. -Draw a line from the current position to the `pt`. +Other path-building functions are `line()`, `curve()`, `arc()`, `rline()`, and +`rmove()`. + +See also `currentpoint()` and `hascurrentpoint()`. """ -line(x, y) = Cairo.line_to(_get_current_cr(), x, y) -line(pt) = line(pt.x, pt.y) +line(x, y) = Cairo.line_to(_get_current_cr(), x, y) +line(pt) = line(pt.x, pt.y) """ line(pt1::Point, pt2::Point; action=:path) line(pt1::Point, pt2::Point, action=:path) -Make a line between two points, `pt1` and `pt2` and do an action. +Add a straight line between two points `pt1` and `pt2`, and do the action. """ function line(pt1::Point, pt2::Point; - action=:path) + action = :path) move(pt1) line(pt2) do_action(action) end -line(pt1::Point, pt2::Point, action::Symbol) = line(pt1, pt2, action=action) +line(pt1::Point, pt2::Point, action::Symbol) = line(pt1, pt2, action = action) """ rline(pt) -Draw a line relative to the current position to the `pt`. +Add a line relative to the current position to the `pt`. """ -rline(x, y) = Cairo.rel_line_to(_get_current_cr(), x, y) -rline(pt) = rline(pt.x, pt.y) +rline(x, y) = Cairo.rel_line_to(_get_current_cr(), x, y) +rline(pt) = rline(pt.x, pt.y) """ rule(pos, theta; @@ -452,13 +477,13 @@ rule(O, pi/2, boundingbox=BoundingBox()/2) draws a line that spans a bounding box half the width and height of the drawing, and returns a Set of end points. If you just want the vertices and don't want to draw anything, use `vertices=true`. """ -function rule(pos, theta=0.0; - boundingbox=BoundingBox(), - vertices=false) +function rule(pos, theta = 0.0; + boundingbox = BoundingBox(), + vertices = false) #TODO interaction with clipping regions needs work #TODO which boundingbox is providing the default??? - bbox = box(boundingbox, vertices=true) + bbox = box(boundingbox, vertices = true) topside = bbox[2:3] rightside = bbox[3:4] bottomside = vcat(bbox[4], bbox[1]) @@ -470,10 +495,10 @@ function rule(pos, theta=0.0; rsina = r * sin(theta) ruledline = (pos - (rcosa, rsina), pos + (rcosa, rsina)) - interpoints = Array{Point, 1}() + interpoints = Array{Point,1}() # check for intersection with top of bounding box - flag, ip = intersectionlines(ruledline[1], ruledline[2], topside[1], topside[2], crossingonly=true) + flag, ip = intersectionlines(ruledline[1], ruledline[2], topside[1], topside[2], crossingonly = true) if flag if !(ip.x > topside[2].x || ip.x < topside[1].x) push!(interpoints, ip) @@ -481,7 +506,7 @@ function rule(pos, theta=0.0; end # check for right intersection - flag, ip = intersectionlines(ruledline[1], ruledline[2], rightside[1], rightside[2], crossingonly=true) + flag, ip = intersectionlines(ruledline[1], ruledline[2], rightside[1], rightside[2], crossingonly = true) if flag if !(ip.y > rightside[2].y || ip.y < rightside[1].y) push!(interpoints, ip) @@ -489,7 +514,7 @@ function rule(pos, theta=0.0; end # check for bottom intersection - flag, ip = intersectionlines(ruledline[1], ruledline[2], bottomside[1], bottomside[2], crossingonly=true) + flag, ip = intersectionlines(ruledline[1], ruledline[2], bottomside[1], bottomside[2], crossingonly = true) if flag if !(ip.x < bottomside[2].x || ip.x > bottomside[1].x) push!(interpoints, ip) @@ -497,7 +522,7 @@ function rule(pos, theta=0.0; end # check for left intersection - flag, ip = intersectionlines(ruledline[1], ruledline[2], leftside[1], leftside[2], crossingonly=true) + flag, ip = intersectionlines(ruledline[1], ruledline[2], leftside[1], leftside[2], crossingonly = true) if flag if !(ip.y > leftside[1].y || ip.y < leftside[2].y) push!(interpoints, ip) @@ -519,16 +544,16 @@ end # - predefine all needed Dict entries in a thread safe way # - each thread has it's own stack, separated by threadid # this is not enough for Threads.@spawn (TODO, but no solution yet) -let _SAVED_COLORS_STACK = Ref{Dict{Int,Array{Tuple{Float64, Float64, Float64, Float64},1}}}(Dict(0 => Array{Tuple{Float64, Float64, Float64, Float64},1}())) +let _SAVED_COLORS_STACK = Ref{Dict{Int,Array{Tuple{Float64,Float64,Float64,Float64},1}}}(Dict(0 => Array{Tuple{Float64,Float64,Float64,Float64},1}())) global _saved_colors function _saved_colors() id = Threads.threadid() - if ! haskey(_SAVED_COLORS_STACK[],id) + if !haskey(_SAVED_COLORS_STACK[], id) # predefine all needed Dict entries lc = ReentrantLock() lock(lc) for preID in 1:Threads.nthreads() - _SAVED_COLORS_STACK[][preID] = Array{Tuple{Float64, Float64, Float64, Float64},1}() + _SAVED_COLORS_STACK[][preID] = Array{Tuple{Float64,Float64,Float64,Float64},1}() end unlock(lc) end @@ -546,15 +571,15 @@ end """ gsave() -Save the current color settings on the stack. +Save the current graphics environment, including current color settings. """ function gsave() Cairo.save(_get_current_cr()) r, g, b, a = (_get_current_redvalue(), - _get_current_greenvalue(), - _get_current_bluevalue(), - _get_current_alpha() - ) + _get_current_greenvalue(), + _get_current_bluevalue(), + _get_current_alpha(), + ) push!(_saved_colors(), (r, g, b, a)) return (r, g, b, a) end @@ -562,18 +587,19 @@ end """ grestore() -Replace the current graphics state with the one on top of the stack. +Replace the current graphics state with the one previously saved by the most recent +`gsave()`. """ function grestore() Cairo.restore(_get_current_cr()) try - (r, g, b, a) = pop!(_saved_colors()) + (r, g, b, a) = pop!(_saved_colors()) _set_current_redvalue(r) _set_current_greenvalue(g) _set_current_bluevalue(b) _set_current_alpha(a) catch err - println("$err Not enough colors on the stack to restore.") + println("$err Not enough colors on the stack to restore.") end end @@ -591,28 +617,32 @@ end """ scale(x, y) -Scale workspace by `x` and `y`. +Scale the current workspace by different values in `x` and `y`. +Values are relative to the current scale. Example: ``` scale(0.2, 0.3) ``` - """ scale(sx::Real, sy::Real) = Cairo.scale(_get_current_cr(), sx, sy) """ scale(f) -Scale workspace by `f` in both `x` and `y`. +Scale the current workspace by `f` in both `x` and `y` directions. + +Values are relative to the current scale. """ scale(f::Real) = Cairo.scale(_get_current_cr(), f, f) """ rotate(a::Float64) -Rotate workspace by `a` radians clockwise (from positive x-axis to positive y-axis). +Rotate the current workspace by `a` radians clockwise (from positive x-axis to positive y-axis). + +Values are relative to the current orientation. """ rotate(a) = Cairo.rotate(_get_current_cr(), a) @@ -620,16 +650,18 @@ rotate(a) = Cairo.rotate(_get_current_cr(), a) translate(point) translate(x::Real, y::Real) -Translate the workspace to `x` and `y` or to `pt`. +Translate the current workspace to `x` and `y` or to `pt`. + +Values are relative to the current location. """ -translate(tx::Real, ty::Real) = Cairo.translate(_get_current_cr(), tx, ty) -translate(pt::Point) = translate(pt.x, pt.y) +translate(tx::Real, ty::Real) = Cairo.translate(_get_current_cr(), tx, ty) +translate(pt::Point) = translate(pt.x, pt.y) """ getpath() Get the current path and return a CairoPath object, which is an array of `element_type` and -`points` objects. With the results you can step through and examine each entry: +`points` objects. With the results you can step through and examine each entry like this: ``` o = getpath() @@ -663,21 +695,21 @@ for e in o end ``` """ -getpath() = Cairo.convert_cairo_path_data(Cairo.copy_path(_get_current_cr())) +getpath() = Cairo.convert_cairo_path_data(Cairo.copy_path(_get_current_cr())) """ getpathflat() -Get the current path, like `getpath()` but flattened so that there are no Bèzier curves. +Get the current path, like `getpath()` but flattened so that there are no Bézier curves. Returns a CairoPath which is an array of `element_type` and `points` objects. """ -getpathflat() = Cairo.convert_cairo_path_data(Cairo.copy_path_flat(_get_current_cr())) +getpathflat() = Cairo.convert_cairo_path_data(Cairo.copy_path_flat(_get_current_cr())) """ rulers() -Draw and label two rulers starting at `O`, the current 0/0, and continuing out +Draw and label two CAD-style rulers starting at `O`, the current 0/0, and continuing out along the current positive x and y axes. """ function rulers() @@ -693,22 +725,22 @@ function rulers() box(O - (w, 0), O + (0, n), :fillstroke) sethue(0.722, 0.525, 0.043) # darkgoldenrod setopacity(1) - [line(Point(x, 0), Point(x, -w/4), :stroke) for x in 0:10:n] - [line(Point(-w/4, y), Point(0, y), :stroke) for y in 0:10:n] - [line(Point(x, 0), Point(x, -w/6), :stroke) for x in 0:5:n] - [line(Point(-w/6, y), Point(0, y), :stroke) for y in 0:5:n] + [line(Point(x, 0), Point(x, -w / 4), :stroke) for x in 0:10:n] + [line(Point(-w / 4, y), Point(0, y), :stroke) for y in 0:10:n] + [line(Point(x, 0), Point(x, -w / 6), :stroke) for x in 0:5:n] + [line(Point(-w / 6, y), Point(0, y), :stroke) for y in 0:5:n] fontsize(2) - [text(string(x), Point(x, -w/3), halign=:right) for x in 10:10:n] + [text(string(x), Point(x, -w / 3), halign = :right) for x in 10:10:n] @layer begin - rotate(pi/2) - [text(string(x), Point(x, w/3), halign=:right) for x in 10:10:n] + rotate(pi / 2) + [text(string(x), Point(x, w / 3), halign = :right) for x in 10:10:n] end fontsize(15) - text("X", O + (n-w/2, w), halign=:right, valign=:middle) - text("Y", O + (-3w/2, n-w), halign=:right, valign=:middle, angle=pi/2) + text("X", O + (n - w / 2, w), halign = :right, valign = :middle) + text("Y", O + (-3w / 2, n - w), halign = :right, valign = :middle, angle = pi / 2) sethue(1.0, 1.0, 1.0) - text("X", O + (w, -w/2), halign=:right, valign=:middle) - text("Y", O + (-w/3, w/3), halign=:right, valign=:middle, angle=pi/2) + text("X", O + (w, -w / 2), halign = :right, valign = :middle) + text("Y", O + (-w / 3, w / 3), halign = :right, valign = :middle, angle = pi / 2) #center circle(O, 2, :strokepreserve) setopacity(0.5) @@ -720,7 +752,7 @@ end """ hcat(D::Drawing...; valign=:top, hpad=0, clip=true) -Creates a new SVG drawing by horizontal concatenation of SVG drawings. If drawings +Create a new SVG drawing by horizontal concatenation of SVG drawings. If drawings have different height, the `valign` option can be used in order to define how to align. The `hpad` argument can be used to add padding between concatenated images. @@ -734,25 +766,25 @@ argument ensures that these elements are not drawn in the concatenated drawing. Example: ```julia -d1 = Drawing(200,100,:svg) +d1 = Drawing(200, 100, :svg) origin() -circle(O,60,:fill) +circle(O, 60, :fill) finish() -d2 = Drawing(200,200,:svg) -rect(O,200,200,:fill) +d2 = Drawing(200, 200, :svg) +rect(O, 200, 200, :fill) finish() -hcat(d1,d2; hpad=10, valign=:top, clip = true) +hcat(d1, d2; hpad = 10, valign = :top, clip = true) ``` """ -function Base.hcat(D::Drawing...; valign=:top, hpad=0, clip=true) - dheight, dwidth = 0,-hpad +function Base.hcat(D::Drawing...; valign = :top, hpad = 0, clip = true) + dheight, dwidth = 0, -hpad for d in D dheight = max(dheight, d.height) dwidth += d.width + hpad @assert d.surfacetype === :svg "Drawings must be SVG." end - dcat = Drawing(dwidth,dheight,:svg) + dcat = Drawing(dwidth, dheight, :svg) @layer begin for d in D if valign === :top @@ -760,18 +792,18 @@ function Base.hcat(D::Drawing...; valign=:top, hpad=0, clip=true) clip ? rect(pt, d.width, d.height, :clip) : nothing placeimage(d, pt) elseif valign === :bottom - pt = Point(0,dheight-d.height) + pt = Point(0, dheight - d.height) clip ? rect(pt, d.width, d.height, :clip) : nothing - placeimage(d,pt) + placeimage(d, pt) elseif valign === :middle - pt = Point(0,dheight-d.height)/2 + pt = Point(0, dheight - d.height) / 2 clip ? rect(pt, d.width, d.height, :clip) : nothing - placeimage(d,pt) + placeimage(d, pt) else throw("`valign` option not valid. Use either `:top`, `:bottom` or `:middle`.") end clipreset() - translate(Point(d.width+hpad,0)) + translate(Point(d.width + hpad, 0)) end end finish() @@ -795,25 +827,25 @@ argument ensures that these elements are not drawn in the concatenated drawing. Example: ```julia -d1 = Drawing(200,100,:svg) +d1 = Drawing(200, 100, :svg) origin() -circle(O,60,:fill) +circle(O, 60, :fill) finish() -d2 = Drawing(200,200,:svg) -rect(O,200,200,:fill) +d2 = Drawing(200, 200, :svg) +rect(O, 200, 200, :fill) finish() -vcat(d1,d2; vpad=10, halign=:left, clip = true) +vcat(d1, d2; vpad = 10, halign = :left, clip = true) ``` """ -function Base.vcat(D::Drawing...; halign=:left, vpad=0, clip=true) +function Base.vcat(D::Drawing...; halign = :left, vpad = 0, clip = true) dheight, dwidth = -vpad, 0 for d in D dwidth = max(dwidth, d.width) dheight += d.height + vpad @assert d.surfacetype === :svg "Drawings must be SVG." end - dcat = Drawing(dwidth,dheight,:svg) + dcat = Drawing(dwidth, dheight, :svg) @layer begin for d in D if halign === :left @@ -821,18 +853,18 @@ function Base.vcat(D::Drawing...; halign=:left, vpad=0, clip=true) clip ? rect(pt, d.width, d.height, :clip) : nothing placeimage(d, pt) elseif halign === :right - pt = Point(dwidth-d.width, 0) + pt = Point(dwidth - d.width, 0) clip ? rect(pt, d.width, d.height, :clip) : nothing - placeimage(d,pt) + placeimage(d, pt) elseif halign === :center - pt = Point(dwidth-d.width, 0)/2 + pt = Point(dwidth - d.width, 0) / 2 clip ? rect(pt, d.width, d.height, :clip) : nothing - placeimage(d,pt) + placeimage(d, pt) else throw("`halign` option not valid. Use either `:left`, `:right` or `:center`.") end clipreset() - translate(Point(0, d.height+vpad)) + translate(Point(0, d.height + vpad)) end end finish() diff --git a/src/drawings.jl b/src/drawings.jl index afca9035..064fe680 100644 --- a/src/drawings.jl +++ b/src/drawings.jl @@ -10,18 +10,18 @@ mutable struct Drawing bluevalue::Float64 alpha::Float64 buffer::IOBuffer # Keeping both buffer and data because I think the buffer might get GC'ed otherwise - bufferdata::Array{UInt8, 1} # Direct access to data + bufferdata::Array{UInt8,1} # Direct access to data strokescale::Bool - function Drawing(img::Matrix{T}, f::AbstractString=""; strokescale=false) where {T<:Union{RGB24,ARGB32}} - w,h = size(img) + function Drawing(img::Matrix{T}, f::AbstractString = ""; strokescale = false) where {T<:Union{RGB24,ARGB32}} + w, h = size(img) bufdata = UInt8[] - iobuf = IOBuffer(bufdata, read=true, write=true) + iobuf = IOBuffer(bufdata, read = true, write = true) the_surfacetype = :image the_surface = Cairo.CairoImageSurface(img) - the_cr = Cairo.CairoContext(the_surface) + the_cr = Cairo.CairoContext(the_surface) currentdrawing = new(w, h, f, the_surface, the_cr, the_surfacetype, 0.0, 0.0, 0.0, 1.0, iobuf, bufdata, strokescale) - if ! isassigned(_current_drawing(), _current_drawing_index()) + if !isassigned(_current_drawing(), _current_drawing_index()) push!(_current_drawing(), currentdrawing) _current_drawing_index(lastindex(_current_drawing())) else @@ -30,25 +30,25 @@ mutable struct Drawing return currentdrawing end - function Drawing(w, h, stype::Symbol, f::AbstractString=""; strokescale=false) + function Drawing(w, h, stype::Symbol, f::AbstractString = ""; strokescale = false) bufdata = UInt8[] - iobuf = IOBuffer(bufdata, read=true, write=true) + iobuf = IOBuffer(bufdata, read = true, write = true) the_surfacetype = stype if stype == :pdf - the_surface = Cairo.CairoPDFSurface(iobuf, w, h) + the_surface = Cairo.CairoPDFSurface(iobuf, w, h) elseif stype == :png # default to PNG - the_surface = Cairo.CairoARGBSurface(w, h) + the_surface = Cairo.CairoARGBSurface(w, h) elseif stype == :eps - the_surface = Cairo.CairoEPSSurface(iobuf, w, h) + the_surface = Cairo.CairoEPSSurface(iobuf, w, h) elseif stype == :svg - the_surface = Cairo.CairoSVGSurface(iobuf, w, h) + the_surface = Cairo.CairoSVGSurface(iobuf, w, h) elseif stype == :rec if isnan(w) || isnan(h) - the_surface = Cairo.CairoRecordingSurface() + the_surface = Cairo.CairoRecordingSurface() else extents = Cairo.CairoRectangle(0.0, 0.0, w, h) bckg = Cairo.CONTENT_COLOR_ALPHA - the_surface = Cairo.CairoRecordingSurface(bckg, extents) + the_surface = Cairo.CairoRecordingSurface(bckg, extents) # Both the CairoSurface and the Drawing stores w and h in mutable structures. # Cairo.RecordingSurface does not set the w and h properties, # probably because that could be misinterpreted (width and height @@ -60,14 +60,14 @@ mutable struct Drawing the_surface.height = h end elseif stype == :image - the_surface = Cairo.CairoImageSurface(w, h, Cairo.FORMAT_ARGB32) + the_surface = Cairo.CairoImageSurface(w, h, Cairo.FORMAT_ARGB32) else - error("Unknown Luxor surface type" \"$stype\"") + error("Unknown Luxor surface type" \ "$stype\"") end - the_cr = Cairo.CairoContext(the_surface) + the_cr = Cairo.CairoContext(the_surface) # @info("drawing '$f' ($w w x $h h) created in $(pwd())") - currentdrawing = new(w, h, f, the_surface, the_cr, the_surfacetype, 0.0, 0.0, 0.0, 1.0, iobuf, bufdata, strokescale) - if ! isassigned(_current_drawing(), _current_drawing_index() ) + currentdrawing = new(w, h, f, the_surface, the_cr, the_surfacetype, 0.0, 0.0, 0.0, 1.0, iobuf, bufdata, strokescale) + if !isassigned(_current_drawing(), _current_drawing_index()) push!(_current_drawing(), currentdrawing) _current_drawing_index(lastindex(_current_drawing())) else @@ -82,17 +82,18 @@ end # - predefine all needed Dict entries in a thread safe way # - each thread has it's own stack, separated by threadid # this is not enough for Threads.@spawn (TODO, but no solution yet) -let _CURRENTDRAWINGS = Ref{Dict{Int,Union{Array{Drawing, 1},Nothing}}}(Dict(0 => nothing)), +let _CURRENTDRAWINGS = Ref{Dict{Int,Union{Array{Drawing,1},Nothing}}}(Dict(0 => nothing)), _CURRENTDRAWINGINDICES = Ref{Dict{Int,Int}}(Dict(0 => 0)) + global _current_drawing function _current_drawing() id = Threads.threadid() - if ! haskey(_CURRENTDRAWINGS[],id) + if !haskey(_CURRENTDRAWINGS[], id) # predefine all needed Dict entries lc = ReentrantLock() lock(lc) for preID in 1:Threads.nthreads() - _CURRENTDRAWINGS[][preID] = Array{Drawing, 1}() + _CURRENTDRAWINGS[][preID] = Array{Drawing,1}() end unlock(lc) end @@ -106,7 +107,7 @@ let _CURRENTDRAWINGS = Ref{Dict{Int,Union{Array{Drawing, 1},Nothing}}}(Dict(0 => global _current_drawing_index function _current_drawing_index() id = Threads.threadid() - if ! haskey(_CURRENTDRAWINGINDICES[],id) + if !haskey(_CURRENTDRAWINGINDICES[], id) # predefine all needed Dict entries lc = ReentrantLock() lock(lc) @@ -124,7 +125,7 @@ let _CURRENTDRAWINGS = Ref{Dict{Int,Union{Array{Drawing, 1},Nothing}}}(Dict(0 => end function _current_drawing_index(i::Int) id = Threads.threadid() - if ! haskey(_CURRENTDRAWINGINDICES[],id) + if !haskey(_CURRENTDRAWINGINDICES[], id) # predefine all needed Dict entries lc = ReentrantLock() lock(lc) @@ -159,14 +160,14 @@ function _get_current_drawing_save() # not checked but perhaps needed: # buffer::IOBuffer # bufferdata::Array{UInt8, 1} - if _current_drawing_index() <= 0 || - ( - _current_drawing_index() > 0 && - getfield(getfield(_current_drawing()[_current_drawing_index()], :cr), :ptr) == C_NULL && - getfield(getfield(_current_drawing()[_current_drawing_index()], :surface), :ptr) == C_NULL && - 1 == 1 - ) - error("Drawing not initialized. Did you call Drawing(...)?") + if _current_drawing_index() <= 0 || + ( + _current_drawing_index() > 0 && + getfield(getfield(_current_drawing()[_current_drawing_index()], :cr), :ptr) == C_NULL && + getfield(getfield(_current_drawing()[_current_drawing_index()], :surface), :ptr) == C_NULL && + 1 == 1 + ) + error("There isn't a current drawing. Create one with `Drawing()` or one of the `@draw`/`@png` etc macros.") end return _current_drawing()[_current_drawing_index()] end @@ -175,17 +176,17 @@ function _get_current_cr() getfield(_get_current_drawing_save(), :cr) end -_get_current_redvalue() = getfield(_get_current_drawing_save(), :redvalue) -_get_current_greenvalue() = getfield(_get_current_drawing_save(), :greenvalue) -_get_current_bluevalue() = getfield(_get_current_drawing_save(), :bluevalue) -_get_current_alpha() = getfield(_get_current_drawing_save(), :alpha) +_get_current_redvalue() = getfield(_get_current_drawing_save(), :redvalue) +_get_current_greenvalue() = getfield(_get_current_drawing_save(), :greenvalue) +_get_current_bluevalue() = getfield(_get_current_drawing_save(), :bluevalue) +_get_current_alpha() = getfield(_get_current_drawing_save(), :alpha) function _get_current_color() d = _get_current_drawing_save() return ( getfield(d, :redvalue), getfield(d, :greenvalue), getfield(d, :bluevalue), - getfield(d, :alpha) + getfield(d, :alpha), ) end function _get_current_cr_color() @@ -195,7 +196,7 @@ function _get_current_cr_color() getfield(d, :redvalue), getfield(d, :greenvalue), getfield(d, :bluevalue), - getfield(d, :alpha) + getfield(d, :alpha), ) end @@ -203,41 +204,41 @@ _set_current_redvalue(r) = setfield!(_get_current_drawing_save(), :redvalue, c _set_current_greenvalue(g) = setfield!(_get_current_drawing_save(), :greenvalue, convert(Float64, g)) _set_current_bluevalue(b) = setfield!(_get_current_drawing_save(), :bluevalue, convert(Float64, b)) _set_current_alpha(a) = setfield!(_get_current_drawing_save(), :alpha, convert(Float64, a)) -function _set_current_color(r,g,b,a) - _set_current_color(r,g,b) +function _set_current_color(r, g, b, a) + _set_current_color(r, g, b) d = _get_current_drawing_save() setfield!(d, :redvalue, convert(Float64, r)) setfield!(d, :greenvalue, convert(Float64, g)) setfield!(d, :bluevalue, convert(Float64, b)) setfield!(d, :alpha, convert(Float64, a)) end -function _set_current_color(r,g,b) +function _set_current_color(r, g, b) d = _get_current_drawing_save() setfield!(d, :redvalue, convert(Float64, r)) setfield!(d, :greenvalue, convert(Float64, g)) setfield!(d, :bluevalue, convert(Float64, b)) end -_current_filename() = getfield(_get_current_drawing_save(), :filename) -_current_width() = getfield(_get_current_drawing_save(), :width) -_current_height() = getfield(_get_current_drawing_save(), :height) -_current_surface() = getfield(_get_current_drawing_save(), :surface) -_current_surface_ptr() = getfield(getfield(_get_current_drawing_save(), :surface), :ptr) -_current_surface_type() = getfield(_get_current_drawing_save(), :surfacetype) +_current_filename() = getfield(_get_current_drawing_save(), :filename) +_current_width() = getfield(_get_current_drawing_save(), :width) +_current_height() = getfield(_get_current_drawing_save(), :height) +_current_surface() = getfield(_get_current_drawing_save(), :surface) +_current_surface_ptr() = getfield(getfield(_get_current_drawing_save(), :surface), :ptr) +_current_surface_type() = getfield(_get_current_drawing_save(), :surfacetype) -_current_buffer() = getfield(_get_current_drawing_save(), :buffer) -_current_bufferdata() = getfield(_get_current_drawing_save(), :bufferdata) +_current_buffer() = getfield(_get_current_drawing_save(), :buffer) +_current_bufferdata() = getfield(_get_current_drawing_save(), :bufferdata) _get_current_strokescale() = getfield(_get_current_drawing_save(), :strokescale) -_set_current_strokescale(s)= setfield!(_get_current_drawing_save(), :strokescale, s) +_set_current_strokescale(s) = setfield!(_get_current_drawing_save(), :strokescale, s) """ Luxor._drawing_indices() Get a UnitRange over all available indices of drawings. -With Luxor you can work on multiple drawings simultaneously. Each drawing is stored -in an internal array. The first drawing is stored at index 1 when you start a +With Luxor you can work on multiple drawings simultaneously. Each drawing is stored +in an internal array. The first drawing is stored at index 1 when you start a drawing with `Drawing(...)`. To start a second drawing you call `Luxor._set_next_drawing_index()`, which returns the new index. Calling another `Drawing(...)` stores the second drawing at this new index. `Luxor._set_next_drawing_index()` will return and set the next available index @@ -251,15 +252,15 @@ Multiple drawings is especially helpful for interactive graphics with live windo like MiniFB. Example: - + using Luxor Drawing(500, 500, "1.svg") origin() setcolor("red") circle(Point(0, 0), 100, action = :fill) - + Luxor._drawing_indices() # returns 1:1 - + Luxor._get_next_drawing_index() # returns 2 but doesn't change current drawing Luxor._set_next_drawing_index() # returns 2 and sets current drawing to it Drawing(500, 500, "2.svg") @@ -292,11 +293,10 @@ Example: Luxor._set_drawing_index(1) # returns 1 preview() # presents the blue circle 3.svg again - + Luxor._set_drawing_index(10) # returns 1 as 10 does not existing Luxor._get_drawing_index() # returns 1 Luxor._get_next_drawing_index() # returns 1, because 1 was finished - """ _drawing_indices() = length(_current_drawing()) == 0 ? (1:1) : (1:length(_current_drawing())) @@ -310,13 +310,13 @@ _get_drawing_index() = _current_drawing_index() == 0 ? 1 : _current_drawing_inde """ Luxor._set_drawing_index(i::Int) -Set the active drawing for successive graphic commands to index i if exist. if index i doesn't exist, +Set the active drawing for successive graphic commands to index i if exist. if index i doesn't exist, the current drawing is unchanged. Returns the current drawing index. Example: - + next_index=5 if Luxor._set_drawing_index(next_index) == next_index # do some additional graphics on the existing drawing @@ -324,10 +324,9 @@ Example: else @warn "Drawing "*string(next_index)*" doesn't exist" endif - """ function _set_drawing_index(i::Int) - if isassigned(_current_drawing(),i) + if isassigned(_current_drawing(), i) _current_drawing_index(i) end return _get_drawing_index() @@ -339,14 +338,14 @@ end Returns the next available drawing index. This can either be a new index or an existing index where a finished (`finish()`) drawing was stored before. """ -function _get_next_drawing_index() +function _get_next_drawing_index() i = 1 if isempty(_current_drawing()) return i end - i = findfirst(x->getfield(getfield(x,:surface),:ptr) == C_NULL,_current_drawing()) + i = findfirst(x -> getfield(getfield(x, :surface), :ptr) == C_NULL, _current_drawing()) if isnothing(i) - return _current_drawing_index()+1 + return _current_drawing_index() + 1 else return i end @@ -372,7 +371,7 @@ end """ Luxor._has_drawing() -returns true if there is a current drawing available or finished, otherwise false. +Returns true if there is a current drawing available or finished, otherwise false. """ function _has_drawing() return _current_drawing_index() != 0 @@ -384,7 +383,7 @@ end Sets and returns the current Luxor drawing overwriting an existing drawing if exists. """ function currentdrawing(d::Drawing) - if ! isassigned(_current_drawing(), _current_drawing_index()) + if !isassigned(_current_drawing(), _current_drawing_index()) push!(_current_drawing(), d) _current_drawing_index(lastindex(_current_drawing())) else @@ -399,13 +398,13 @@ end Return the current Luxor drawing, if there currently is one. """ function currentdrawing() - if ! isassigned(_current_drawing(), _current_drawing_index()) || - isempty(_current_drawing()) || - _current_surface_ptr() == C_NULL || - false - # Already finished or not even started - @info "There is no current drawing" - return false + if !isassigned(_current_drawing(), _current_drawing_index()) || + isempty(_current_drawing()) || + _current_surface_ptr() == C_NULL || + false + # Already finished or not even started + @info "There is no current drawing" + return false else return _current_drawing()[_current_drawing_index()] end @@ -433,12 +432,12 @@ function Base.show(io::IO, ::MIME"text/plain", d::Drawing) # the image MIME and a second time for the text/plain MIME. # We check if this is such a 'second call': if (get(io, :jupyter, false) || Juno.isactive()) && - (d.surfacetype == :svg || d.surfacetype == :png) + (d.surfacetype == :svg || d.surfacetype == :png) return d.filename end if (isdefined(Main, :VSCodeServer) && Main.VSCodeServer.PLOT_PANE_ENABLED[]) && (d.surfacetype == :svg || d.surfacetype == :png) - return d.filename + return d.filename end # perhaps drawing hasn't started yet, eg in the REPL @@ -484,8 +483,8 @@ function tidysvg(fname) open(fname) do f r = string(rand(100000:999999)) d = read(f, String) - d = replace(d, "id=\"glyph" => "id=\"glyph"*r) - d = replace(d, "href=\"#glyph" => "href=\"#glyph"*r) + d = replace(d, "id=\"glyph" => "id=\"glyph" * r) + d = replace(d, "href=\"#glyph" => "href=\"#glyph" * r) open(outfile, "w") do out write(out, d) end @@ -537,22 +536,22 @@ The `paper_sizes` Dictionary holds a few paper sizes, width is first, so default "E" => (3168, 2448)) ``` """ -const paper_sizes = Dict{String, Tuple}( - "A0" => (2384, 3370), - "A1" => (1684, 2384), - "A2" => (1191, 1684), - "A3" => (842, 1191), - "A4" => (595, 842), - "A5" => (420, 595), - "A6" => (298, 420), - "A" => (612, 792), - "Letter" => (612, 792), - "Legal" => (612, 1008), - "Ledger" => (792, 1224), - "B" => (612, 1008), - "C" => (1584, 1224), - "D" => (2448, 1584), - "E" => (3168, 2448)) +const paper_sizes = Dict{String,Tuple}( + "A0" => (2384, 3370), + "A1" => (1684, 2384), + "A2" => (1191, 1684), + "A3" => (842, 1191), + "A4" => (595, 842), + "A5" => (420, 595), + "A6" => (298, 420), + "A" => (612, 792), + "Letter" => (612, 792), + "Legal" => (612, 1008), + "Ledger" => (792, 1224), + "B" => (612, 1008), + "C" => (1584, 1224), + "D" => (2448, 1584), + "E" => (3168, 2448)) """ Create a new drawing, and optionally specify file type (PNG, PDF, SVG, EPS), @@ -647,29 +646,29 @@ Drawing(img, strokescale=true) ``` creates the drawing from an existing image buffer of type `Matrix{Union{RGB24,ARGB32}}`, e.g.: + ``` using Luxor, Colors buffer=zeros(ARGB32, 100, 100) d=Drawing(buffer) ``` """ -function Drawing(w=800.0, h=800.0, f::AbstractString="luxor-drawing.png"; strokescale=false) - (path, ext) = splitext(f) - currentdrawing = Drawing(w, h, Symbol(ext[2:end]), f, strokescale=strokescale) +function Drawing(w = 800.0, h = 800.0, f::AbstractString = "luxor-drawing.png"; strokescale = false) + (path, ext) = splitext(f) + currentdrawing = Drawing(w, h, Symbol(ext[2:end]), f, strokescale = strokescale) return currentdrawing end -function Drawing(paper_size::AbstractString, f="luxor-drawing.png"; strokescale=false) - if occursin("landscape", paper_size) - psize = replace(paper_size, "landscape" => "") - h, w = paper_sizes[psize] - else - w, h = paper_sizes[paper_size] - end - Drawing(w, h, f, strokescale=strokescale) +function Drawing(paper_size::AbstractString, f = "luxor-drawing.png"; strokescale = false) + if occursin("landscape", paper_size) + psize = replace(paper_size, "landscape" => "") + h, w = paper_sizes[psize] + else + w, h = paper_sizes[paper_size] + end + Drawing(w, h, f, strokescale = strokescale) end - @doc raw""" _adjust_background_rects(buffer::UInt8[]; addmarker = true) @@ -695,18 +694,18 @@ If `addmarker` is not set to false, a class property is set as marker: ``` """ function _adjust_background_rects(buffer; addmarker = true) - adjusted_buffer=String(buffer) + adjusted_buffer = String(buffer) # check if there is any transform= part, if not we do not need the next heavy regex - m=match(r"transform=\"matrix\((.+?),(.+?),(.+?),(.+?),(.+?),(.+?)\)\"/>"is,adjusted_buffer) + m = match(r"transform=\"matrix\((.+?),(.+?),(.+?),(.+?),(.+?),(.+?)\)\"/>"is, adjusted_buffer) if !isnothing(m) && length(m.captures) == 6 # get SVG viewbox coordinates to replace the generic 16777215 values # expected example: # - m=match(r"]*?viewBox=\"(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\".*?>"is,adjusted_buffer) - adjust_vb=false + m = match(r"]*?viewBox=\"(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\".*?>"is, adjusted_buffer) + adjust_vb = false if !isnothing(m) && length(m.captures) == 4 - (vbx,vby,vbw,vbh)=string.([ parse(Float64,m[i]) for i in 1:4 ]) - adjust_vb=true + (vbx, vby, vbw, vbh) = string.([parse(Float64, m[i]) for i in 1:4]) + adjust_vb = true end # do adjustment for all elements (after ) which have a transform attribute as matrix # expected example: @@ -719,48 +718,48 @@ function _adjust_background_rects(buffer; addmarker = true) # to: # # adding class as verification that tweak was applied. - m=findall(r""is,adjusted_buffer) + m = findall(r""is, adjusted_buffer) # check if there is exactly 1 element if !isnothing(m) && length(m) == 1 # get SVG part after to search for # could be done in a single RegEx but can produce ERROR: PCRE.exec error: match limit exceeded - m=match(r"(.*)$"is,adjusted_buffer) + m = match(r"(.*)$"is, adjusted_buffer) if !isnothing(m) && length(m.captures) == 1 - adjusted_buffer_part=m[1] - for m in eachmatch(r"]*?(xlink:)*?href=\"#(.*?)\"[^>]*?transform=\"matrix\((.+?),(.+?),(.+?),(.+?),(.+?),(.+?)\)\"/>"is,adjusted_buffer_part) + adjusted_buffer_part = m[1] + for m in eachmatch(r"]*?(xlink:)*?href=\"#(.*?)\"[^>]*?transform=\"matrix\((.+?),(.+?),(.+?),(.+?),(.+?),(.+?)\)\"/>"is, adjusted_buffer_part) if !isnothing(m) && length(m.captures) == 8 # id of group block - id=m[2] + id = m[2] # transform matrix applied to all elements in group block - transform=vcat(reshape([ parse(Float64,m[i]) for i in 3:8 ],2,3),[0.0 0.0 1.0]) + transform = vcat(reshape([parse(Float64, m[i]) for i in 3:8], 2, 3), [0.0 0.0 1.0]) # inverse transform matrix must be applied to background rect to neutralize transform matrix - it=inv(transform) + it = inv(transform) # get the group block with id into mid::String - (head,mid,tail,split_ok)=_split_string_into_head_mid_tail(adjusted_buffer,id) + (head, mid, tail, split_ok) = _split_string_into_head_mid_tail(adjusted_buffer, id) if split_ok # add inverse transform matrix to every background rect # background rects look like: # # add class="luxor_adjusted" too, for future reference that element has been tweaked - invtransformstring="transform=\"matrix("*join(string.(it[1:2,1:3][:]),",")*")\"" - marker="" + invtransformstring = "transform=\"matrix(" * join(string.(it[1:2, 1:3][:]), ",") * ")\"" + marker = "" if addmarker - marker="class=\"luxor_adjusted\" " + marker = "class=\"luxor_adjusted\" " end - mid=replace(mid,r"("is => SubstitutionString("\\1 $(marker)\\2 $(invtransformstring)/>") ) + mid = replace(mid, r"("is => SubstitutionString("\\1 $(marker)\\2 $(invtransformstring)/>")) if adjust_vb # some SVG tools don't like this huge rects (e.g. inkscape) # => replace 0,0,16777215,16777215 with viewBox coordinates - mid=replace(mid,r"(?\" y=\")0(?\" width=\")16777215(?\" height=\")16777215(?\".*?/>)"is => SubstitutionString("\\g$(vbx)\\g$(vby)\\g$(vbw)\\g$(vbh)\\g")) + mid = replace(mid, r"(?\" y=\")0(?\" width=\")16777215(?\" height=\")16777215(?\".*?/>)"is => SubstitutionString("\\g$(vbx)\\g$(vby)\\g$(vbw)\\g$(vbh)\\g")) end - adjusted_buffer=head*mid*tail + adjusted_buffer = head * mid * tail end end end end end end - adjusted_buffer=UInt8.(collect(adjusted_buffer)) + adjusted_buffer = UInt8.(collect(adjusted_buffer)) return adjusted_buffer end @@ -781,67 +780,64 @@ Example:\ tail="...tail" ``` """ -function _split_string_into_head_mid_tail(s,id) - head="" - mid=s - tail="" - split_ok=false +function _split_string_into_head_mid_tail(s, id) + head = "" + mid = s + tail = "" + split_ok = false # find all group start elements - startgroups=findall(r"|\s+?.*?>)"is,s) + startgroups = findall(r"|\s+?.*?>)"is, s) # find all group end elements - endgroups=findall(r""is,s) + endgroups = findall(r""is, s) # there must be as many start elements as end elements - if length(startgroups) == length(endgroups) - #if length(startgroups) != length(endgroups) - # @warn "number of group starting tags do not match number of closing tags in SVG" - #else + if length(startgroups) == length(endgroups) + #if length(startgroups) != length(endgroups) + # @warn "number of group starting tags do not match number of closing tags in SVG" + #else # find the group start element with the proper id - first_group=findfirst(Regex("]*?id=\"$(id)\".*?>","is"),s) - group_start_index=findfirst(e->e==first_group,startgroups) + first_group = findfirst(Regex("]*?id=\"$(id)\".*?>", "is"), s) + group_start_index = findfirst(e -> e == first_group, startgroups) if !isnothing(group_start_index) && !isempty(group_start_index) - mid_start=first_group[1] - group_end_index=0 + mid_start = first_group[1] + group_end_index = 0 # starting with group start element with the proper id traverse the end group elements # and the start group elements until the number traversed are equal the first time - while group_start_index-group_end_index !== 0 && group_start_index < length(startgroups) && group_end_index <= length(endgroups) - group_end_index+=1 + while group_start_index - group_end_index !== 0 && group_start_index < length(startgroups) && group_end_index <= length(endgroups) + group_end_index += 1 while group_end_index <= length(endgroups) && endgroups[group_end_index][1] < startgroups[group_start_index][1] - group_end_index+=1 + group_end_index += 1 end - while group_start_index < length(startgroups) && startgroups[group_start_index+1][1] < endgroups[group_end_index][1] - group_start_index+=1 + while group_start_index < length(startgroups) && startgroups[group_start_index + 1][1] < endgroups[group_end_index][1] + group_start_index += 1 end end - if group_start_index-group_end_index == 0 - mid_end=endgroups[group_end_index][end] + if group_start_index - group_end_index == 0 + mid_end = endgroups[group_end_index][end] # start and end character of mid is found, construct the substrings - if prevind(s,mid_start) > 0 - head=s[1:prevind(s,mid_start)] + if prevind(s, mid_start) > 0 + head = s[1:prevind(s, mid_start)] end - if nextind(s,mid_end)>0 - tail=s[nextind(s,mid_end):end] + if nextind(s, mid_end) > 0 + tail = s[nextind(s, mid_end):end] end - mid=s[mid_start:mid_end] - split_ok=true + mid = s[mid_start:mid_end] + split_ok = true end end end - return (head,mid,tail,split_ok) + return (head, mid, tail, split_ok) end @doc raw""" - finish() - -Finish the drawing, and close the file. You may be able to open it in an -external viewer application with `preview()`. - finish(;svgpostprocess = false, addmarker = true) + +Finish the drawing, close any related files. You may be able to view the drawing +in another application with `preview()`. -For more information about `svgpostprocess` and `addmarker` see help for `Luxor._adjust_background_rects` - - ?Luxor._adjust_background_rects +For more information about `svgpostprocess` and `addmarker` see help for +`Luxor._adjust_background_rects` """ -function finish(;svgpostprocess = false, addmarker = true) +function finish(; svgpostprocess = false, addmarker = true) if _current_surface_ptr() == C_NULL # Already finished return false @@ -850,20 +846,20 @@ function finish(;svgpostprocess = false, addmarker = true) Cairo.write_to_png(_current_surface(), _current_buffer()) end - if _current_surface_type() == :image && - ( - typeof(_current_surface()) == Cairo.CairoSurfaceImage{ARGB32} || - typeof(_current_surface()) == Cairo.CairoSurfaceImage{RGB24} - ) && - endswith(_current_filename(), r"\.png"i) - Cairo.write_to_png(_current_surface(), _current_buffer()) + if _current_surface_type() == :image && + ( + typeof(_current_surface()) == Cairo.CairoSurfaceImage{ARGB32} || + typeof(_current_surface()) == Cairo.CairoSurfaceImage{RGB24} + ) && + endswith(_current_filename(), r"\.png"i) + Cairo.write_to_png(_current_surface(), _current_buffer()) end Cairo.finish(_current_surface()) Cairo.destroy(_current_surface()) if _current_filename() != "" - if _current_surface_type() != :svg || ! svgpostprocess + if _current_surface_type() != :svg || !svgpostprocess write(_current_filename(), _current_bufferdata()) else # next function call adresses the issue in @@ -877,9 +873,9 @@ function finish(;svgpostprocess = false, addmarker = true) # which is applied to every element including the background rects. # This transformation needs to be inversed for the background rects # which is added in this function. - buffer=_adjust_background_rects(copy(_current_bufferdata()); addmarker = addmarker) + buffer = _adjust_background_rects(copy(_current_bufferdata()); addmarker = addmarker) # hopefully safe as we are at the end of finish: - _current_drawing()[_current_drawing_index()].bufferdata=buffer + _current_drawing()[_current_drawing_index()].bufferdata = buffer write(_current_filename(), buffer) end end @@ -927,16 +923,16 @@ pngdrawing = snapshot(fname = "temp.png", cb = cb, scalefactor = 10) The last example would return and also write a png drawing with 1024 x 960 pixels to storage. """ function snapshot(; - fname = :png, - cb = missing, - scalefactor = 1.0, - addmarker = true) + fname = :png, + cb = missing, + scalefactor = 1.0, + addmarker = true) rd = currentdrawing() isbits(rd) && return false # currentdrawing provided 'info' if ismissing(cb) if isnan(rd.width) || isnan(rd.height) - @info "The current recording surface has no bounds. Define a crop box for snapshot." - return false + @info "The current recording surface has no bounds. Define a crop box for snapshot." + return false end # When no cropping box is given, we take the intention # to be a snapshot of the entire rectangular surface, @@ -992,7 +988,7 @@ function snapshot(fname, cb, scalefactor; addmarker = true) nh = Float64(round(scalefactor * boxheight(cb))) # New drawing ctm - user space origin and device space origin at top left - nm = scalefactor.* [rmai[1], rmai[2], rmai[3], rmai[4], 0.0, 0.0] + nm = scalefactor .* [rmai[1], rmai[2], rmai[3], rmai[4], 0.0, 0.0] # Create new drawing, to which we'll project a snapshot nd = Drawing(round(nw), round(nh), fname) @@ -1009,7 +1005,7 @@ function snapshot(fname, cb, scalefactor; addmarker = true) paint() # Even in-memory drawings are finished, since such drawings are displayed. - finish(;svgpostprocess = true, addmarker = addmarker) + finish(; svgpostprocess = true, addmarker = addmarker) # Switch back to continue recording _current_drawing()[_current_drawing_index()] = rd @@ -1017,23 +1013,23 @@ function snapshot(fname, cb, scalefactor; addmarker = true) nd end - """ preview() -If working in a notebook (eg Jupyter/IJulia), display a PNG or SVG file in the notebook. +If you're working in a notebook (eg Jupyter/IJulia), display a PNG or SVG file +in the notebook. -If working in Juno, display a PNG or SVG file in the Plot pane. +If you're working in VS-Code, display a PNG or SVG file in the Plots pane. Drawings of type :image should be converted to a matrix with `image_as_matrix()` before calling `finish()`. Otherwise: -- on macOS, open the file in the default application, which is probably the Preview.app for - PNG and PDF, and Safari for SVG -- on Unix, open the file with `xdg-open` -- on Windows, refer to `COMSPEC`. + - on macOS, open the file in the default application, which is probably the + Preview.app for PNG and PDF, and Safari for SVG + - on Unix, open the file with `xdg-open` + - on Windows, refer to `COMSPEC`. """ function preview() @debug "preview()" @@ -1089,19 +1085,19 @@ default is 600 by 600). The file is saved in the current working directory as @svg circle(O, 20, :fill) 400 1200 "/tmp/test.svg" @svg begin - setline(10) - sethue("purple") - circle(O, 20, :fill) - end + setline(10) + sethue("purple") + circle(O, 20, :fill) +end @svg begin - setline(10) - sethue("purple") - circle(O, 20, :fill) - end 1200 1200 + setline(10) + sethue("purple") + circle(O, 20, :fill) +end 1200 1200 ``` """ -macro svg(body, width=600, height=600, fname="luxor-drawing-$(Dates.format(Dates.now(), "HHMMSS_s")).svg") +macro svg(body, width = 600, height = 600, fname = "luxor-drawing-$(Dates.format(Dates.now(), "HHMMSS_s")).svg") quote local lfname = _add_ext($(esc(fname)), :svg) Drawing($(esc(width)), $(esc(height)), lfname) @@ -1135,20 +1131,19 @@ default is 600 by 600). The file is saved in the current working directory as @png circle(O, 20, :fill) 400 1200 "/tmp/round.png" @png begin - setline(10) - sethue("purple") - circle(O, 20, :fill) - end - + setline(10) + sethue("purple") + circle(O, 20, :fill) +end @png begin - setline(10) - sethue("purple") - circle(O, 20, :fill) - end 1200 1200 + setline(10) + sethue("purple") + circle(O, 20, :fill) +end 1200 1200 ``` """ -macro png(body, width=600, height=600, fname="luxor-drawing-$(Dates.format(Dates.now(), "HHMMSS_s")).png") +macro png(body, width = 600, height = 600, fname = "luxor-drawing-$(Dates.format(Dates.now(), "HHMMSS_s")).png") quote local lfname = _add_ext($(esc(fname)), :png) Drawing($(esc(width)), $(esc(height)), lfname) @@ -1168,7 +1163,6 @@ Create and preview an PDF drawing, optionally specifying width and height (the default is 600 by 600). The file is saved in the current working directory as `filename` if supplied, or `luxor-drawing(timestamp).pdf`. - ### Examples ```julia @@ -1183,20 +1177,20 @@ default is 600 by 600). The file is saved in the current working directory as @pdf circle(O, 20, :fill) 400 1200 "/tmp/A0-version.pdf" @pdf begin - setline(10) - sethue("purple") - circle(O, 20, :fill) - end + setline(10) + sethue("purple") + circle(O, 20, :fill) +end @pdf begin - setline(10) - sethue("purple") - circle(O, 20, :fill) - end 1200 1200 + setline(10) + sethue("purple") + circle(O, 20, :fill) +end 1200 1200 ``` """ -macro pdf(body, width=600, height=600, fname="luxor-drawing-$(Dates.format(Dates.now(), "HHMMSS_s")).pdf") - quote +macro pdf(body, width = 600, height = 600, fname = "luxor-drawing-$(Dates.format(Dates.now(), "HHMMSS_s")).pdf") + quote local lfname = _add_ext($(esc(fname)), :pdf) Drawing($(esc(width)), $(esc(height)), lfname) origin() @@ -1231,21 +1225,21 @@ On some platforms, EPS files are converted automatically to PDF when previewed. @eps circle(O, 20, :fill) 400 1200 "/tmp/A0-version.eps" @eps begin - setline(10) - sethue("purple") - circle(O, 20, :fill) - end + setline(10) + sethue("purple") + circle(O, 20, :fill) +end @eps begin - setline(10) - sethue("purple") - circle(O, 20, :fill) - end 1200 1200 + setline(10) + sethue("purple") + circle(O, 20, :fill) +end 1200 1200 ``` """ -macro eps(body, width=600, height=600, fname="luxor-drawing-$(Dates.format(Dates.now(), "HHMMSS_s")).eps") +macro eps(body, width = 600, height = 600, fname = "luxor-drawing-$(Dates.format(Dates.now(), "HHMMSS_s")).eps") quote - local lfname = _add_ext($(esc(fname)), :eps) + local lfname = _add_ext($(esc(fname)), :eps) Drawing($(esc(width)), $(esc(height)), lfname) origin() background("white") @@ -1271,22 +1265,20 @@ default is 600 by 600). The drawing is stored in memory, not in a file on disk. @draw circle(O, 20, :fill) 400 1200 - @draw begin - setline(10) - sethue("purple") - circle(O, 20, :fill) - end - + setline(10) + sethue("purple") + circle(O, 20, :fill) +end @draw begin - setline(10) - sethue("purple") - circle(O, 20, :fill) - end 1200 1200 + setline(10) + sethue("purple") + circle(O, 20, :fill) +end 1200 1200 ``` """ -macro draw(body, width=600, height=600) +macro draw(body, width = 600, height = 600) quote Drawing($(esc(width)), $(esc(height)), :png) origin() @@ -1321,7 +1313,7 @@ finish() ``` """ function image_as_matrix() - if ! isassigned(_current_drawing(),_current_drawing_index()) + if !isassigned(_current_drawing(), _current_drawing_index()) error("no current drawing") end w = Int(_current_surface().width) @@ -1329,7 +1321,7 @@ function image_as_matrix() z = zeros(UInt32, w, h) # create a new image surface to receive the data from the current drawing # flipxy: see issue https://github.com/Wikunia/Javis.jl/pull/149 - imagesurface = CairoImageSurface(z, Cairo.FORMAT_ARGB32, flipxy=false) + imagesurface = CairoImageSurface(z, Cairo.FORMAT_ARGB32, flipxy = false) cr = Cairo.CairoContext(imagesurface) Cairo.set_source_surface(cr, _current_surface(), 0, 0) Cairo.paint(cr) @@ -1351,6 +1343,7 @@ function. The default drawing is 256 by 256 points. You don't need `finish()` (the macro calls it), and it's not previewed by `preview()`. + ``` m = @imagematrix begin sethue("red") @@ -1440,7 +1433,7 @@ julia> @imagematrix begin picks up the default alpha of 1.0. """ -macro imagematrix(body, width=256, height=256) +macro imagematrix(body, width = 256, height = 256) quote Drawing($(esc(width)), $(esc(height)), :image) origin() @@ -1472,12 +1465,12 @@ Images.RGB.(m) ``` """ function image_as_matrix!(buffer) - if ! isassigned(_current_drawing(),_current_drawing_index()) + if !isassigned(_current_drawing(), _current_drawing_index()) error("no current drawing") end # create a new image surface to receive the data from the current drawing # flipxy: see issue https://github.com/Wikunia/Javis.jl/pull/149 - imagesurface = Cairo.CairoImageSurface(buffer, Cairo.FORMAT_ARGB32, flipxy=false) + imagesurface = Cairo.CairoImageSurface(buffer, Cairo.FORMAT_ARGB32, flipxy = false) cr = Cairo.CairoContext(imagesurface) Cairo.set_source_surface(cr, Luxor._current_surface(), 0, 0) Cairo.paint(cr) @@ -1500,7 +1493,7 @@ m = @imagematrix! buffer juliacircles(40) 200 150; Images.RGB.(m) ``` """ -macro imagematrix!(buffer, body, width=256, height=256) +macro imagematrix!(buffer, body, width = 256, height = 256) quote Drawing($(esc(width)), $(esc(height)), :image) origin() @@ -1537,7 +1530,8 @@ PlutoUI.Show(MIME"image/svg+xml"(), svgstring()) ## Example -This example manipulates the raw SVG code representing the Julia logo: +This example examines the generated SVG code produced by drawing +the Julia logo. ``` Drawing(500, 500, :svg) @@ -1555,6 +1549,8 @@ eachmatch(r"rgb.*?;", s) |> collect RegexMatch("rgb(22%,59.6%,14.9%);") ``` +Here's another example, post-processing the SVG file with the `svgo` optimizer. + ``` @drawsvg begin background("midnightblue") @@ -1588,7 +1584,7 @@ Create and preview an SVG drawing. Like `@draw` but using SVG format. Unlike `@draw` (PNG), there is no background, by default. """ -macro drawsvg(body, width=600, height=600) +macro drawsvg(body, width = 600, height = 600) quote Drawing($(esc(width)), $(esc(height)), :svg) origin() @@ -1600,7 +1596,6 @@ macro drawsvg(body, width=600, height=600) end end """ - @savesvg begin body end w h @@ -1609,8 +1604,26 @@ Like `@drawsvg` but returns the raw SVG code of the drawing in a string. Uses `svgstring`. Unlike `@draw` (PNG), there is no background, by default. + +THis example scans the generated SVG for color values: + +```julia +s = @savesvg begin + julialogo() +end + +eachmatch(r"rgb.*?;", s) |> collect + +5-element Vector{RegexMatch}: + RegexMatch("rgb(0%,0%,0%);") + RegexMatch("rgb(25.1%,38.8%,84.7%);") + RegexMatch("rgb(22%,59.6%,14.9%);") + RegexMatch("rgb(58.4%,34.5%,69.8%);") + RegexMatch("rgb(79.6%,23.5%,20%);") + +``` """ -macro savesvg(body, width=600, height=600) +macro savesvg(body, width = 600, height = 600) quote Drawing($(esc(width)), $(esc(height)), :svg) origin() diff --git a/src/matrix.jl b/src/matrix.jl index f63cb95a..621b467f 100644 --- a/src/matrix.jl +++ b/src/matrix.jl @@ -3,82 +3,93 @@ """ getmatrix() -Get the current matrix. Returns an array of six float64 numbers: +Get the current workspace (position, scale, and orientation) as a 6-element +vector: -- xx component of the affine transformation +``` +[xx, yx, xy, yy, x0, y0] +``` + + - `xx` component of the affine transformation + - `yx` component of the affine transformation + - `xy` component of the affine transformation + - `yy` component of the affine transformation + - `x0` translation component of the affine transformation + - `y0` translation component of the affine transformation + +When a drawing is first created, the 'matrix' looks like this: + + getmatrix() = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] + +When the origin is moved to 400/400, it looks like this: + + getmatrix() = [1.0, 0.0, 0.0, 1.0, 400.0, 400.0] + +To reset the 'matrix' to the original: -- yx component of the affine transformation + setmatrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]) -- xy component of the affine transformation +To modify the current 'matrix' by multiplying it by a 6 element 'matrix' `a`, +see `transform(a::Array)`. -- yy component of the affine transformation +To convert between Luxor/Cairo 'matrix' format (6-element Vector{Float64}) and a +3x3 Julia matrix, use `cairotojuliamatrix(c)` and `juliatocairomatrix(c)`. -- x0 translation component of the affine transformation +See also `rotationmatrix(a)`, `translationmatrix()`, and `scalingmatrix()`. -- y0 translation component of the affine transformation +# Extended help -Some basic matrix transforms: +Here are some basic matrix transforms: -- translate + - translate `transform([1, 0, 0, 1, dx, dy])` shifts by `dx`, `dy` -- scale + - scale `transform([fx 0 0 fy 0 0])` scales by `fx` and `fy` -- rotate + - rotate `transform([cos(a), -sin(a), sin(a), cos(a), 0, 0])` rotates around to `a` radians rotate around O: [c -s s c 0 0] -- shear + - shear `transform([1 0 a 1 0 0])` shears in x direction by `a` shear in y direction by `a`: [1 a 0 1 0 0] -- x-skew + - x-skew `transform([1, 0, tan(a), 1, 0, 0])` skews in x by `a` -- y-skew + - y-skew `transform([1, tan(a), 0, 1, 0, 0])` skews in y by `a` -- flip + - flip `transform([fx, 0, 0, fy, centerx * (1 - fx), centery * (fy-1)])` flips with center at `centerx`/`centery` -- reflect + - reflect `transform([1 0 0 -1 0 0])` reflects in xaxis `transform([-1 0 0 1 0 0])` reflects in yaxis - -When a drawing is first created, the matrix looks like this: - - getmatrix() = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] - -When the origin is moved to 400/400, it looks like this: - - getmatrix() = [1.0, 0.0, 0.0, 1.0, 400.0, 400.0] - -To reset the matrix to the original: - - setmatrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]) - """ function getmatrix() gm = Cairo.get_matrix(_get_current_cr()) - return([gm.xx, gm.yx, gm.xy, gm.yy, gm.x0, gm.y0]) + return ([gm.xx, gm.yx, gm.xy, gm.yy, gm.x0, gm.y0]) end """ setmatrix(m::Array) -Change the current matrix to matrix `m`. Use `getmatrix()` to get the current matrix. +Change the current matrix to 6-element matrix `m`. + +See `getmatrix()` for details. """ function setmatrix(m::Array) if eltype(m) != Float64 @@ -104,17 +115,17 @@ For example, to skew the current state by 45 degrees in x and move by 20 in y di transform([1, 0, tand(45), 1, 0, 20]) -Use `getmatrix()` to get the current matrix. +See `getmatrix()` for details. """ function transform(a::Array) b = Cairo.get_matrix(_get_current_cr()) setmatrix([ - (a[1] * b.xx) + a[2] * b.xy, # xx - (a[1] * b.yx) + a[2] * b.yy, # yx - (a[3] * b.xx) + a[4] * b.xy, # xy - (a[3] * b.yx) + a[4] * b.yy, # yy - (a[5] * b.xx) + (a[6] * b.xy) + b.x0, # x0 - (a[5] * b.yx) + (a[6] * b.yy) + b.y0 # y0 + (a[1] * b.xx) + a[2] * b.xy, # xx + (a[1] * b.yx) + a[2] * b.yy, # yx + (a[3] * b.xx) + a[4] * b.xy, # xy + (a[3] * b.yx) + a[4] * b.yy, # yy + (a[5] * b.xx) + (a[6] * b.xy) + b.x0, # x0 + (a[5] * b.yx) + (a[6] * b.yy) + b.y0, # y0 ]) end @@ -122,48 +133,58 @@ end rotationmatrix(a) Return a 3x3 Julia matrix that will apply a rotation through `a` radians. + +See `getmatrix()` for details. """ function rotationmatrix(a) - return ([cos(a) -sin(a) 0.0 ; - sin(a) cos(a) 0.0 ; - 0.0 0.0 1.0 ]) + return ([cos(a) -sin(a) 0.0 + sin(a) cos(a) 0.0 + 0.0 0.0 1.0]) end """ translationmatrix(x, y) Return a 3x3 Julia matrix that will apply a translation in `x` and `y`. + +See `getmatrix()` for details. """ function translationmatrix(x, y) - return ([1.0 0.0 x ; - 0.0 1.0 y ; - 0.0 0.0 1.0 ]) + return ([1.0 0.0 x + 0.0 1.0 y + 0.0 0.0 1.0]) end """ scalingmatrix(sx, sy) Return a 3x3 Julia matrix that will apply a scaling by `sx` and `sy`. + +See `getmatrix()` for details. """ function scalingmatrix(sx, sy) - return ([sx 0.0 0.0 ; - 0.0 sy 0.0 ; - 0.0 0.0 1.0]) + return ([sx 0.0 0.0 + 0.0 sy 0.0 + 0.0 0.0 1.0]) end """ cairotojuliamatrix(c) Return a 3x3 Julia matrix that's the equivalent of the six-element matrix in `c`. + +See `getmatrix()` for details. """ function cairotojuliamatrix(c::Array) - return [c[1] c[3] c[5] ; c[2] c[4] c[6] ; 0.0 0.0 1.0] + return [c[1] c[3] c[5]; c[2] c[4] c[6]; 0.0 0.0 1.0] end """ juliatocairomatrix(c) Return a six-element matrix that's the equivalent of the 3x3 Julia matrix in `c`. + +See `getmatrix()` for details. """ function juliatocairomatrix(c::Matrix) return [c[1] c[2] c[4] c[5] c[7] c[8]] @@ -175,6 +196,11 @@ end Get the rotation of a Julia 3x3 matrix, or the current Luxor rotation. +```julia +getrotation() +0.0 +``` + ```math \\begin{bmatrix} a & b & tx \\\\ @@ -184,6 +210,8 @@ c & d & ty \\\\ ``` The rotation angle is `atan(-b, a)` or `atan(c, d)`. + +See `getmatrix()` for details. """ function getrotation(R::Matrix) # t = atan(-R[4], R[1]) # should be the same as: @@ -199,9 +227,16 @@ end getscale(R::Matrix) getscale() -Get the current scale of a Julia 3x3 matrix, or the current Luxor scale. +Get the current scale of a 3x3 matrix, or the current Luxor scale. Returns a tuple of x and y values. + +```julia +getscale() +(1.0, 1.0) +``` + +See `getmatrix()` for details. """ function getscale(R::Matrix) sx = hypot(R[1], R[2]) @@ -217,9 +252,11 @@ end gettranslation(R::Matrix) gettranslation() -Get the current translation of a Julia 3x3 matrix, or the current Luxor translation. +Get the current translation of a 3x3 matrix R, or get the current Luxor translation. Returns a tuple of x and y values. + +See `getmatrix()` for details. """ function gettranslation(R::Matrix) return (R[7], R[8]) diff --git a/src/point.jl b/src/point.jl index 157a20dd..5718399a 100644 --- a/src/point.jl +++ b/src/point.jl @@ -60,6 +60,21 @@ Base.length(::Point) = 2 """ *(m::Matrix, pt::Point) +Transform a point `pt` by the 3×3 matrix `m`. + +```julia +julia> M = [2 0 0; 0 2 0; 0 0 1] +3×3 Matrix{Int64}: + 2 0 0 + 0 2 0 + 0 0 1 + +julia> M * Point(20, 20) +Point(40.0, 40.0) +``` + +To convert between Cairo matrices (6-element Vector{Float64}) to a 3×3 Matrix, +use `cairotojuliamatrix()` and `juliatocairomatrix()`. """ function Base.:*(m::AbstractMatrix, pt::Point) if size(m) != (3, 3) @@ -106,12 +121,24 @@ end # comparisons +""" +isequal(p1::Point, p2::Point) = + isapprox(p1.x, p2.x, atol = 0.00000001) && + (isapprox(p1.y, p2.y, atol = 0.00000001)) + +Compare points. +""" isequal(p1::Point, p2::Point) = isapprox(p1.x, p2.x, atol = 0.00000001) && (isapprox(p1.y, p2.y, atol = 0.00000001)) # allow kwargs +""" +isapprox(p1::Point, p2::Point; atol = 1e-6, kwargs...) + +Compare points. +""" function Base.isapprox(p1::Point, p2::Point; - atol = 1e-6, kwargs...) + atol = 1e-6, kwargs...) return isapprox(p1.x, p2.x, atol = atol, kwargs...) && isapprox(p1.y, p2.y, atol = atol, kwargs...) end @@ -210,7 +237,7 @@ is perpendicular to `p3`. """ function perpendicular(p1::Point, p2::Point, p3::Point) v2 = p2 - p1 - return p1 + (dotproduct(p3 - p1, v2)/dotproduct(v2, v2)) * v2 + return p1 + (dotproduct(p3 - p1, v2) / dotproduct(v2, v2)) * v2 end """ @@ -280,7 +307,7 @@ If `extended` is false (the default) the point must lie on the line segment betw either direction. """ function ispointonline(pt::Point, pt1::Point, pt2::Point; - atol = 10E-5, extended = false) + atol = 10E-5, extended = false) dxc = pt.x - pt1.x dyc = pt.y - pt1.y dxl = pt2.x - pt1.x @@ -312,8 +339,8 @@ end Return `true` if `pt` lies on the polygon `pgon.` """ function ispointonpoly(pt::Point, pgon::Array{Point,1}; - atol = 10E-5) - @inbounds for i = 1:length(pgon) + atol = 10E-5) + @inbounds for i in 1:length(pgon) p1 = pgon[i] p2 = pgon[mod1(i + 1, end)] if ispointonline(pt, p1, p2, atol = atol) @@ -383,9 +410,9 @@ Return a tuple of `(n, pt1, pt2)` where -- `n` is the number of intersections, `0`, `1`, or `2` -- `pt1` is first intersection point, or `Point(0, 0)` if none -- `pt2` is the second intersection point, or `Point(0, 0)` if none + - `n` is the number of intersections, `0`, `1`, or `2` + - `pt1` is first intersection point, or `Point(0, 0)` if none + - `pt2` is the second intersection point, or `Point(0, 0)` if none The calculated intersection points won't necessarily lie on the line segment between `p1` and `p2`. """ @@ -399,6 +426,7 @@ Convert a tuple of two numbers to a Point of x, y Cartesian coordinates. @polar (10, pi/4) @polar [10, pi/4] + @polar 10, pi/4 produces @@ -413,7 +441,7 @@ end """ polar(r, theta) -Convert point in polar form (radius and angle) to a Point. +Convert a point specified in polar form (radius and angle) to a Point. polar(10, pi/4) @@ -427,7 +455,7 @@ polar(r, theta) = Point(r * cos(theta), r * sin(theta)) intersectionlines(p0, p1, p2, p3; crossingonly=false) -Find point where two lines intersect. +Find the point where two lines intersect. If `crossingonly == true` the point of intersection must lie on both lines. @@ -444,7 +472,7 @@ If the lines are collinear and share a point in common, that is the intersection point. """ function intersectionlines(p0::Point, p1::Point, p2::Point, p3::Point; - crossingonly = false) + crossingonly = false) resultflag = false resultip = Point(0.0, 0.0) if p0 == p1 # no lines at all @@ -536,7 +564,11 @@ end """ currentpoint() -Return the current point. +Return the current point. This is the most recent point in the current path, as +defined by one of the path building functions such as `move()`, `line()`, +`curve()`, `arc()`, `rline()`, and `rmove()`. + +To see if there is a current point, use `hascurrentpoint()`. """ function currentpoint() x, y = Cairo.get_current_point(_get_current_cr()) @@ -546,8 +578,13 @@ end """ hascurrentpoint() -Return true if there is a current point. Obtain the current point -with `currentpoint()`. +Return true if there is a current point. This is the most recent point in the +current path, as defined by one of the path building functions such as `move()`, +`line()`, `curve()`, `arc()`, `rline()`, and `rmove()`. + +To obtain the current point, use `currentpoint()`. + +There's no current point after `strokepath()` and `strokepath()` calls. """ function hascurrentpoint() return Cairo.has_current_point(_get_current_cr()) @@ -559,13 +596,14 @@ end Return the world coordinates of `pt`. -The default coordinate system for Luxor/Cairo is that the top left corner is 0/0. -If you use `origin()`, everything moves to the center of the drawing, and this function -with the default `centered` option assumes an `origin()` function. If you choose -`centered=false`, the returned coordinates will be relative to the top left corner of -the drawing. +The default coordinate system for Luxor drawings is that the top left corner is +0/0. If you use `origin()` (or the various `@-` macro shortcuts), everything +moves to the center of the drawing, and this function with the default +`centered` option assumes an `origin()` function. If you choose +`centered=false`, the returned coordinates will be relative to the top left +corner of the drawing. -``` +```julia origin() translate(120, 120) @show currentpoint() # => Point(0.0, 0.0) @@ -573,7 +611,7 @@ translate(120, 120) ``` """ function getworldposition(pt::Point = O; - centered = true) + centered = true) x, y = cairotojuliamatrix(getmatrix()) * [pt.x, pt.y, 1] return Point(x, y) - (centered ? (Luxor._current_width() / 2.0, Luxor._current_height() / 2.0) : (0, 0)) @@ -656,11 +694,10 @@ rotatepoint(targetpt::Point, angle) = rotatepoint(targetpt, O, angle) For a line passing through points A and B: -- return true if point C is on the left of the line - -- return false if point C lies on the line + - return true if point C is on the left of the line -- return false if point C is on the right of the line + - return false if point C lies on the line + - return false if point C is on the right of the line """ function ispointonleftofline(A::Point, B::Point, C::Point) z = ((B.x - A.x) * (C.y - A.y)) - ((B.y - A.y) * (C.x - A.x))