Need Help to Create Animation for Tangent Line with Luxor

Hi all,

I want to understand more of tangent line / gradient line then read the wikipedia and saw the animation here for the tangent line:

I create the plot (static image):

using Plots,  LaTeXStrings
gr()

f(x) =  -x^2 + 2x + 2
f'

# gradient_line(f, x₀) is a tangent line with an intercept at point x₀ 
# in other words it will pass x₀ 
# and a slope of f'(x₀) - essentially your usual y = a + bx 
# where a is the value of the actual function at the point x₀ through 
# which we want the line to pass, 
# b is the derivative of the function at that point, 
# and the "x" in y=a+bx is replaced by x-x₀ 
# because our "origin" is the point at which we're taking the tangent. 

gradient_line(f, x₀) = (x -> f(x₀) + f'(x₀)*(x-x₀))

default(markerstrokecolor = "white", linewidth = 2);

#Plot f
plot(f, -3:0.1:5, label = "f(x) = -x² + 2x + 2", xlabel = "x", ylabel = "f(x)");

# Plot tangent lines
scatter!([-1], [f(-1)], label = "", markersize = 5);
plot!(gradient_line(f, -1), -3:0.1:0.5, label = "f'(-1)", color = 2);

scatter!([1/2], [f(1/2)], label = "", markersize = 5, color = 3);
plot!(gradient_line(f, 1/2), -1:0.1:2, label = "f'(1/2)", color = 3)

scatter!([2], [f(2)], label = "", markersize = 5, color = 4);
plot!(gradient_line(f, 2), 0.2:0.1:3, label = "f'(2)", color = 4)

scatter!([3], [f(3)], label = "", markersize = 5, color = 5);
plot!(gradient_line(f, 3), 0.8:0.1:4, label = "f'(3)", color = 5)

Now, I want to create the moving tangent lines animation, but there is an error, which I have no idea why it said missing comma or ), it works at the above codes…

using Plots,  LaTeXStrings
using Luxor
gr()

let
    f(x) = x*(sin(x^(2))+1
    f'
    gradient_line(f, x₀) = (x -> f(x₀) + f'(x₀)*(x-x₀))
    default(markerstrokecolor = "white", linewidth = 2);
    w, h = 600, 400
    i = 1
    
    plot(f, -3:0.1:5, label = L"f(x) = x \ * sin(x^{2}) + 1", 
	 xlabel = "x", ylabel = "f(x)");

    function frame(scene, i)
        
        background("white"); sethue("green")

        # translate near bottom right corner
        # scatter([i], [f(i)], label = "", markersize = 5);
        plot(gradient_line(f, i), -3:0.1:0.5, label = "", color = 2);
        circle_marker_pos = getworldposition(Point([i], f[i]))
	
	for k = 1:0.1:3
		setfont("Georgia Bold", 73)
		sethue("black")
		text(string("x = $k"),
		Point(O.x-83, O.y-108),
		halign=:center)
	end

        i += 0.1

    end
    demo = Movie(600, 400, "needforspeedtangent")
    animate(demo, [Scene(demo, frame, 1:360)], creategif=true)
end

LoadError: syntax: missing comma or ) in argument list
Stacktrace:
** [1] top-level scope**
** @ ~/LasthrimProjection/plot.jl:7**
** [2] include(fname::String)**
** @ Base.MainInclude ./client.jl:451**
** [3] top-level scope**
** @ REPL[1]:1**
in expression starting at /home/browni/LasthrimProjection/plot.jl:7

There is a missing comma or ) here.

1 Like

Plots.plot() and frame() won’t draw onto the same canvas. I’ve never tried to use Plots and Luxor at the same time - I don’t think it’s possible, but I’m happy if I’m wrong… :slight_smile:

I forgot this, but remember again, thus I can’t plot the graph easily then.

Here’s a quick version in Luxor.jl of f(x) = sin(2x) * cos(x/2), using Zygote and code from @nilshg’s StackOverflow answer.

slope

Julia code
using Luxor
using Zygote

## mathspace to drawingspace
K = 400/2 / 2π
Pt(x, y) = Point(K * x, -K * y)

f(x) = sin(2x) * cos(x/2)

gradient_line(f, x₀) = (x -> f(x₀) + f'(x₀) * (x - x₀))

function drawcurve(w, h)
    @layer begin
        setopacity(0.25)
        rule(O)
        rule(O, π/2)
    end
    move(Pt(-2π, 0))
    for x in -2π:π/24:2π
        line(Pt(x, f(x)))
    end
    strokepath()
end

function frame(scene, framenumber)
    w, h = scene.movie.width, scene.movie.height
    background("black")
    sethue("gold")
    drawcurve(w, h)
    x = rescale(framenumber, 1, scene.framerange.stop, -2π, 2π)
    pt = Pt(x, f(x))
    circle(pt, 5, :fill)
    δ = 0.1
    y = gradient_line(f, x)(x)
    point_on_curve = Pt(x, y)

    y1 = gradient_line(f, x)(x - δ)
    y2 = gradient_line(f, x)(x + δ)

    sl = slope(Pt(x - δ, y1), Pt(x + δ, y2))
    sethue("white")
    line(point_on_curve - polar(100, sl), 
         point_on_curve + polar(100, sl), :stroke)
end

demo = Movie(400, 300, "slope")
animate(demo, [Scene(demo, frame, 1:100)], framerate=10, creategif=true)

This isn’t to say that this is the best or only way to write it - it’s probably easier in Plots or Makie or Javis, although I’m not familiar enough with those packages to know for sure.

Although most Julia packages usually work well together, I’d recommend using only one of the “graphical output” packages in a session. The least of the problems will be having multiple definitions for common functions; accessing the output ‘channels’ of other modules could require some advanced knowledge of their internals.

6 Likes

That’s really cool! I like how Luxor takes care of keeping the length of the tangent line constant for us. This is what comes to mind for trying something similar in Makie (just manually applying Luxor.slope)

test

Julia code
using CairoMakie, Zygote

f(x) = sin(2x) * cos(x/2)
gradient_line(f, x₀) = (x -> f(x₀) + f'(x₀) * (x - x₀))

with_theme(theme_dark()) do
    fig = Figure()
    ax = Axis(fig[1, 1]; limits=(-2π, 2π, -3, 3))

    # Static plot setup
    x = -2π:π/24:2π
    lines!(ax, x, f)
    r_line = 1

    # Animated point setup
    x_point = Observable(x[begin])
    y_point = @lift f($x_point)

    # Animated line setup
    x_line = @lift begin
        m_line = f'($x_point)
        x_lim = r_line * cos(mod2pi(atan(m_line)))
        range($x_point - x_lim, $x_point + x_lim, 100)
    end
    y_line = @lift gradient_line(f, $x_point).($x_line)

    # Plot line and point
    lines!(ax, x_line, y_line; color=:goldenrod)
    scatter!(ax, x_point, y_point; color=:goldenrod)

    # Animate
    record(fig, "test.gif", x; framerate=15) do xi
        x_point[] = xi
    end
end
2 Likes

I was trying to make a code works for the old conventional Plots, because I think Luxor is way of my league now… thanks again @cormullion

But I got errors trying Makie’ code of yours (using IJulia):

Makie.convert_arguments for the plot type Scatter{Tuple{Float64, Float64}} and its conversion trait Makie.PointBased() was unsuccessful.

The signature that could not be converted was:
::Float64, ::Float64

Makie needs to convert all plot input arguments to types that can be consumed by the backends (typically Arrays with Float32 elements).
You can define a method for Makie.convert_arguments (a type recipe) for these types or their supertypes to make this set of arguments convertible (See http://makie.juliaplots.org/stable/recipes.html).

Alternatively, you can define Makie.convert_single_argument for single arguments which have types that are unknown to Makie but which can be converted to known types and fed back to the conversion pipeline.

Could it be that you are using an older version of Makie? scatter should be able to handle Tuple{Float64, Float64} just fine I believe, e.g., scatter((1., 2.)). This is my current environment:

  [13f3f980] CairoMakie v0.8.13
  [e88e6eb3] Zygote v0.6.49

on Makie v0.17.13