Block.js

/**
Creates a new rigid body model with a rectangular hull.  <br/>
<br/>
This class allows the block <br/>
- to be constrained to other blocks or to the scene itself <br/>
- to apply a force from other blocks it collides with <br/>
- to rotate around its center via attribute rotate <br/>
- trigger an actions from other blocks it collides with <br/>

@param {Matter.World} world - The Matter.js world
@param {object} attributes - Visual properties e.g. position, dimensions and color
@param {Matter.IChamferableBodyDefinition} [options] - Defines the behaviour e.g. mass, bouncyness or whether it can move
@extends BlockCore

@example
const attributes = {
  x: 400,
  y: 500,
  w: 810,
  h: 15,
  color: "grey"
}

const options = {
  isStatic: true,
  angle: PI / 36
}

let box = new Block(world, attributes, options)

@tutorial
1 - Mouse Example
<a target="_blank" href="https://b-g.github.io/p5-matter-examples/1-mouse/">Open preview</a>
,
<a target="_blank" href="https://github.com/b-g/p5-matter-examples/blob/master/1-mouse/sketch.js">open code</a>
*/

class Block extends BlockCore {
  /** @type {Matter.Constraint[]} */ constraints;

  /**
   * @param {Matter.World} world
   * @param {object} attributes
   * @param {Matter.IChamferableBodyDefinition} options
   */
  constructor(world, attributes, options) {
    super(world, attributes, options);
    this.collisions = [];
    this.constraints = [];
  }

  draw() {
    if (this.body) {
      this.update();
      if (this.attributes.color || this.attributes.stroke) {
        super.draw();
      }
      if (this.attributes.image) {
        this.drawSprite();
      }
      if (this.constraints.length > 0) {
        for (let c of this.constraints) {
          // TODO: Die Eigenschaft "draw" ist für den Typ "Constraint" nicht vorhanden. ts(2339)
          if (c.draw === true) this.drawConstraint(c);
        }
      }
    }
  }

  /**
   * Draws the constraints (if any) of the matter body to the p5 canvas
   * @method drawConstraints
   * @memberof Block
   */
  drawConstraints() {
    if (this.constraints.length > 0) {
      for (let c of this.constraints) {
        this.drawConstraint(c);
      }
    }
  }

  drawConstraint(constraint) {
    if (constraint.color) {
      stroke(constraint.color);
    } else {
      stroke("magenta");
    }
    strokeWeight(2);
    const offsetA = constraint.pointA;
    let posA = {
      x: 0,
      y: 0
    };
    if (constraint.bodyA) {
      posA = constraint.bodyA.position;
    }
    const offsetB = constraint.pointB;
    let posB = {
      x: 0,
      y: 0
    };
    if (constraint.bodyB) {
      posB = constraint.bodyB.position;
    }
    if (constraint.image) {
      push();
      translate(this.body.position.x, this.body.position.y);
      const angle = Math.atan2((posB.y + offsetB.y) - (posA.y + offsetA.y), (posB.x + offsetB.x) - (posA.x + offsetA.x))
      rotate(angle + Math.PI / 2);
      imageMode(CENTER);
      image(constraint.image, this.offset.x, this.offset.y, constraint.image.width * this.attributes.scale, constraint.image.height * this.attributes.scale);
      pop();
    } else {
      line(
        posA.x + offsetA.x,
        posA.y + offsetA.y,
        posB.x + offsetB.x,
        posB.y + offsetB.y
      );
    }
  }

  update() {
    this.collisions.forEach(block => {
      if (block.attributes.force) {
        Matter.Body.applyForce(this.body, this.body.position, block.attributes.force);
      }
      if (block.attributes.trigger) {
        block.attributes.trigger(this, block);
      }
    });
    this.collisions = [];
  }

  /**
   * Constrains this block to another block.
   * Constraints are used for specifying that a fixed distance must be maintained between two blocks (or a block and a fixed world-space position).
   * The stiffness of constraints can be modified via the options to create springs or elastic.
   * @param {Block} block
   * @param {Matter.IConstraintDefinition} [options]
   * @return {Matter.Constraint}
   * @memberof Block
   */
  constrainTo(block, options) {
    options.bodyA = this.body;
    if (block) {
      // constrain to another block
      if (!options.bodyB) {
        options.bodyB = block.body;
      }
    } else {
      // constrain to "background" scene
      if (!options.pointB) {
        options.pointB = {
          x: this.body.position.x,
          y: this.body.position.y
        };
      }
    }
    const constraint = Matter.Constraint.create(options);
    this.constraints.push(constraint);
    Matter.World.add(this.world, constraint);
    return constraint;
  }

  /**
   * Remove a constraint of this block to another block.
   * @param {Matter.Constraint} constraint
   * @memberof Block
   */
  removeConstraint(constraint) {
    const idx = this.constraints.indexOf(constraint)
    if (idx > -1) {
      this.constraints.splice(idx, 1)
      Matter.World.remove(world, constraint)
    }
  }

  /**
   * Adds a block to an internal collisions array, to check whether this block colides with another block
   * @param {Block} block
   * @memberof Block
   */
  collideWith(block) {
    if (block && !this.collisions.includes(block)) {
      this.collisions.push(block);
    }
  }

  /**
   * Rotates the block to a specific angle (absolute)
   * set the angle of the block to the given angle
   * @param {number} rotation - The angle in radians
   * @param {Matter.Vector} [point] - The point to rotate around
   * @param {boolean} [updateVelocity] - Whether to update the velocity of the block
   * @memberof Block
   * @example
   * // Rotate the block to 45 degrees
   * block.rotate(PI / 4)
   *
   * // Rotate the block to 45 degrees around a specific point
   * block.rotate(PI / 4, { x: 100, y: 100 })
   *
   * // Rotate the block to 45 degrees around a specific point and update the velocity
   * block.rotate(PI / 4, { x: 100, y: 100 }, true)
   *
   * // Rotate the block to 45 degrees around a specific point and update the velocity
   * block.rotate(PI / 4, { x: 100, y: 100 }, true)
   */
  rotate(rotation, point, updateVelocity) {
    const body = this.body;
    if (!point) {
      Matter.Body.setAngle(body, rotation, updateVelocity);
    } else {
      const currentRotation = body.angle;
      const delta = rotation - currentRotation;
      const cos = Math.cos(delta),
        sin = Math.sin(delta),
        dx = body.position.x - point.x,
        dy = body.position.y - point.y;

      Matter.Body.setPosition(body, {
        x: point.x + (dx * cos - dy * sin),
        y: point.y + (dx * sin + dy * cos)
      }, updateVelocity);

      Matter.Body.setAngle(body, rotation, updateVelocity);
    }
  };

  /**
   * Rotates the block by a specific angle (relative)
   * adds the angle to the current angle of the block
   * @param {number} rotation - The angle in radians
   * @param {Matter.Vector} [point] - The point to rotate around
   * @param {boolean} [updateVelocity] - Whether to update the velocity of the block
   * @memberof Block
   * @example
   * // Increments the rotation of the block by 1 degrees
   * block.rotateBy(PI / 180)
   *
   * // Increments the rotation of the block by 1 degrees around a specific point
   * block.rotateBy(PI / 180, { x: 100, y: 100 })
   */
  rotateBy(rotation, point, updateVelocity) {
    const body = this.body;
    if (!point) {
      Matter.Body.setAngle(body, body.angle + rotation, updateVelocity);
    } else {
      const cos = Math.cos(rotation),
        sin = Math.sin(rotation),
        dx = body.position.x - point.x,
        dy = body.position.y - point.y;

      Matter.Body.setPosition(body, {
        x: point.x + (dx * cos - dy * sin),
        y: point.y + (dx * sin + dy * cos)
      }, updateVelocity);

      Matter.Body.setAngle(body, body.angle + rotation, updateVelocity);
    }
  };

  /**
   * Sets the mass centre of the block to a specific offset
   * the offset is relative to the current mass centre
   * @param {Block} block
   * @param {Matter.Vector} offset
   * @memberof Block
   */
  offsetMassCentre(offset) {
    this.body.position.x += offset.x;
    this.body.position.y += offset.y;
    this.body.positionPrev.x += offset.x;
    this.body.positionPrev.y += offset.y;
  }

  /**
   * Draw an image "sprite" instead of the shape of the block.
   * Make sure to set attributes.image so that there is an image to draw.
   * @memberof Block
   */
  drawSprite() {
    const pos = this.body.position;
    const angle = this.body.angle;
    push();
    translate(pos.x, pos.y);
    rotate(angle);
    imageMode(CENTER);
    image(this.attributes.image, this.offset.x, this.offset.y, this.attributes.image.width * this.attributes.scale, this.attributes.image.height * this.attributes.scale);
    pop();
  }

}