Source

Verly.js

import Mouse from './Mouse';

/**
 * @class Verly
 * @version 1.3.0
 * @author <hazru.anurag@gmail.com>
 */
class Verly {
  /**
   * 
   * @param {Number} iterations 
   * @param {HTMLCanvasElement} canvas 
   * @param {CanvasRenderingContext2D} ctx 
   */
  constructor(iterations, canvas, ctx) {
    this.entities = [];
    this.iterations = iterations;
    this.currentFrame = 0;
    this.canvas = canvas;
    this.WIDTH = canvas.width;
    this.HEIGHT = canvas.height;
    this.ctx = ctx;
    this.mouse = new Mouse(this.entities, this.canvas, this.ctx);
  }

  /**
   * sets the canvas DPI for better rendering quality
   */
  setDPI() {
    // Set up CSS size.
    this.canvas.style.width = this.canvas.style.width || this.canvas.width + 'px';
    this.canvas.style.height = this.canvas.style.height || this.canvas.height + 'px';

    // Get size information.
    var scaleFactor = window.devicePixelRatio / 1;
    var width = parseFloat(this.canvas.style.width);
    var height = parseFloat(this.canvas.style.height);

    // Backup the this.canvas contents.
    var oldScale = this.canvas.width / width;
    var backupScale = scaleFactor / oldScale;
    var backup = this.canvas.cloneNode(false);
    backup.getContext('2d').drawImage(this.canvas, 0, 0);

    // Resize the this.canvas.
    this.canvas.width = Math.ceil(width * scaleFactor);
    this.canvas.height = Math.ceil(height * scaleFactor);

    // Redraw the this.canvas image and scale future draws.
    this.ctx.setTransform(backupScale, 0, 0, backupScale, 0, 0);
    this.ctx.drawImage(backup, 0, 0);
    this.ctx.setTransform(scaleFactor, 0, 0, scaleFactor, 0, 0);
  }

  
  /**
   * @param  {...Entity} args
   * @description Joins two Entity Class Together 
   * 
   * @example
   * let canvas = document.getElementById('c');
   * let ctx = canvas.getContext('2d');
   * let width = canvas.width = 600;
   * let height = canvas.height = 500;
   * 
   * let verly = new Verly(16, canvas, ctx);
   * let box = verly.createBox(100, 100, 100, 100);
   * let rope = verly.createRope(100, 100, 15, 10, 0);
   * 
   * // verly.joinEntities(...Entity)
   * let mix = verly.joinEntities(box, rope);
   * mix.addStick(0, 18, 20)
   * 
   * function animate() {
   *  ctx.clearRect(0, 0, width, height);
   * 
   *  verly.update();
   *  verly.render();
   *  verly.interact();
   *  verly.renderPointIndex();
   * 
   *  requestAnimationFrame(animate);
   * }
   * animate();
   * 
   */
  joinEntities(...args) {
    let mixEntity = new Entity(this.iterations, this);

    let points = [];
    let sticks = [];

    // loop through the args and push points and sticks to the array
    for (let i = 0; i < args.length; i++) {
      points.push(args[i].points);
      sticks.push(args[i].sticks);

      // get the index which item we should splice in [this.entities]
      let index = this.entities.indexOf(args[i]);
      this.entities.splice(index, 1);
    }

    // join multiple arrays
    points = [].concat.apply([], points);
    sticks = [].concat.apply([], sticks);

    // add the arrays to the mix::Entity
    mixEntity.points = points;
    mixEntity.sticks = sticks;

    // add the mix::Entity to [this.entities]
    this.addEntity(mixEntity);
    return mixEntity; // return for chaining
  }

  /**
   * @param {Entity} e 
   */
  addEntity(e) {
    this.entities.push(e);
  }

  /**
   * drags points
   */
  interact() {
    this.mouse.drag();
  }

  /**
   * updates all the physics stuff
   */
  update() {
    for (let i = 0; i < this.entities.length; i++) {
      this.entities[i].update();
    }

    this.currentFrame++;
  }

  /**
   */
  renderPointIndex() {
    for (let i = 0; i < this.entities.length; i++) {
      this.entities[i].renderPointIndex(this.ctx);
    }
  }

  /**
   * renders all the entity
   */
  render() {
    for (let i = 0; i < this.entities.length; i++) {
      this.entities[i].render(this.ctx);
    }
  }




  /**
   * @param {number} x 
   * @param {number} y 
   * @param {number} w 
   * @param {number} h 
   */
  createBox(x, y, w, h) {
    const box = new Entity(this.iterations, this);
    box.addPoint(x, y, 0, 0);
    box.addPoint(x + w, y, 0, 0);
    box.addPoint(x + w, y + h, 0, 0);
    box.addPoint(x, y + h, 0, 0);
    box.addStick(0, 1);
    box.addStick(1, 2);
    box.addStick(2, 3);
    box.addStick(3, 0);
    box.addStick(3, 1);

    this.addEntity(box);
    return box;
  }


  /**
   * @param {number} x 
   * @param {number} y 
   * @param {number} segments 
   * @param {number} radius=50
   * @param {number} stride1=1
   * @param {number} stride2=5
   */
  createHexagon(x, y, segments, radius = 50, stride1 = 1, stride2 = 5) {
    const hexagon = new Entity(this.iterations, this);

    let stride = (2 * Math.PI) / segments;

    // points
    for (let i = 0; i < segments; ++i) {
      let theta = i * stride;
      hexagon.addPoint(
        x + Math.cos(theta) * radius,
        y + Math.sin(theta) * radius,
        0, 0
      );
    }

    let center = hexagon.addPoint(x, y, 0, 0);

    // sticks
    for (let i = 0; i < segments; ++i) {
      hexagon.addStick(i, (i + stride1) % segments);
      hexagon.addStick(new Stick(hexagon.points[i], center));
      hexagon.addStick(i, (i + stride2) % segments);
    }


    this.addEntity(hexagon);
    return hexagon;
  }

  /**
   * @param {number} posx 
   * @param {number} posy 
   * @param {number} w 
   * @param {number} h 
   * @param {number} segments 
   * @param {number} pinOffset 
   */
  createCloth(posx, posy, w, h, segments, pinOffset) {
    let cloth = new Entity(this.iterations, this);

    let xStride = w / segments;
    let yStride = h / segments;

    let x, y;
    for (y = 0; y < segments; ++y) {
      for (x = 0; x < segments; ++x) {
        let px = posx + x * xStride - w / 2 + xStride / 2;
        let py = posy + y * yStride - h / 2 + yStride / 2;
        cloth.addPoint(px, py);

        if (x > 0) {
          cloth.addStick(y * segments + x, y * segments + x - 1);
        }

        if (y > 0) {
          cloth.addStick(y * segments + x, (y - 1) * segments + x);
        }
      }
    }

    // as the name suggest
    function tear(threshold) {
      for (let i = 0; i < cloth.sticks.length; i++) {
        // find the distance between two points
        let dist = cloth.sticks[i].startPoint.pos.dist(cloth.sticks[i].endPoint.pos)
        if (dist > (threshold || 20)) { // remove if the dist is > than threshold 
          cloth.removeSticks(cloth.sticks[i].startPoint);
        }
      }
    }

    cloth.tear = tear;

    for (x = 0; x < segments; ++x) {
      if (x % pinOffset == 0) { // magic
        cloth.pin(x);
      }
    }

    !this.dontPush && this.addEntity(cloth);
    return cloth;
  }


  /**
   * @param {number} x 
   * @param {number} y 
   * @param {number} segments=10
   * @param {number} gap=15
   * @param {number} pin=0
   */
  createRope(x, y, segments = 10, gap = 15, pin) {
    let rope = new Entity(this.iterations, this);

    for (let i = 0; i < segments; i++) {
      rope.addPoint(x + i * gap, y, 0, 0)
    }

    for (let i = 0; i < segments - 1; i++) {
      rope.addStick(i, (i + 1) % segments);
    }

    if (pin !== undefined) {
      rope.pin(pin);
    }
    this.addEntity(rope);
    return rope;
  }


  createRagdoll(x0, y0) {
    let ragdoll = new Entity(this.iterations, this);

    // Head
    ragdoll.addPoint(x0, y0).setRadius(15).setMass(5);

    // Groin
    ragdoll.addPoint(x0, y0 + 100);

    // Hips
    ragdoll.addPoint(x0 + 30, y0 + 90);
    ragdoll.addPoint(x0 - 30, y0 + 90);

    // Knees
    ragdoll.addPoint(x0 + 20, y0 + 150);
    ragdoll.addPoint(x0 - 20, y0 + 150);

    // Feet
    ragdoll.addPoint(x0 + 30, y0 + 190).setRadius(10).setMass(20);
    ragdoll.addPoint(x0 - 30, y0 + 190).setRadius(10).setMass(20);

    // Neck
    ragdoll.addPoint(x0, y0 + 25);

    // Shoulders
    ragdoll.addPoint(x0 + 25, y0 + 30);
    ragdoll.addPoint(x0 - 25, y0 + 30);

    // Hands
    ragdoll.addPoint(x0 + 15, y0 + 105).setRadius(10).setMass(5);
    ragdoll.addPoint(x0 - 15, y0 + 105).setRadius(10).setMass(5);



    // "Muscles"
    // Head - shoulders
    ragdoll.addStick(0, 9);
    ragdoll.addStick(0, 10);
    // Shoulder - shoulder
    ragdoll.addStick(9, 10);

    // Shoulders - hips
    ragdoll.addStick(9, 2);
    ragdoll.addStick(10, 3);
    // Shoulders - hips opposite side
    ragdoll.addStick(9, 3);
    ragdoll.addStick(10, 2);

    // Hips - feet
    ragdoll.addStick(2, 6);
    ragdoll.addStick(3, 7);

    // Hips - feet, opposite
    ragdoll.addStick(2, 7);
    ragdoll.addStick(3, 6);

    // Head - groin
    ragdoll.addStick(0, 1);

    // Hip - hip
    ragdoll.addStick(2, 3);
    // Shoulder - hip
    ragdoll.addStick(9, 2);
    ragdoll.addStick(10, 3);

    // Head - knee
    ragdoll.addStick(0, 4);
    // Head - knee
    ragdoll.addStick(0, 5);

    // Head feet
    ragdoll.addStick(0, 6);
    ragdoll.addStick(0, 7);

    // Body parts
    // Hips
    ragdoll.addStick(1, 2);
    ragdoll.addStick(1, 3);
    // Legs
    ragdoll.addStick(2, 4);
    ragdoll.addStick(3, 5);
    ragdoll.addStick(4, 6);
    ragdoll.addStick(5, 7);

    ragdoll.addStick(0, 8);
    ragdoll.addStick(8, 1);

    // Left arm
    ragdoll.addStick(8, 9);
    ragdoll.addStick(9, 11);

    // Right arm
    ragdoll.addStick(8, 10);
    ragdoll.addStick(10, 12);

    this.addEntity(ragdoll);
    return ragdoll;
  }
}

export default Verly;