Colors and styles

Colors and styles

Color and opacity

For color definitions and conversions, you can use Colors.jl.

setcolor() and sethue() apply a single solid or transparent color to shapes.

setblend() applies a smooth transition between two or more colors.

setmesh() applies a color mesh.

The difference between the setcolor() and sethue() functions is that sethue() is independent of alpha opacity, so you can change the hue without changing the current opacity value.

Named colors, such as "gold", or "lavender", can be found in Colors.color_names. This code shows the first 625 colors.

cols = collect(Colors.color_names)
tiles = Tiler(800, 500, 25, 25)
for (pos, n) in tiles
    sethue(cols[n][1])
    box(pos, tiles.tilewidth, tiles.tileheight, :fill)
    clab = convert(Lab, parse(Colorant, cols[n][1]))
    labelbrightness = 100 - clab.l
    sethue(convert(RGB, Lab(labelbrightness, clab.b, clab.a)))
    text(string(cols[n][1]), pos, halign=:center)
end
WARNING: Base.uninitialized is deprecated, use undef instead.
  likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
WARNING: Base.uninitialized is deprecated, use undef instead.
  likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
WARNING: Base.uninitialized is deprecated, use undef instead.
  likely near /Users/travis/build/JuliaGraphics/Luxor.jl/docs/make.jl:3
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│   caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145

line endings

Some fiddling with Lab colors adjusts the label color to make it stand out against the background.

Luxor.sethueFunction.
sethue("black")
sethue(0.3, 0.7, 0.9)
setcolor(sethue("red")..., .2)

Set the color without changing opacity.

sethue() is like setcolor(), but we sometimes want to change the current color without changing alpha/opacity. Using sethue() rather than setcolor() doesn't change the current alpha opacity.

See also setcolor.

source
sethue(col::ColorTypes.Colorant)

Set the color without changing the current alpha/opacity:

source
sethue(0.3, 0.7, 0.9)

Set the color's r, g, b values. Use setcolor(r,g,b,a) to set transparent colors.

source
sethue((r, g, b))

Set the color to the tuple's values.

source
sethue((r, g, b, a))

Set the color to the tuple's values.

source
Luxor.setcolorFunction.
setcolor("gold")
setcolor("darkturquoise")

Set the current color to a named color. This use the definitions in Colors.jl to convert a string to RGBA eg setcolor("gold") or "green", "darkturquoise", "lavender", etc. The list is at Colors.color_names.

Use sethue() for changing colors without changing current opacity level.

sethue() and setcolor() return the three or four values that were used:

julia> setcolor(sethue("red")..., .8)

(1.0,0.0,0.0,0.8)

julia> sethue(setcolor("red")[1:3]...)

(1.0,0.0,0.0)

You can also do:

using Colors
sethue(colorant"red")

See also setcolor.

source
setcolor(r, g, b)
setcolor(r, g, b, alpha)
setcolor(color)
setcolor(col::ColorTypes.Colorant)
setcolor(sethue("red")..., .2)

Set the current color.

Examples:

setcolor(convert(Colors.HSV, Colors.RGB(0.5, 1, 1)))
setcolor(.2, .3, .4, .5)
setcolor(convert(Colors.HSV, Colors.RGB(0.5, 1, 1)))

for i in 1:15:360
   setcolor(convert(Colors.RGB, Colors.HSV(i, 1, 1)))
   ...
end

See also sethue.

source
setcolor((r, g, b))

Set the color to the tuple's values.

source
setcolor((r, g, b, a))

Set the color to the tuple's values.

source
Luxor.setgrayFunction.
setgray(n)
setgrey(n)

Set the color to a gray level of n, where n is between 0 and 1.

source
Luxor.setopacityFunction.
setopacity(alpha)

Set the current opacity to a value between 0 and 1. This modifies the alpha value of the current color.

source
Luxor.randomhueFunction.
randomhue()

Set a random hue, without changing the current alpha opacity.

source
Luxor.randomcolorFunction.
randomcolor()

Set a random color. This may change the current alpha opacity too.

source
Luxor.setblendFunction.
setblend(blend)

Start using the named blend for filling graphics.

This aligns the original coordinates of the blend definition with the current axes.

source

Line styles

There are set- functions for controlling subsequent lines' width, end shapes, join behavior, and dash patterns:

for l in 1:3
    sethue("black")
    setline(20)
    setlinecap(["butt", "square", "round"][l])
    textcentred(["butt", "square", "round"][l], 80l, 80)
    setlinejoin(["round", "miter", "bevel"][l])
    textcentred(["round", "miter", "bevel"][l], 80l, 120)
    poly(ngon(Point(80l, 0), 20, 3, 0, vertices=true), :strokepreserve, close=false)
    sethue("white")
    setline(1)
    strokepath()
end
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│   caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145

line endings

patterns = ["solid", "dotted", "dot", "dotdashed", "longdashed",
  "shortdashed", "dash", "dashed", "dotdotdashed", "dotdotdotdashed"]
setline(12)

table = Table(fill(20, length(patterns)), [50, 300])
text.(patterns, table[:, 1], halign=:right, valign=:middle)

for p in 1:length(patterns)
    setdash(patterns[p])
    pt = table[p, 2]
    line(pt - (150, 0), pt + (150, 0), :stroke)
end
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│   caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145

dashes

Luxor.setlineFunction.
setline(n)

Set the line width.

source
Luxor.setlinecapFunction.
setlinecap(s)

Set the line ends. s can be "butt" (the default), "square", or "round".

source
Luxor.setlinejoinFunction.
setlinejoin("miter")
setlinejoin("round")
setlinejoin("bevel")

Set the line join style, or how to render the junction of two lines when stroking.

source
Luxor.setdashFunction.
setlinedash("dot")

Set the dash pattern to one of: "solid", "dotted", "dot", "dotdashed", "longdashed", "shortdashed", "dash", "dashed", "dotdotdashed", "dotdotdotdashed"

source
Luxor.fillstrokeFunction.
fillstroke()

Fill and stroke the current path.

source
Luxor.strokepathFunction.
strokepath()

Stroke the current path with the current line width, line join, line cap, and dash settings. The current path is then cleared.

source
Luxor.fillpathFunction.
fillpath()

Fill the current path according to the current settings. The current path is then cleared.

source
Luxor.strokepreserveFunction.
strokepreserve()

Stroke the current path with current line width, line join, line cap, and dash settings, but then keep the path current.

source
Luxor.fillpreserveFunction.
fillpreserve()

Fill the current path with current settings, but then keep the path current.

source
Luxor.paintFunction.
paint()

Paint the current clip region with the current settings.

source
Luxor.do_actionFunction.
do_action(action)

This is usually called by other graphics functions. Actions for graphics commands include :fill, :stroke, :clip, :fillstroke, :fillpreserve, :strokepreserve, :none, and :path.

source

Soon you'll be able to define dash patterns in Luxor. For now:

dashes = [50.0,  # ink
          10.0,  # skip
          10.0,  # ink
          10.0   # skip
          ]
offset = -50.0
Cairo.set_dash(currentdrawing.cr, dashes, offset)

Blends

A blend is a color gradient. Use setblend() to select a blend in the same way that you'd use setcolor() and sethue() to select a solid color.

You can make linear or radial blends. Use blend() in either case.

To create a simple linear blend between two colors, supply two points and two colors to blend():

orangeblue = blend(Point(-200, 0), Point(200, 0), "orange", "blue")
setblend(orangeblue)
box(O, 400, 100, :fill)
rulers()
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│   caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145

linear blend

And for a radial blend, provide two point/radius pairs, and two colors:

greenmagenta = blend(Point(0, 0), 5, Point(0, 0), 150, "green", "magenta")
setblend(greenmagenta)
box(O, 400, 200, :fill)
rulers()
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│   caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145

radial blends

You can also use blend() to create an empty blend. Then you use addstop() to define the locations of specific colors along the blend, where 0 is the start, and 1 is the end.

goldblend = blend(Point(-200, 0), Point(200, 0))
addstop(goldblend, 0.0,  "gold4")
addstop(goldblend, 0.25, "gold1")
addstop(goldblend, 0.5,  "gold3")
addstop(goldblend, 0.75, "darkgoldenrod4")
addstop(goldblend, 1.0,  "gold2")
setblend(goldblend)
box(O, 400, 200, :fill)
rulers()
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│   caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145

blends from scratch

When you define blends, the location of the axes (eg the current workspace as defined by translate(), etc.), is important. In the first of the two following examples, the blend is selected before the axes are moved with translate(pos). The blend 'samples' the original location of the blend's definition.

goldblend = blend(Point(0, 0), Point(200, 0))
addstop(goldblend, 0.0,  "gold4")
addstop(goldblend, 0.25, "gold1")
addstop(goldblend, 0.5,  "gold3")
addstop(goldblend, 0.75, "darkgoldenrod4")
addstop(goldblend, 1.0,  "gold2")
setblend(goldblend)
tiles = Tiler(600, 200, 1, 5, margin=10)
for (pos, n) in tiles
    gsave()
    setblend(goldblend)
    translate(pos)
    ellipse(O, tiles.tilewidth, tiles.tilewidth, :fill)
    grestore()
end
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│   caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145

blends 1

Outside the range of the original blend's definition, the same color is used, no matter how far away from the origin you go (there are Cairo options to change this). But in the next example, the blend is relocated to the current axes, which have just been moved to the center of the tile. The blend refers to 0/0 each time, which is at the center of shape.

goldblend = blend(Point(0, 0), Point(200, 0))
addstop(goldblend, 0.0,  "gold4")
addstop(goldblend, 0.25, "gold1")
addstop(goldblend, 0.5,  "gold3")
addstop(goldblend, 0.75, "darkgoldenrod4")
addstop(goldblend, 1.0,  "gold2")
setblend(goldblend)
tiles = Tiler(600, 200, 1, 5, margin=10)
for (pos, n) in tiles
    gsave()
    translate(pos)
    setblend(goldblend)
    ellipse(O, tiles.tilewidth, tiles.tilewidth, :fill)
    grestore()
end
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│   caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145

blends 2

Luxor.blendFunction.
blend(from::Point, to::Point)

Create an empty linear blend.

A blend is a specification of how one color changes into another. Linear blends are defined by two points: parallel lines through these points define the start and stop locations of the blend. The blend is defined relative to the current axes origin. This means that you should be aware of the current axes when you define blends, and when you use them.

To add colors, use addstop().

source
blend(centerpos1, rad1, centerpos2, rad2, color1, color2)

Create a radial blend.

Example:

redblue = blend(
    pos, 0,                   # first circle center and radius
    pos, tiles.tilewidth/2,   # second circle center and radius
    "red",
    "blue"
    )
source
blend(pt1::Point, pt2::Point, color1, color2)

Create a linear blend.

Example:

redblue = blend(pos, pos, "red", "blue")
source
blend(from::Point, startradius, to::Point, endradius)

Create an empty radial blend.

Radial blends are defined by two circles that define the start and stop locations. The first point is the center of the start circle, the first radius is the radius of the first circle.

A new blend is empty. To add colors, use addstop().

source
Luxor.addstopFunction.
addstop(b::Blend, offset, col)
addstop(b::Blend, offset, (r, g, b, a))
addstop(b::Blend, offset, string)

Add a color stop to a blend. The offset specifies the location along the blend's 'control vector', which varies between 0 (beginning of the blend) and 1 (end of the blend). For linear blends, the control vector is from the start point to the end point. For radial blends, the control vector is from any point on the start circle, to the corresponding point on the end circle.

Examples:

blendredblue = blend(Point(0, 0), 0, Point(0, 0), 1)
addstop(blendredblue, 0, setcolor(sethue("red")..., .2))
addstop(blendredblue, 1, setcolor(sethue("blue")..., .2))
addstop(blendredblue, 0.5, sethue(randomhue()...))
addstop(blendredblue, 0.5, setcolor(randomcolor()...))
source

Using blendadjust()

You can use blendadjust() to modify the blend so that objects scaled and positioned after the blend was defined are rendered correctly.

setline(20)

# first line
blendgoldmagenta = blend(Point(-100, 0), Point(100, 0), "gold", "magenta")
setblend(blendgoldmagenta)
line(Point(-100, -50), Point(100, -50))
strokepath()

# second line
blendadjust(blendgoldmagenta, Point(50, 0), 0.5, 0.5)
line(O, Point(100, 0))
strokepath()

# third line
blendadjust(blendgoldmagenta, Point(-50, 50), 0.5, 0.5)
line(Point(-100, 50), Point(0, 50))
strokepath()

# fourth line
gsave()
translate(0, 100)
scale(0.5, 0.5)
setblend(blendgoldmagenta)
line(Point(-100, 0), Point(100, 0))
strokepath()
grestore()
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│   caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145

blends adjust

The blend is defined to span 200 units, horizontally centered at 0/0. The top line is also 200 units long and centered horizontally at 0/0, so the blend is rendered exactly as you'd hope.

The second line is only half as long, at 100 units, centered at 50/0, so blendadjust() is used to relocate the blend's center to the point 50/0 and scale it by 0.5 (100/200).

The third line is also 100 units long, centered at -50/0, so again blendadjust() is used to relocate the blend's center and scale it.

The fourth line shows that you can translate and scale the axes instead of adjusting the blend, if you use setblend() again in the new scene.

Luxor.blendadjustFunction.
blendadjust(ablend, center::Point, xscale, yscale, rot=0)

Modify an existing blend by scaling, translating, and rotating it so that it will fill a shape properly even if the position of the shape is nowhere near the original location of the blend's definition.

For example, if your blend definition was this (notice the 1)

blendgoldmagenta = blend(
        Point(0, 0), 0,                   # first circle center and radius
        Point(0, 0), 1,                   # second circle center and radius
        "gold",
        "magenta"
        )

you can use it in a shape that's 100 units across and centered at pos, by calling this:

blendadjust(blendgoldmagenta, Point(pos.x, pos.y), 100, 100)

then use setblend():

setblend(blendgoldmagenta)
source

Blending (compositing) operators

Graphics software provides ways to modify how the virtual "ink" is applied to existing graphic elements. In PhotoShop and other software products the compositing process is done using blend modes.

Use setmode() to set the blending mode of subsequent graphics.

origin()
# transparent, no background
fontsize(15)
setline(1)
tiles = Tiler(600, 600, 4, 5, margin=30)
modes = length(Luxor.blendingmodes)
setcolor("black")
for (pos, n) in tiles
    n > modes && break
    gsave()
    translate(pos)
    box(O, tiles.tilewidth-10, tiles.tileheight-10, :clip)

    # calculate points for circles
    diag = (Point(-tiles.tilewidth/2, -tiles.tileheight/2),
            Point(tiles.tilewidth/2,  tiles.tileheight/2))
    upper = between(diag, 0.4)
    lower = between(diag, 0.6)

    # first red shape uses default blend operator
    setcolor(0.7, 0, 0, .8)
    circle(upper, tiles.tilewidth/4, :fill)

    # second blue shape shows results of blend operator
    setcolor(0, 0, 0.9, 0.4)
    blendingmode = Luxor.blendingmodes[mod1(n, modes)]
    setmode(blendingmode)
    circle(lower, tiles.tilewidth/4, :fill)

    clipreset()
    grestore()

    gsave()
    translate(pos)
    text(Luxor.blendingmodes[mod1(n, modes)], O.x, O.y + tiles.tilewidth/2, halign=:center)
    grestore()
end
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│   caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145

blend modes

Notice in this example that clipping was used to restrict the area affected by the blending process.

In Cairo these blend modes are called operators. A source for a more detailed explanation can be found here.

You can access the list of modes with the unexported symbol Luxor.blendingmodes.

Luxor.setmodeFunction.
setmode(mode::String)

Set the compositing/blending mode. mode can be one of:

  • "clear" Where the second object is drawn, the first is completely removed.
  • "source" The second object is drawn as if nothing else were below.
  • "over" The default mode: like two transparent slides overlapping.
  • "in" The first object is removed completely, the second is only drawn where the first was.
  • "out" The second object is drawn only where the first one wasn't.
  • "atop" The first object is mostly intact, but mixes both objects in the overlapping area. The second object object is not drawn elsewhere.
  • "dest" Discard the second object completely.
  • "dest_over" Like "over" but draw second object below the first
  • "dest_in" Keep the first object whereever the second one overlaps.
  • "dest_out" The second object is used to reduce the visibility of the first where they overlap.
  • "dest_atop" Like "over" but draw second object below the first.
  • "xor" XOR where the objects overlap
  • "add" Add the overlapping areas together
  • "saturate" Increase Saturation where objects overlap
  • "multiply" Multiply where objects overlap
  • "screen" Input colors are complemented and multiplied, the product is complemented again. The result is at least as light as the lighter of the input colors.
  • "overlay" Multiplies or screens colors, depending on the lightness of the destination color.
  • "darken" Selects the darker of the color values in each component.
  • "lighten" Selects the lighter of the color values in each component.

See the Cairo documentation for details.

source
Luxor.getmodeFunction.
getmode()

Return the current compositing/blending mode as a string.

source

Meshes

A mesh provides smooth shading between three or four colors across a region defined by lines or curves.

To create a mesh, use the mesh() function and save the result as a mesh object. To use a mesh, supply the mesh object to the setmesh() function.

The mesh() function accepts either an array of Bézier paths or a polygon.

This basic example obtains a polygon from the drawing area using box(BoundingBox()... and uses the four corners of the mesh and the four colors in the array to build the mesh. The paint() function fills the drawing.

garishmesh = mesh(
    box(BoundingBox(), vertices=true),
    ["purple", "green", "yellow", "red"])

setmesh(garishmesh)

paint()

setline(2)
sethue("white")
hypotrochoid(180, 81, 130, :stroke)
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│   caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145

mesh 1

The next example uses a Bézier path conversion of a square as the outline of the mesh. Because the box to be filled is larger than the mesh's outlines, not all the box is filled.

setcolor("grey50")
circle.([Point(x, y) for x in -200:25:200, y in -200:25:200], 10, :fill)

bp = makebezierpath(box(O, 300, 300, vertices=true), smoothing=.4)
setline(3)
sethue("black")

drawbezierpath(bp, :stroke)
mesh1 = mesh(bp, [
    Colors.RGBA(1, 0, 0, 1),   # bottom left, red
    Colors.RGBA(1, 1, 1, 0.0), # top left, transparent
    Colors.RGB(0, 0, 1),      # top right, blue
    Colors.RGB(1, 0, 1)        # bottom right, purple
    ])
setmesh(mesh1)
box(O, 500, 500, :fillpreserve)
sethue("grey50")
strokepath()
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│   caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145

mesh 1

The second example uses a polygon defined by ngon() as the outline of the mesh. The mesh is drawn when the path is stroked.

pl = ngon(O, 250, 3, pi/6, vertices=true)
mesh1 = mesh(pl, [
    "purple",
    "green",
    "yellow"
    ])
setmesh(mesh1)
setline(180)
poly(pl, :strokepreserve, close=true)
setline(5)
sethue("black")
strokepath()
┌ Warning: The function `cfunction` is now written as a macro `@cfunction`.
│   caller = get_stream_callback at Cairo.jl:145 [inlined]
└ @ Core Cairo.jl:145

mesh 2

Luxor.meshFunction.
mesh(bezierpath::BezierPath,
     colors=AbstractArray{ColorTypes.Colorant, 1})

Create a mesh. The first three or four elements of the supplied bezierpath define the three or four sides of the mesh shape.

The colors array define the color of each corner point. Colors are reused if necessary. At least one color should be supplied.

Use setmesh() to select the mesh, which will be used to fill shapes.

Example

@svg begin
    bp = makebezierpath(ngon(O, 50, 4, 0, vertices=true))
    mesh1 = mesh(bp, [
        "red",
        Colors.RGB(0, 1, 0),
        Colors.RGB(0, 1, 1),
        Colors.RGB(1, 0, 1)
        ])
    setmesh(mesh1)
    box(O, 500, 500, :fill)
end
source
mesh(points::AbstractArray{Point},
     colors=AbstractArray{ColorTypes.Colorant, 1})

Create a mesh.

Create a mesh. The first three or four sides of the supplied points polygon define the three or four sides of the mesh shape.

The colors array define the color of each corner point. Colors are reused if necessary. At least one color should be supplied.

Example

@svg begin
    pl = ngon(O, 250, 3, pi/6, vertices=true)
    mesh1 = mesh(pl, [
        "purple",
        Colors.RGBA(0.0, 1.0, 0.5, 0.5),
        "yellow"
        ])
    setmesh(mesh1)
    setline(180)
    ngon(O, 250, 3, pi/6, :strokepreserve)
    setline(5)
    sethue("black")
    strokepath()
end
source
Luxor.setmeshFunction.
setmesh(mesh::Mesh)

Select a mesh previously created with mesh() for filling shapes.

source