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

Elegant practical record of introducing iconfont icon library into vue

Table of contents Preface Generate SVG Introducti...

Implementation steps for installing FTP server in Ubuntu 14.04

Table of contents Install Software Management Ano...

How to view the creation time of files in Linux

1. Introduction Whether the creation time of a fi...

jQuery plugin to achieve image suspension

This article shares the specific code of the jQue...

Vue project realizes paging effect

The paging effect is implemented in the vue proje...

svg+css or js to create tick animation effect

Previously, my boss asked me to make a program th...

Introduction to the use of MySQL pt-slave-restart tool

Table of contents When setting up a MySQL master-...

10 HTML table-related tags

In fact many people will say “I’ve seen that table...

Web design dimensions and rules for advertising design on web pages

1. Under 800*600, if the width of the web page is...

Introduction to 10 Hooks in React

Table of contents What is ReactHook? React curren...

Installation and configuration tutorial of MongoDB under Linux

MongoDB Installation Choose to install using Yum ...