How to use Javascript to generate smooth curves

How to use Javascript to generate smooth curves

Preface

image.png

Smooth curve generation is a very practical technology

Often, we need to draw some polylines and then let the computer connect them smoothly.

Let's take a look at the final effect first (the red line is the straight line we input, and the blue line is the curve after fitting). The beginning and the end can be specially processed to make the graph look better:)

Jul-09-2021 15-28-04.gif

The idea is to use Bezier curve for fitting

Introduction to Bezier curves

The Bézier curve is a very important parametric curve in computer graphics.

Quadratic Bezier curve

240px-Bézier_2_big.gif

The path of a quadratic Bezier curve is traced by the function B(t) given the points P0, P1, P2:

image.png

Cubic Bezier curve

240px-Bézier_3_big.gif

For a cubic curve, it can be constructed by the intermediate points Q0, Q1, Q2 described by the linear Bezier curve and the points R0 and R1 described by the quadratic curve.

image.png

Bezier curve calculation function

According to the above formula, we can get the calculation function

Second order

  /**
   *
   *
   * @param {number} p0
   * @param {number} p1
   * @param {number} p2
   * @param {number} t
   * @return {*}
   * @memberof Path
   */
  bezier2P(p0: number, p1: number, p2: number, t: number) {
    const P0 = p0 * Math.pow(1 - t, 2);
    const P1 = p1 * 2 * t * (1 - t);
    const P2 = p2 * t * t;
    return P0 + P1 + P2;
  }
  
    /**
   *
   *
   * @param {Point} p0
   * @param {Point} p1
   * @param {Point} p2
   * @param {number} num
   * @param {number} tick
   * @return {*} {Point}
   * @memberof Path
   */
  getBezierNowPoint2P(
      p0: Point,
      p1: Point,
      p2: Point,
      num: number,
      tick: number,
  ): Point {
    return {
      x: this.bezier2P(p0.x, p1.x, p2.x, num * tick),
      y: this.bezier2P(p0.y, p1.y, p2.y, num * tick),
    };
  }
  
    /**
   * Generate quadratic Bezier curve vertex data*
   * @param {Point} p0
   * @param {Point} p1
   * @param {Point} p2
   * @param {number} [num=100]
   * @param {number} [tick=1]
   * @return {*}
   * @memberof Path
   */
  create2PBezier(
      p0: Point,
      p1: Point,
      p2: Point,
      num: number = 100,
      tick: number = 1,
  ) {
    const t = tick / (num - 1);
    const points = [];
    for (let i = 0; i < num; i++) {
      const point = this.getBezierNowPoint2P(p0, p1, p2, i, t);
      points.push({x: point.x, y: point.y});
    }
    return points;
  }

Third level

/**
   * Cubic Searl Curve Formula*
   * @param {number} p0
   * @param {number} p1
   * @param {number} p2
   * @param {number} p3
   * @param {number} t
   * @return {*}
   * @memberof Path
   */
  bezier3P(p0: number, p1: number, p2: number, p3: number, t: number) {
    const P0 = p0 * Math.pow(1 - t, 3);
    const P1 = 3 * p1 * t * Math.pow(1 - t, 2);
    const P2 = 3 * p2 * Math.pow(t, 2) * (1 - t);
    const P3 = p3 * Math.pow(t, 3);
    return P0 + P1 + P2 + P3;
  }
  
    /**
   * Get coordinates *
   * @param {Point} p0
   * @param {Point} p1
   * @param {Point} p2
   * @param {Point} p3
   * @param {number} num
   * @param {number} tick
   * @return {*}
   * @memberof Path
   */
  getBezierNowPoint3P(
      p0: Point,
      p1: Point,
      p2: Point,
      p3: Point,
      num: number,
      tick: number,
  ) {
    return {
      x: this.bezier3P(p0.x, p1.x, p2.x, p3.x, num * tick),
      y: this.bezier3P(p0.y, p1.y, p2.y, p3.y, num * tick),
    };
  }
  
    /**
   * Generate cubic Bezier curve vertex data*
   * @param {Point} p0 starting point {x: number, y: number}
   * @param {Point} p1 control point 1 { x : number, y : number}
   * @param {Point} p2 control point 2 { x : number, y : number}
   * @param {Point} p3 end point {x: number, y: number}
   * @param {number} [num=100]
   * @param {number} [tick=1]
   * @return {Point[]}
   * @memberof Path
   */
  create3PBezier(
      p0: Point,
      p1: Point,
      p2: Point,
      p3: Point,
      num: number = 100,
      tick: number = 1,
  ) {
    const pointMum = num;
    const _tick = tick;
    const t = _tick / (pointMum - 1);
    const points = [];
    for (let i = 0; i < pointMum; i++) {
      const point = this.getBezierNowPoint3P(p0, p1, p2, p3, i, t);
      points.push({x: point.x, y: point.y});
    }
    return points;
  }

Fitting algorithm

image.png

The problem is how to get the control points. We use a relatively simple method

Take the angle bisector c1c2 of p1-pt-p2, which is perpendicular to the angle bisector c2. Take the short side as the length of c1-pt c2-pt. Scale the length. This length can be roughly understood as the curvature of the curve.

image.png

The ab line segment is simply processed here and only uses the second-order curve generation-> 🌈 You can process it according to your personal ideas

The bc line segment uses the control point c2 calculated by abc and the control point c3 calculated by bcd, and so on.

  /**
   * Control points needed to generate a smooth curve *
   * @param {Vector2D} p1
   * @param {Vector2D} pt
   * @param {Vector2D} p2
   * @param {number} [ratio=0.3]
   * @return {*}
   * @memberof Path
   */
  createSmoothLineControlPoint(
      p1: Vector2D,
      pt: Vector2D,
      p2: Vector2D,
      ratio: number = 0.3,
  ) {
    const vec1T: Vector2D = vector2dMinus(p1, pt);
    const vecT2: Vector2D = vector2dMinus(p1, pt);
    const len1: number = vec1T.length;
    const len2: number = vecT2.length;
    const v: number = len1 / len2;
    let delta;
    if (v > 1) {
      delta = vector2dMinus(
          p1,
          vector2dPlus(pt, vector2dMinus(p2, pt).scale(1 / v)),
      );
    } else {
      delta = vector2dMinus(
          vector2dPlus(pt, vector2dMinus(p1, pt).scale(v)),
          p2,
      );
    }
    delta = delta.scale(ratio);
    const control1: Point = {
      x: vector2dPlus(pt, delta).x,
      y: vector2dPlus(pt, delta).y,
    };
    const control2: Point = {
      x: vector2dMinus(pt, delta).x,
      y: vector2dMinus(pt, delta).y,
    };
    return {control1, control2};
  }
  
    /**
   * Smooth curve generation *
   * @param {Point[]} points
   * @param {number} ratio
   * @return {*}
   * @memberof Path
   */
  createSmoothLine(points: Point[], ratio: number = 0.3) {
    const len ​​= points.length;
    let resultPoints = [];
    const controlPoints = [];
    if (len < 3) return;
    for (let i = 0; i < len - 2; i++) {
      const {control1, control2} = this.createSmoothLineControlPoint(
          new Vector2D(points[i].x, points[i].y),
          new Vector2D(points[i + 1].x, points[i + 1].y),
          new Vector2D(points[i + 2].x, points[i + 2].y),
          ratio,
      );
      controlPoints.push(control1);
      controlPoints.push(control2);
      let points1;
      let points2;

      // The first control point only uses one if (i === 0) {
        points1 = this.create2PBezier(points[i], control1, points[i + 1], 50);
      } else {
        console.log(controlPoints);
        points1 = this.create3PBezier(
            points[i],
            controlPoints[2 * i - 1],
            control1,
            points[i + 1],
            50,
        );
      }
      // Tail part if (i + 2 === len - 1) {
        points2 = this.create2PBezier(
            points[i + 1],
            control2,
            points[i + 2],
            50,
        );
      }

      if (i + 2 === len - 1) {
        resultPoints = [...resultPoints, ...points1, ...points2];
      } else {
        resultPoints = [...resultPoints, ...points1];
      }
    }
    return resultPoints;
  }

Example code

    const input = [
        { x: 0, y: 0 },
        { x: 150, y: 150 },
        { x: 300, y: 0 },
        { x: 400, y: 150 },
        { x: 500, y: 0 },
        { x: 650, y: 150 },
    ]
    const s = path.createSmoothLine(input);
    let ctx = document.getElementById('cv').getContext('2d');
    ctx.strokeStyle = 'blue';
    ctx.beginPath();
    ctx.moveTo(0, 0);
    for (let i = 0; i < s.length; i++) {
        ctx.lineTo(s[i].x, s[i].y);
    }
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(0, 0);
    for (let i = 0; i < input.length; i++) {
        ctx.lineTo(input[i].x, input[i].y);
    }
    ctx.strokeStyle = 'red';
    ctx.stroke();
    document.getElementById('btn').addEventListener('click', () => {
        let app = document.getElementById('app');
        let index = 0;
        let move = () => {
            if (index < s.length) {
                app.style.left = s[index].x - 10 + 'px';
                app.style.top = s[index].y - 10 + 'px';
                index++;
                requestAnimationFrame(move)
            }
        }
        move()
    })

Appendix: Vector2D related code

/**
 *
 *
 * @class Vector2D
 * @extends {Array}
 */
class Vector2D extends Array {
  /**
   * Creates an instance of Vector2D.
   * @param {number} [x=1]
   * @param {number} [y=0]
   * @memberof Vector2D
   * */
  constructor(x: number = 1, y: number = 0) {
    super();
    this.x = x;
    this.y = y;
  }

  /**
   *
   * @param {number} v
   * @memberof Vector2D
   */
  set x(v) {
    this[0] = v;
  }

  /**
   *
   * @param {number} v
   * @memberof Vector2D
   */
  set y(v) {
    this[1] = v;
  }

  /**
   *
   *
   * @readonly
   * @memberof Vector2D
   */
  get x() {
    return this[0];
  }

  /**
   *
   *
   * @readonly
   * @memberof Vector2D
   */
  get y() {
    return this[1];
  }

  /**
   *
   *
   * @readonly
   * @memberof Vector2D
   */
  get length() {
    return Math.hypot(this.x, this.y);
  }

  /**
   *
   *
   * @readonly
   * @memberof Vector2D
   */
  get dir() {
    return Math.atan2(this.y, this.x);
  }

  /**
   *
   *
   * @return {*}
   * @memberof Vector2D
   */
  copy() {
    return new Vector2D(this.x, this.y);
  }

  /**
   *
   *
   * @param {*} v
   * @return {*}
   * @memberof Vector2D
   */
  add(v) {
    this.x += vx;
    this.y += vy;
    return this;
  }

  /**
   *
   *
   * @param {*} v
   * @return {*}
   * @memberof Vector2D
   */
  sub(v) {
    this.x -= vx;
    this.y -= vy;
    return this;
  }

  /**
   *
   *
   * @param {*} a
   * @return {Vector2D}
   * @memberof Vector2D
   */
  scale(a) {
    this.x *= a;
    this.y *= a;
    return this;
  }

  /**
   *
   *
   * @param {*} rad
   * @return {*}
   * @memberof Vector2D
   */
  rotate(rad) {
    const c = Math.cos(rad);
    const s = Math.sin(rad);
    const [x, y] = this;

    this.x = x * c + y * -s;
    this.y = x * s + y * c;

    return this;
  }

  /**
   *
   *
   * @param {*} v
   * @return {*}
   * @memberof Vector2D
   */
  cross(v) {
    return this.x * vy - vx * this.y;
  }

  /**
   *
   *
   * @param {*} v
   * @return {*}
   * @memberof Vector2D
   */
  dot(v) {
    return this.x * vx + vy * this.y;
  }

  /**
   * Normalization*
   * @return {*}
   * @memberof Vector2D
   */
  normalize() {
    return this.scale(1 / this.length);
  }
}

/**
 * Vector addition *
 * @param {*} vec1
 * @param {*} vec2
 * @return {Vector2D}
 */
function vector2dPlus(vec1, vec2) {
  return new Vector2D(vec1.x + vec2.x, vec1.y + vec2.y);
}

/**
 * Vector subtraction *
 * @param {*} vec1
 * @param {*} vec2
 * @return {Vector2D}
 */
function vector2dMinus(vec1, vec2) {
  return new Vector2D(vec1.x - vec2.x, vec1.y - vec2.y);
}

export {Vector2D, vector2dPlus, vector2dMinus};

Summarize

This is the end of this article on how to use Javascript to generate smooth curves. For more information about JS generating smooth curves, please search for previous articles on 123WORDPRESS.COM or continue to browse the following related articles. I hope you will support 123WORDPRESS.COM in the future!

<<:  Guide to Efficient Use of MySQL Indexes

>>:  DockerToolBox file mounting implementation code

Recommend

IE6 implements min-width

First of all, we know that this effect should be ...

Implementation of grayscale release with Nginx and Lua

Install memcached yum install -y memcached #Start...

Detailed explanation of Docker working mode and principle

As shown in the following figure: When we use vir...

Docker image access to local elasticsearch port operation

Using the image service deployed by docker stack,...

CSS3 overflow property explained

1. Overflow Overflow is overflow (container). Whe...

Vue+ElementUI implements paging function-mysql data

Table of contents 1. Problem 2. Solution 2.1 Pagi...

CSS optimization skills self-practice experience

1. Use css sprites. The advantage is that the smal...

Summary of the advantages of Vue3 vs. Vue2

Table of contents 1. Why do we need vue3? 2. Adva...

Mysql practical exercises simple library management system

Table of contents 1. Sorting function 2. Prepare ...

Linux bridge method steps to bridge two VirtualBox virtual networks

This article originated from my complaints about ...

Detailed explanation of Vue filters

<body> <div id="root"> <...

Detailed explanation of common methods of JavaScript String

Table of contents 1. charAt grammar parameter ind...

MySql5.7.21 installation points record notes

The downloaded version is the Zip decompression v...