JuliaGraphics / Luxor.jl

Simple drawings using vector graphics; Cairo "for tourists!"
http://juliagraphics.github.io/Luxor.jl/
Other
585 stars 72 forks source link

Custom arrow heads and tails #159

Closed hustf closed 3 years ago

hustf commented 3 years ago

Ref #158.

Arrows are wonderfully powerful symbols. The brand new custom arrowhead approach is really flexible and elegant.

I was thinking that perhaps a natural extension to the custom head functionality is a custom vane / notch / fletching function?

As in, image

But on second thought, a 'fletching' function might need more (keyword?) parameters to be as useful as the custom head function. Because there's a edge case with short spines, where the vanes would start before the tip.

Below is the function that made those 'velocity arrows'.

function curved_point_arrow_with_vanes(p, Δv, endpoint, linewidth)
    arrowheadangle = pi/24

    isapprox(Δv, Point(0,0)) && throw(error("can't draw velocity arrow between two identical points"))
    shaftlength = hypot(Δv)
    arrowheadlength = shaftlength > 3 * EM ? EM :  shaftlength / 3

    shaftangle = atan(p.y - endpoint.y, p.x - endpoint.x)

    # shorten the length so that lines
    # stop before we get to the arrow
    # thus wide shafts won't stick out through the head of the arrow.
    max_undershoot = shaftlength - ((linewidth/2) / tan(arrowheadangle))
    ratio = max_undershoot / shaftlength
    tox = p.x + Δv.x * ratio
    toy = p.y + Δv.y * ratio
    fromx = p.x
    fromy = p.y

    # draw the shaft of the arrow
    newpath()
    line(Point(fromx, fromy), Point(tox, toy), :stroke)

    # draw the arrowhead
    arrowheadtopsideangle = shaftangle + arrowheadangle
    arrowheadtopsidemidangle = shaftangle + 0.67arrowheadangle
    topx = endpoint.x + cos(arrowheadtopsideangle) * arrowheadlength
    topy = endpoint.y + sin(arrowheadtopsideangle) * arrowheadlength
    topmidx = endpoint.x + cos(arrowheadtopsidemidangle) * arrowheadlength / 2
    topmidy = endpoint.y + sin(arrowheadtopsidemidangle) * arrowheadlength / 2

    arrowheadbottomsideangle = shaftangle - arrowheadangle
    arrowheadbottomsidemidangle = shaftangle - 0.67arrowheadangle
    botx = endpoint.x + cos(arrowheadbottomsideangle) * arrowheadlength
    boty = endpoint.y + sin(arrowheadbottomsideangle) * arrowheadlength
    botmidx = endpoint.x + cos(arrowheadbottomsidemidangle) * arrowheadlength / 2
    botmidy = endpoint.y + sin(arrowheadbottomsidemidangle) * arrowheadlength / 2

    poly([Point(topx, topy), Point(topmidx, topmidy), endpoint,
        Point(botmidx, botmidy), Point(botx, boty)], :stroke)

    # draw the rearmost vane
    vaneangle = shaftangle + π / 3
    vanelength = arrowheadlength / 2
    vanex = fromx + cos(vaneangle) * vanelength
    vaney = fromy + sin(vaneangle) * vanelength

    # draw the mid vane
    from1x = p.x + Δv.x * (1 - ratio)
    from1y = p.y + Δv.y * (1 - ratio)
    vane1x = from1x + cos(vaneangle) * vanelength
    vane1y = from1y + sin(vaneangle) * vanelength

    # draw the front vane
    from2x = p.x + Δv.x * 2 * (1 - ratio)
    from2y = p.y + Δv.y * 2 * (1 - ratio)
    vane2x = from2x + cos(vaneangle) * vanelength
    vane2y = from2y + sin(vaneangle) * vanelength

    poly([Point(vanex, vaney), Point(fromx, fromy), Point(from1x, from1y),
        Point(vane1x, vane1y), Point(from1x, from1y), Point(from2x, from2y),
        Point(vane2x, vane2y)] , :stroke)

end
cormullion commented 3 years ago

Perhaps the decorate keyword argument could be used/adapted for this?

cormullion commented 3 years ago

I think most of what you want can be achieved with the decorate functionality:

Screenshot 2021-07-05 at 15 00 27
using Luxor

function fletcher()
    line(O, polar(30, deg2rad(220)), :stroke)
    line(O, polar(30, deg2rad(140)), :stroke)
end

function hollowarrowhead(shaftendpoint, endpoint, shaftangle)
    @layer begin
        sidept1 = shaftendpoint  + polar(10, shaftangle + π/2 )
        sidept2 = shaftendpoint  - polar(10, shaftangle + π/2)
        poly([sidept1, endpoint, sidept2], :fill)
        poly([sidept1, endpoint, sidept2], :stroke, close=false)
    end
end

@drawsvg begin
    tiles = Tiler(600, 600, 2, 2)
    background("grey99")
    @layer begin
        translate(first(tiles[1]))
        sethue("orange")
        arrow(O, O + (150, 100), 
        linewidth=4,
        decorate=fletcher,
        arrowheadfunction = hollowarrowhead,
        decoration=range(0.1, .2, length=3))
    end

    @layer begin
        translate(first(tiles[2]))
        sethue("blue")
        arrow(O, 50, 0, π + π/3, 
        linewidth=4,
        decorate=fletcher,
        arrowheadfunction = hollowarrowhead,
        decoration=range(0.1, .3, length=3))
    end

    @layer begin
        translate(first(tiles[3]))
        sethue("purple")
        arrow(O, O + (150, 10), [15, 15], 
        linewidth=4,
        decorate=fletcher,
        arrowheadfunction = hollowarrowhead,
        decoration=range(0.1, .2, length=3))
    end

    @layer begin
        sethue("red")
        translate(first(tiles[4]))
        arrow(box(O, 100, 100, vertices=true)..., 
        linewidth=4,
        decorate=fletcher,
        arrowheadfunction = hollowarrowhead,
        decoration=range(0.1, .2, length=3))
    end    
end

I've fixed a few things in the code which weren't ideal.

hustf commented 3 years ago

Thanks for looking into this idea. Sorry for the delay in replying.

Something like the curved fletchings you demonstrated above could be an easily perceivable visualization of angular momentum.

Imagine similar arrows used in an animation of a distributed mass pendulum. Fletched arrows to represent angular momentum, unfletched to represent torque. The length of the arcs would cross zero during the animation. Fletchings would need scaling down when the arrow length is below a treshold. The treshold would depend on the length of spine and also the shape of decorations. The fletchings might fit best only on the outside of curvature.

So the interface soon becomes complicated, and users like yours truly always have the option of drawing their own wonderful superarrows.

As for passing more arguments and context to 'decorate' functions. That would be nice, but for backwards compatibility an argument-less callback would need to be acceptable. We could wish that something like the following would work (but it doesn't, and wouldn't be inferrable to the compiler).

function custom_luxor_arrow1(pt; linewidth = 1.0, decoratorfunc = (;kwa...) -> nothing, kw...)
       args = union(kw, :linewidth => linewidth)
       if applicable(decoratorfunc, args...)
           println("kwargs applicable")
           decoratorfunc(;args...)
       else
           decoratorfunc()
       end
end
cormullion commented 3 years ago

Many thanks for your insights and observations!

Is there anything specific to be done here…?

I think Luxor’s job is to provide basic tools to make simple drawings; for more complex work it should be possible to write your own custom functions, or else it will necessary to advance to more powerful systems such as TikZ or Asymptote. Complex interfaces tend to beneficial to fewer people…

hustf commented 3 years ago

I fully agree!