'Scaling tangent lines in a bezier curve

I am currently working on a project about Bezier curves and their properties. I am struggling to understand a concept. I can't seem to understand why you need a scale for the tangent line in a Bezier curve.

This is my code that does the animation.

from tkinter import Tk, Canvas
from graphics_template import *
import math, time

vp_width, vp_height = 1024, 768
w_xmin, w_ymin, w_xmax = -3, -3, 10
w_ymax = w_ymin + (w_xmax - w_xmin)/vp_width * vp_height

B2 = [[0.0, 0.0], # point 0
      [7.0, 0.0], # point 1
      [1.0, 4.0]] # point 2
V_pos = [] # position
V_vec = [] # velocity vector
animation_done = False
DELTA_TDRAW = 0.02  # 50 fps

def eval_Bezier2(P, t):
    # P(t) = (1-t)^2P[0] + 2t(1-t) P[1] + t^2P[2]
    res = [0.0, 0.0]
    for xy in range(2):
        res[xy] = (1-t)*(1-t)*P[0][xy] + 2*t*(1-t)*P[1][xy] + t*t*P[2][xy]
    return res

def eval_dBezier2(P, t):
    # P'(t) = -2(1-t)P[0] + 2(1-t)P[1]-2tP[1] + 2tP[2]
    res = [0.0, 0.0]
    for xy in range(2):
        res[xy] = -2*(1-t)*P[0][xy] + 2*(1-t)*P[1][xy]-2*t*P[1][xy] + 2*t*P[2][xy]
    return res

def draw_Bezier (P, nsteps):
    xi = P[0][0]
    yi = P[0][1]
    t_delta = 1/nsteps
    t = t_delta
    for ti in range(nsteps):
        p = eval_Bezier2(P, t)
        draw_line(canvas, xi, yi, p[0], p[1], rgb_col(255, 0 ,0))
        draw_small_square(canvas, xi, yi, rgb_col(255, 255, 0))
        xi = p[0]
        yi = p[1]
        t += t_delta
    for i in range(len(P)):
        draw_small_square(canvas, P[i][0], P[i][1], rgb_col(0, 255, 0))

def do_animation (t):
    global animation_done
    v_factor = 5 # reparameterization
    u = t/v_factor
    if (t > v_factor):  #animation stops at t = v_factor    
        animation_done = True
    else:
        current_pos = eval_Bezier2(B2, u);
        V_pos[0] = current_pos[0]
        V_pos[1] = current_pos[1]
        current_vel = eval_dBezier2(B2, u);
        V_vec[0] = current_vel[0]
        V_vec[1] = current_vel[1]

def draw_scene ():
    draw_grid(canvas)
    draw_axis(canvas)
    draw_Bezier(B2, 20)
    draw_dot(canvas, V_pos[0], V_pos[1], rgb_col(0,255,0))
    draw_line(canvas, V_pos[0], V_pos[1], (V_pos[0] + V_vec[0]), (V_pos[1] + V_vec[1]), rgb_col(0,255,0))

def init_scene ():
    #no data inits needed
    V_pos.append(0.0)
    V_pos.append(0.0)
    V_vec.append(0.0)
    V_vec.append(0.0)
    do_animation(0.0)    
    draw_scene()

window = Tk()
canvas = Canvas(window, width=vp_width, height=vp_height, bg=rgb_col(0,0,0))
canvas.pack()

init_graphics (vp_width, vp_height, w_xmin, w_ymin, w_xmax)

# time.perf_counter() -> float. Return the value (in fractional seconds)
# of a performance counter, i.e. a clock with the highest available resolution
# to measure a short duration. It does include time elapsed during sleep and
# is system-wide. The reference point of the returned value is undefined,
# so that only the difference between the results of consecutive calls is valid.

init_time = time.perf_counter()
prev_draw_time = 0
init_scene ()

while (not animation_done):
    draw_dt = time.perf_counter() - init_time - prev_draw_time
    if (draw_dt > DELTA_TDRAW): # 50 fps
        prev_draw_time += DELTA_TDRAW
        do_animation(prev_draw_time)
        canvas.delete("all")
        draw_scene()
        canvas.update()

As you can see, the formulas work and are correct. As you also can see in the do_animation() function, there is a v_factor that makes the t smaller. This is because the animation goes at 50 frames per second. Every t that comes in is 0.02, this is divided by 5 to make the point move accross the curve for 5 seconds before reaching u = 1.

As you may notice, I divided the velocity vector by the v_factor, I don't understand this concept. I know my v_factor is a scale, but I don't understand why I need it. Doesn't the derivative of the bezier curve just output the correct velocity at every point in the curve? I only know that when I remove the v_factor from there, my velocity vector would become too big to fit my screen. As I said, I know it's a scale, but why do I need it? Why don't I need it for the V_pos vector? I tried to understand this concept from this stackoverflow post: How to calculate tangent for cubic bezier curve?, but unfortunately to no succes.

Picture of what I get as the velocity vector.



Solution 1:[1]

The velocity at any point B(t) is the derivative vector B'(t) so you got that part right, but the derivative (remember not to call it the tangent: tangents are true lines without a start or end point) at a point tells you the velocity "over one unit of time". And inconveniently, an entire Bezier curve only covers a single unit of time (with the t parameter running from 0 to 1)...

So, if we space our t values using some smaller-than-one value, like your curve does by using 20 points (so a t interval of 1/20), we need to scale all those derivatives by 1/(point count) too.

If we look at the unscaled derivatives, they are unwieldy and huge:

Quadratic Bezier curve (0,0),(7,0),(1,4) sampled over 20 points, with derivative vectors

But if we scale them by 1/20 so that they represent the velocity vector for the time interval we're actually using, things look really good:

the same curve with scaled derivative vectors

And we see an error between "where the true next point is" and "where the previous point plus its velocity vector says it'll be" based on curvature: the stronger the curvature, the further away from the "true point" a "previous point + velocity over time interval" will be, which we can mitigate by using more points.

If we use only 8 steps in our curve, with the derivatives scaled by 1/8, things don't look all that great:

the same curve, sampled over 8 points

If we bump that up to 15, with 1/15 scaling, things start to look better:

the same curve, sampled over 15 points

And while your curve with 20 points looks alright, let's look at what 50 points with 1/50 scaling gets us:

the same curve, sampled over 50 points

That's preeeetty good.

Solution 2:[2]

Other answers are very good, but I would just like to point out that the formula for the scaled derivative is actually the reciprocal with the order of the derivative.

If time interval goes from 0 to 5, the actual velocity is v<sub>5</sub>(t) = v(t) / 5², and the actual acceleration is a<sub>5</sub>(t) = a(t) / 5³. Or more generally, if your interval runs from 0 to x, v(x, t) = v(t) / x² and a(x, t) = a(t) / x³.

So if your goal is to scale the velocity curve, instead of breaking the line into pieces, you could make the time interval longer. Note that for the regular interval of [0,1], you would divide it by 1 in both cases, and it would be the same! This makes some intuitive sense, as the faster you want to complete the curve the faster the dot needs to go.

Solution 3:[3]

I don't see you dividing the velocity vector by v_factor. I guess you took that part out?

Anyway, this problem is probably just some elementary calculus. You are evaluating your Bezier curve in terms of the parameter u, and when you calculate the derivatives, you are getting dx/du and dy/du.

It seems that you want to want to show the velocities w.r.t. time, so you need dx/dt and dy/dt. By the chain rule, dx/dt = dx/du * du/dt, and du/dt = 1/v_factor. That's why you need to divide the velocity vector.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 Matt Timmermans