Spline curve and Bezier curve

Spline curve

The origin of spline curves

The origin of spline curves is a common problem, that is, how to obtain a smooth curve through a number of points given?

A simple and effective method is to treat these points as limiting points, and then place an elastic metal sheet in the limiting points. The final state of the metal sheet after bypassing these points is the desired curve. The resulting shape curve is a spline. That’s where the name comes from, where the sheet metal is a spline, and the curve it forms is a spline curve. Figure below (interpolation spline curve)

Classification of spline curves

  • Interpolate curve

Interpolating spline curves generates spline curves through this set of control points

  • Approximate spline curve

Approximate spline curve surface spline curve does not pass through or through part of the control point.

Bessel curve

The origin of Bessel curves

Bezier curve, also known as Bezier curve or Bezier curve, is a mathematical curve used in two-dimensional graphics applications.

The mathematical basis of Bessel curves is known as the Bernstein polynomials as early as 1912.

But it was not until 1959 that Paul de Casteljau, a French mathematician then working for Citroen, began to attempt to apply it graphically and proposed a numerically stable de Casteljau algorithm.

The Bezier curve, however, got its name from the publicity of another French engineer, Pierre Bezier, who was working at Renault in 1962.

It uses this method of generating complex and smooth curves with very few control points to assist in the industrial design of the car body, and we can see this beautiful picture of the car model:

Bezier curve

Bessel curve has been widely used in the field of industrial design because of its simple control but strong descriptive ability.

Bessel curves also play an important role in the field of computer graphics, especially vector graphics. Some of our most common vector drawing software today, such as Flash, Illustrator, CorelDraw, etc., all provide Bessel curve drawing capability without exception. Even bitmap editing software like Photoshop includes bezier curves as the only vector drawing tool (the pen tool).

Bessel curves also have a place in web development. Many JavaScript animations, and even CSS3’s new transition-timing-function property, can be set to a cubic Bezier equation.

De Casteljau algorithm

Choose 3 non-collinear points in the plane and connect them with line segments in turn

So let’s pick any point D on the first segment. Calculate the ratio of the distance AD from the point to the beginning of the line segment to the total length AB of the line segment

Based on the ratio obtained in the previous step, find the corresponding point E on the second line segment such that AD/AB = BE/BC

Connect the two points DE

From the new line segment DE find the same proportion of points F again, such that DF/DE = AD/AB = BE/BC

At this point, we have identified a point F on the Bezier curve

Next, let the selected point D move from the starting point A to the ending point B on the first segment, and find all the points F on the Bezier curve

After all the points are found, we also have this Bezier curve

If you can’t imagine the process, watch the animation

Looking back at the Bezier curve, in order to identify a point on the curve, we need to do two rounds of picking points, so we call the resulting Bezier curve a quadratic

What happens when we have four control points?

The procedure is the same, except that for every point on the Bezier curve, there are three rounds of picking

As shown, AE/AB = BF/BC = CG/CD = EH/EF = FI/FG = HJ/HI

The point J is a point on the Bessel curve that you end up with

So what we get is a cubic Bezier curve

Three times Bezier draws the animation

From the illustration above, we can see that a cubic Bezier curve requires four coordinate points

1. Two starting and ending coordinates, which are also two control points

2. Coordinates of the other two control points

Draw a spline curve

Draw a spline curve

We use HTML5 canvas to draw

Canvas’s function bezierCurveTo (cubic Bezier curve) can join two points by smoothing the curve, A cubic Bezier curve is specified by its endpoints (usually called knots) and two control points. But Bezier curves don’t interpolate their control points (they don’t go through them), so we can’t just pass the list of endpoints to Bezier CurveTo (), we have to figure out where those control points are.

Splines must go through each node and be joined “smoothly” at the node, that is, have the same slope (first derivative) at the node, but need not have the same curvature (second derivative). At the same time, we also want to adjust the smoothness of the curve

Figure 1 shows our goal: If we can create two Bessel curves (shown in red and orange) that start and end at points X0 and X2 and join smoothly at points X1, then we can repeat the process and splice together any number of nodes.

First, in order to achieve smooth connections, the control points on either side of the node must be on a straight line through the node

Second, the line should be parallel to the line on both sides of the connecting node.

As shown in Figure 2, our control point will be located somewhere on line L1, which is parallel to line L02.

For clarity, Figure 3 shows more things we need: First, the distance between nodes D01 and D12. These are all easy to compute from the node (x, y) coordinates, which are our input data. We also need right triangle T, which joins x0 and x2 and has width W and height H, which can also be easily calculated from node coordinates.

The final step is to draw two smaller similar triangles of T (same Angle and direction) with their long sides along L1 (parallel) and joined at x1.

These smaller similar triangles Ta and Tb are reduced from T by two factors: first, the distances D01 and D12, respectively,

The second is the constant parameter t.

Scaling in proportion to the distance between nodes is a simple way to get proper curvature: if two nodes are very close together, the control points between them should also be close to the nodes, and the resulting curve will be steep. The scaling of T gives us the width and height of Ta and Tb, from which we can easily obtain the coordinates of control points P1 and P2.

The Javascript corresponding to the procedure:

function getControlPoints(x0,y0,x1,y1,x2,y2,t){
    var d01=Math.sqrt(Math.pow(x1-x0,2) +Math.pow(y1-y0,2));
    var d12=Math.sqrt(Math.pow(x2-x1,2) +Math.pow(y2-y1,2));
    var fa=t*d01/(d01+d12);   // scaling factor for triangle Ta
    var fb=t*d12/(d01+d12);   // ditto for Tb, simplifies to fb=t-fa
    var p1x=x1-fa*(x2-x0);    // x2-x0 is the width of triangle T
    var p1y=y1-fa*(y2-y0);    // y2-y0 is the height of T
    var p2x=x1+fb*(x2-x0);
    var p2y=y1+fb*(y2-y0);  
    return [p1x,p1y,p2x,p2y];
}
Copy the code

In these sketches, we find two control points, but for different Bezier curves: control point P1 (Figure 4) is needed to plot the left Bezier curve (red in Figures 1 and 2), and control point P2 to plot the right Bezier curve (orange).

This only means that we must calculate all the control points before drawing

After drawing all the control points, we can combine the original nodes to draw the sample curve. However, we should pay attention to the following rules:

1. Bezier curves for the start and end segments (non-closed curves) (yellow rectangle curves) need to be drawn with quadratic Bezier curves (because there is only one control point) (yellow circle control points)

2. The rest of the curve segment needs to be plotted using cubic Bezier curves (e.g. the blue curve), because two control points can be found (the blue circle control points).

Drawing sample Web sites

The sample url: scaledinnovation.com/analytics/s…

In the demo, when t=0, the curve becomes a straight line connecting the nodes, and when t=1, the curve is “too curved” for an open zigzag curve

There is no upper bound on t, but above t=1, you are bound to get irrelevant points and rings. T could also be negative, and that would draw some rings.

Dynamic mapping

Dynamic mapping

The key to dynamic drawing is that we need to figure out the coordinates of the points on the bezier curve that we draw that are not nodes

To find the point on the Bezier curve, we can use the formula for the Bezier curve

Quadratic Bessel curve (quadratic formula)

Cubic Bezier curve (cubic formula)

General parameter curve

Equation corresponding code implementation

// t represents the progress from 0 to 1 between the start point and the end point, such as the point between p0 and p3

// Cubic Bezier curve equation
const CalculateBezierPointForCubic = (t, p0, p1, p2, p3) = > {
    var point = {x:0.y:0};
    var k = 1 - t;
    point.x = p0.x * k * k * k + 3 * p1.x * t * k * k + 3 * p2.x * t * t * k + p3.x * t * t * t;
    point.y = p0.y * k * k * k + 3 * p1.y * t * k * k + 3 * p2.y * t * t * k + p3.y * t * t * t;
    return point;
}

// Quadratic Bezier curve equation
const quadraticBezier = (t, p0, p1, p2) = > {
  var point = {x:0.y:0}
  var k = 1 - t;
  point.x = k * k * p0.x + 2 * (1 - t) * t * p1.x + t * t * p2.x;
  point.y = k * k * p0.y + 2 * (1 - t) * t * p1.y + t * t * p2.y;
  return point
}
Copy the code

Dynamic rendering code implementation

/ / HTML
<canvas id="smooth_line" height="500" width="1000"></canvas>

// the js implementation part
const canvasHeight = 500
const canvasWidth = 1000

// Calculate two control points according to the starting point, middle pass point and end point
function getControlPoints(x0,y0,x1,y1,x2,y2,t){
    var d01=Math.sqrt(Math.pow(x1-x0,2) +Math.pow(y1-y0,2));
    var d12=Math.sqrt(Math.pow(x2-x1,2) +Math.pow(y2-y1,2));
    var fa=t*d01/(d01+d12);   // scaling factor for triangle Ta
    var fb=t*d12/(d01+d12);   // ditto for Tb, simplifies to fb=t-fa
    var p1x=x1-fa*(x2-x0);    // x2-x0 is the width of triangle T
    var p1y=y1-fa*(y2-y0);    // y2-y0 is the height of T
    var p2x=x1+fb*(x2-x0);
    var p2y=y1+fb*(y2-y0);  
    return [p1x,p1y,p2x,p2y];
}
// Calculate the control points corresponding to a group of points
function createControlPoints(points, t=0.5) {
    let controlPoints = []
    for(let i=0; i<points.length-2; i++) {const controlPoint = getControlPoints(points[i].x, points[i].y, points[i+1].x, points[i+1].y, points[i+2].x, points[i+2].y, t)
      controlPoints.push({x:controlPoint[0].y:controlPoint[1] {},x:controlPoint[2].y:controlPoint[3]})}return controlPoints
 }
 
// Cubic Bezier curve equation
const CalculateBezierPointForCubic = (t, p0, p1, p2, p3) = > {
    var point = {x:0.y:0};
    var k = 1 - t;
    point.x = p0.x * k * k * k + 3 * p1.x * t * k * k + 3 * p2.x * t * t * k + p3.x * t * t * t;
    point.y = p0.y * k * k * k + 3 * p1.y * t * k * k + 3 * p2.y * t * t * k + p3.y * t * t * t;
    return point;
}
  // Quadratic Bezier curve equation
const quadraticBezier = (t, p0, p1, p2) = > {
    var point = {x:0.y:0}
    var k = 1 - t;
    point.x = k * k * p0.x + 2 * (1 - t) * t * p1.x + t * t * p2.x;
    point.y = k * k * p0.y + 2 * (1 - t) * t * p1.y + t * t * p2.y;
    return point
}
  
let smoothCanvas = document.querySelector('#smooth_line')
let smoothCanvasContext2D = smoothCanvas.getContext('2d')

const cleanCanvas = () = > {
  smoothCanvas = document.querySelector('#smooth_line')
  smoothCanvasContext2D = smoothCanvas.getContext('2d')
  smoothCanvasContext2D.clearRect(0.0, canvasWidth, canvasHeight);
  smoothCanvasContext2D.strokeStyle = '#0cc'
  smoothCanvasContext2D.strokeRect(0.0, canvasWidth, canvasHeight)
}

// Draw a spline curve based on a given set of points and smoothness
const drawPointsSmoothLine = (ctx, points, t=0.5) = > {
    ctx.beginPath()
    let controlPoints = createControlPoints(points, t)
    for(let i=0; i<points.length-1; i++) {if(i===0) {
        ctx.save()
        ctx.beginPath()
        ctx.moveTo(points[i].x, points[i].y)
        ctx.quadraticCurveTo(controlPoints[0].x, controlPoints[0].y, points[i+1].x, points[i+1].y)
        ctx.stroke();
        ctx.closePath()
        ctx.restore()
      } else if(i===points.length - 2) {
        ctx.save()
        ctx.beginPath()
        ctx.moveTo(points[i].x, points[i].y)
        ctx.quadraticCurveTo(controlPoints[controlPoints.length -1].x, controlPoints[controlPoints.length -1].y, points[i+1].x, points[i+1].y)
        ctx.stroke();
        ctx.closePath()
        ctx.restore()
      } else {
        ctx.save()
        ctx.lineTo(points[i].x, points[i].y)
        ctx.beginPath()
        ctx.moveTo(points[i].x, points[i].y)
        ctx.bezierCurveTo(controlPoints[2*(i-1) + 1].x, controlPoints[2*(i-1) + 1].y, controlPoints[2*(i-1) + 2].x, controlPoints[2*(i-1) + 2].y, points[i+1].x, points[i+1].y)
        ctx.stroke();
        ctx.closePath()
        ctx.restore()
      }
    }
  }
  
  
// Draw a point
const drawPoint = (ctx, {x,y}, radius, color='#0cc') = > {
  ctx.save()
  ctx.beginPath()
  ctx.fillStyle = color
  ctx.arc(x, y, radius, 0.2 * Math.PI);
  ctx.closePath();
  ctx.fill();
  ctx.restore()
}

// When you need to stop drawing externally
let animationId = 0

// staticData is the data passed in. Here are some examples of the data
const staticData = [
  {
    "x": 10."y": 41.02601317115307
  }, {
    "x": 60."y": 367.53590353849495
  }, {
    "x": 110."y": 55.44857431904126
  }, {
    "x": 160."y": 228.40188926709214
  }, {
    "x": 210."y": 243.49719756836208
  }, {
    "x": 260."y": 31.12232864622555
  }, {
    "x": 310."y": 312.980045442814
  }, {
    "x": 360."y": 380.36533757874724
  }, {
    "x": 410."y": 153.25910899820738
  }, {
    "x": 460."y": 370.25905804521585}]let originPoints = staticData
// Draw the curve smoothness (0 to 1)
const smoothDegree = 0.5

let startTime

// The percentage of the current time between two points
let nowT = 0

// The current start point
let nowIndex = 0

// Indicates that the interval between the two points is 1000 ms
let perPointTime = 1000

const draw = (timestamp) = > {
    if (startTime === undefined) {
      // Initialize the assignment the first time you enter the draw function
      startTime = timestamp;
    }
    
    // The elapsed time since the drawing started
    const elapsed = timestamp - startTime;
    
    // It is between intNum and intNum+1
    let intNum = Math.floor(elapsed / perPointTime)
    
    // The current percentage between the intNum point and the intNum+1 point (keep two decimal places)
    let otherNum = Math.floor((elapsed / perPointTime - Math.floor(elapsed / perPointTime)) * 100) /100
    
    nowIndex = intNum
    nowT = otherNum
    
    if(intNum === originPoints.length - 1) {
      // Stop drawing when you reach the last point
      return
    }
    
    // Clear the previous draw and draw borders
    cleanCanvas()
    
    let points = originPoints.map(item= > { return { x: item.x, y:item.y} })
    // Calculate the array of control points
    let controlPoints = createControlPoints(originPoints).map(item= > { return { x: item.x, y:item.y } })
    
    // The point corresponding to the current time
    let NowPoint = {x:0.y:0}
    
    // Calculate the x and y coordinates of the current point in time,
    if(nowIndex === 0) {
      // The first section of the curve is drawn using quadratic Bezier curves, so use quadratic Bezier curve equation
      NowPoint = quadraticBezier(nowT, points[nowIndex], controlPoints[0], points[nowIndex + 1])}else if(nowIndex === points.length - 2) {
      // The last part of the curve is drawn using quadratic Bezier curves, so use quadratic Bezier curve equation
      NowPoint = quadraticBezier(nowT, points[nowIndex], controlPoints[controlPoints.length -1], points[nowIndex+1])}else {
      // Other sections of the curve are drawn using cubic Bezier curves, so use cubic Bezier curve equations
      NowPoint = CalculateBezierPointForCubic(nowT, points[nowIndex], controlPoints[2*(nowIndex-1) +1], controlPoints[2*(nowIndex-1) +2], points[nowIndex + 1])}// Draw control points and control lines
    // 
    //   for(let i=0;i<controlPoints.length;i=i+2) {
    // drawPoint(smoothCanvasContext2D, controlPoints[i], 2, 'black')
    // drawPoint(smoothCanvasContext2D, controlPoints[i+1], 2, 'black')
    // smoothCanvasContext2D.save()
    // smoothCanvasContext2D.beginPath()
    // smoothCanvasContext2D.moveTo(controlPoints[i].x, controlPoints[i].y)
    // smoothCanvasContext2D.lineTo(controlPoints[i+1].x, controlPoints[i+1].y)
    / / smoothCanvasContext2D strokeStyle = 'rgba (0,0,0,0.5)'
    // smoothCanvasContext2D.stroke()
    // smoothCanvasContext2D.closePath()
    // smoothCanvasContext2D.restore()
    / /}
    
    // The percentage of distance between the current point and the last point
    const nowLastLinePercent = (NowPoint.x - points[0].x)/(points[points.length - 1].x - points[0].x)
    
    smoothCanvasContext2D.save()
    // Create a linear gradient with the fill style set before nowLastLinePercent and transparent after nowLastLinePercent
    const gradient = smoothCanvasContext2D.createLinearGradient(points[0].x,0, points[points.length - 1].x,0);
    gradient.addColorStop(0, smoothCanvasContext2D.strokeStyle);
    gradient.addColorStop(nowLastLinePercent, smoothCanvasContext2D.strokeStyle);
    gradient.addColorStop(nowLastLinePercent,"white");
    gradient.addColorStop(1."white");
    
    // Set the fill style
    smoothCanvasContext2D.strokeStyle = gradient
    
    // Draw a curve
    drawPointsSmoothLine(smoothCanvasContext2D, points, smoothDegree)
    
    // Draw all the nodes
    // for(let i=0; i
    // drawPoint(smoothCanvasContext2D, points[i], 2, 'red')
    // }
    
    // Draw the current point
    // drawPoint(smoothCanvasContext2D, NowPoint, 2, 'red')
    
    animationId = window.requestAnimationFrame(draw);
}

animationId = window.requestAnimationFrame(draw);
Copy the code

The essence of the code above is to draw a spline by calculating the position of the point on the curve at the current time, generating a gradient based on that position, making the gradient behind the current point transparent, and then using that gradient as a fill style.

This logic is then applied to the requestAnimationFrame to continuously compute new points and redraw to achieve the desired dynamic rendering.

Of course, we can also achieve the effect of dynamically adding nodes and displaying spline curves in real time by translating and managing all corresponding points according to time, which will not be discussed here.

The resources

# Detail spline curves (top) (including Bezier curves)

Canvas draws a curved path bezierCurveTo

# An introduction to Bezier curves

# Spline Interpolation

# # Spline Interpolation DEMO

Canvas draws a curved path bezierCurveTo

# Baike Bessel curve