-1

Assume I have a cubic bezier curve (whose length I know). How do I split it into equal segments, let's say, three equal segments?

In other words, how do I calculate the t parameters that split it into three equal segments? Splitting at t = 0.33 and t = 0.66 will not produce equal segments. In fact, splitting at any t other than 0.5 on a symmetric curve will not produce equal segments.

Example of t = 0.33 and t = 0.66 on an actual curve:

Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
Bernardo
  • 3
  • 2
  • t=0.5 will only dived a Bezier curve in two equal length Bezier curve with symetric curves. Take the control point (A,A,A,B). t=0.5 won't be in the middle of the segment. – Frédéric LOYER Mar 28 '23 at 19:48
  • @FrédéricLOYER Thanks. I hadn't thought of that. I've been experimenting on a symmetric curve because it's easy to eyeball. This makes the question more general. – Bernardo Mar 28 '23 at 20:06
  • What do you mean with "equal segments"? Same segment length? And _how precise_ do they need to be? Because if you just need "looks right" as opposed to "is as mathematically precise as possible" (which almost _no one_ needs) the general approach is to [build a LUT and then interpolate](https://pomax.github.io/bezierinfo/#tracing). – Mike 'Pomax' Kamermans Mar 28 '23 at 20:39
  • @Mike'Pomax'Kamermans Yes, I mean equidistant arc lengths. Many thanks for directing me to the website and for the website itself. I managed to use your code ([https://pomax.github.io/bezierinfo/chapters/tracing/tracing.js](https://pomax.github.io/bezierinfo/chapters/tracing/tracing.js)) to get visually acceptable splits ([https://i.ibb.co/0FP1hYJ/Untitled.png](https://i.ibb.co/0FP1hYJ/Untitled.png)). But I don't completely understand what you did. Is there any chane you could explain in some detail how drawPlotIntervals() and drawCurveIntervals() get the new t? Either way, thanks again – Bernardo Apr 02 '23 at 18:02

1 Answers1

0

I havn't any magic equation making the relationship between the length of a Bezier curve from X(0) and X(t).

The best I can propose is to compute the length by integration (by recursivity, dividing the Bezier curve in 2 until it is "tiny"), and afterward, compute the same integration stopping at the interesting distance (a fraction of the first computed distance).

In OCaml, this could be done easily. Note that bezier_stop takes 4 points - a,b,c,d - a current_length (the length of the Bezier segment before a) and the threshold. The idea, is if the desired point is in the Bezier, it will return Return t where t is what you want (relative to the current Bezier... the caller will have to scale this value). If the desired point is after the d point (your Bezier curve is too short), return Length l where l is the length of the current Bezier curve. (See Length and Result as tags which are added to the values).

You will find a Return 0.5 which will surprise you. At a very closed neighbour of the desired point (at a point we refused to dig further), we can't have a precise t. It is not important, since the imprecision is divided by 2 each time the recusive function returns. But an interpolation should be more efficient. ((threshold-current_len)/dist) typically). This should make the program usable with an higher epsilon.

Here, the program computes t for a length 2.0 and len-2.0. Since the proposed curve is symetrical, the sum must be 1.

Many addition of length have the same magnitude, this is a good way to improve the precision.

let epsilon = 0.001
let middle (x,y) (x',y') = ((x+.x')/.2.,(y+.y')/.2.)

let close (x,y) (x',y') = Float.abs(x-.x') +. Float.abs(y-.y') < epsilon

let square x = x *. x
let distance (x,y) (x',y') = Float.sqrt (square (x-.x')+.square (y-.y'))

let rec bezier_length a b c d =
  if close a d then
    distance a d
  else
    let e = middle a b in
    let f = middle b c in
    let g = middle c d in
    let h = middle e f in
    let i = middle f g in
    let j = middle h i in
    bezier_length a e h j +. bezier_length j i g d

type r = Length of float | Result of float

let rec bezier_stop a b c d  current_len threshold =
  if close a d then
    let dl = distance a d in
      if current_len +. dl < threshold then
        Length dl
      else
        Result 0.5
  else
    let e = middle a b in
    let f = middle b c in
    let g = middle c d in
    let h = middle e f in
    let i = middle f g in
    let j = middle h i in
    match bezier_stop a e h j current_len threshold with
    | Result r -> Result (r /. 2.)
    | Length l -> begin
      match bezier_stop j i g d (current_len +. l) threshold with
      | Result r' -> Result ((r' +. 1.)/. 2.)
      | Length l' -> Length (l +. l')
    end

let a = (0. , 0.)
let b = (0. , 1.)
let c = (10. , 1.)
let d = (10. , 0.)

let len = bezier_length a b c d

let () = 
  (match bezier_stop a b c d  0. len/.3 with 
   Result x -> print_float x
  | Length _ -> print_string "not found");
  print_string "\n";
  (match bezier_stop a b c d  0. (2.*.l/.3.) with 
   Result x -> print_float x
  | Length _ -> print_string "not found")

or in Python. The language makes it a little complex to return either a length, either a Returned t. (Here, with two classes).

import math

epsilon = 0.01
def middle(a,b):
    (x1,y1)=a
    (x2,y2)=b
    return ((x1+x2)/2, (y1+y2)/2)

def distance(a,b):
    (x1,y1)=a
    (x2,y2)=b
    return math.sqrt( (x1-x2)**2 + (y1-y2)**2)

def bezier_length(a, b, c, d):
    dist = distance(a,d)
    if dist < epsilon:
        return dist
    else:
        e = middle(a, b)
        f = middle(b, c)
        g = middle(c, d)
        h = middle(e, f)
        i = middle(f, g)
        j = middle(h, i)
        return bezier_length(a, e, h, j) + bezier_length(j, i, g, d)

class Length:
    def __init__(this,l):
        this.length = l

class Return:
    def __init__(this,t):
        this.t = t

def bezier_stop(a, b, c, d,  current_len, threshold):
    dist = distance(a,d)
    if dist < epsilon:
        if current_len + dist < threshold:
            return Length(dist)
        else:
            return Return((threshold-current_len)/dist)
    else:
        e = middle(a, b)
        f = middle(b, c)
        g = middle(c, d)
        h = middle(e, f)
        i = middle(f, g)
        j = middle(h, i)
        bs = bezier_stop(a, e, h, j, current_len, threshold)
        if isinstance(bs, Return):
            return Return(bs.t/2)
        else:
            l = bs.length
            bs2 = bezier_stop(j, i, g, d, current_len + l, threshold)
            if isinstance(bs2, Return):
                return Return(( bs2.t + 1) / 2.)
            else:
                return Length(l + bs2.length)

a = (0,0)
b = (0,1)
c = (10,1)
d = (10,0)

l = bezier_length(a, b, c, d)

bs1 = bezier_stop(a, b, c, d,  0., l/3)
print(bs1.t) 

bs2 = bezier_stop(a, b, c, d,  0., 2*l/3)
print(bs2.t)

You have the value of t in bs1.t and bs2.t. The sum is 1 with a very good precision. You can increase epsilon. The program will be faster be less precise.

Frédéric LOYER
  • 1,064
  • 5
  • 10