Source: Shape.js

/**
 * @license GPL-3.0-only
 *
 * @author Mark Mayes / mm-dev
 *
 *
 * @module Shape
 *
 * @description
 * ## Draw basic polygon-type shapes
 */

import { GRADIENT_TYPE } from "./PD/ENUM.js";
import * as GAME from "./PD/GAME.js";

import { Display } from "./Display.js";
import { Layout } from "./Layout.js";

import {
  degreesToRadians,
  degreesToVector,
  hexOpacityToRGBA,
  vectorGetUnitNormal,
} from "./utils.js";

class Shape {}

/**
 * @function updateSizes
 * @static
 *
 * @description
 * ##### Update the generic outer stroke thickness based on the current layout/viewport
 */
Shape.updateSizes = function () {
  Shape.strokeThickness = Math.round(
    GAME.SHAPE_STROKE_THICKNESS * Layout.proportionalMultiplier
  );
};

/**
 * @function drawCircle
 * @static
 *
 * @description
 * ##### Draw a basic gradient-filled circle
 *
 * @param {number} _x - X coordinate to draw at
 * @param {number} _y - Y coordinate to draw at
 * @param {object} _data - Other details
 */
Shape.drawCircle = function (_x, _y, _data) {
  var gradient = Shape.getObstacleGradient(_x, _y, _data);

  Display.ctx.beginPath();
  Display.ctx.arc(_x, _y, _data.radius, 0, GAME.PIx2);

  if (_data.useDefaultStroke) {
    Shape.defaultStrokeCurrentPath();
  }

  Display.ctx.fillStyle = gradient;
  Display.ctx.fill();
};

Shape.drawSkewedCircle = function (_x, _y, _obstacle) {
  var /*
ctx.transform(
  [Increases or decreases the size of the pixels horizontally],
  [This effectively angles the X axis up or down],
  [This effectively angles the Y axis left or right],
  [Increases or decreases the size of the pixels vertically],
  [Moves the whole coordinate system horizontally],
  [Moves the whole coordinate system vertically],
)

  */
    // Skew x coordinates by angle skewHorizRadians
    skewHorizDegrees = 90,
    skewHorizRadians = degreesToRadians(skewHorizDegrees),
    // Skew y coordinates by angle skewVertRadians
    skewVertDegrees = 0,
    skewVertRadians = degreesToRadians(skewVertDegrees),
    //offsetX = Math.sin(_obstacle.radius * skewHorizRadians),
    //offsetY = Math.cos(_obstacle.radius * skewVertRadians)
    //offsetX = Math.sin(skewVertRadians) * _obstacle.radius * 2,
    offsetX = 0,
    offsetY = 0;
  // opposite = Math.tan(0.915)*60
  //offsetY = 0
  //offsetY = _obstacle.radius * skewVertRadians
  Display.ctx.save();

  //Display.ctx.translate(Layout.canvasWidth / 2, Layout.canvasHeight / 2);
  Display.ctx.translate(_x, _y);
  Display.ctx.transform(1, skewHorizRadians, skewVertRadians, 1, 0, 0);
  //Display.ctx.rotate(skewVertRadians);
  //Display.ctx.rotate(skewHorizRadians);

  Display.ctx.beginPath();
  Display.ctx.arc(_x, _y, _obstacle.radius, 0, GAME.PIx2);

  if (_obstacle.useDefaultStroke) {
    Shape.defaultStrokeCurrentPath();
  }

  Display.ctx.fillStyle = Shape.getObstacleGradient(_x, _y, _obstacle);
  Display.ctx.fill();
  //Display.ctx.strokeStyle = "red";
  //Display.ctx.stroke();

  Display.ctx.restore();
};

/**
 * @function drawEllipse
 * @static
 *
 * @description
 * ##### Draw an ellipse
 *
 * @param {number} _x
 * @param {number} _y
 * @param {number} _radius
 * @param {string} _color
 * @param {number} _stretch
 * @param {number} _angleDegrees
 */
Shape.drawEllipse = function (
  _x,
  _y,
  _radius,
  _color,
  _stretch,
  _angleDegrees
) {
  var radians = degreesToRadians(_angleDegrees);

  Display.ctx.beginPath();
  Display.ctx.ellipse(
    Math.round(_x),
    Math.round(_y),
    _radius,
    _radius * _stretch,
    radians,
    0,
    GAME.PIx2
  );
  Display.ctx.fillStyle = _color;
  Display.ctx.fill();
};

/**
 * @function getObstacleGradient
 * @static 
 *
 * @description
 * ##### Draw a gradient based on obstacle properties
 * - Matches size and rotation of the gradient to the obstacle
 * - Uses defaults where properties aren't specified
 *
 * @param {number} _x - X coordinate to draw at
 * @param {number} _y - Y coordinate to draw at
 * @param {object} _obstacle - The obstacle to get properties from
 * @param {object} _obstacle.gradient - Details about the gradient to be applied
 * @param {object[]} _obstacle.gradient.stop_ar - Gradient stops in `{pos, color}` format
 * @param {GRADIENT_TYPE} _obstacle.gradient.type - Eg linear/radial
 * @param {number} _obstacle.gradient.fadePoint - Used to fade the default gradient out to the background colour of the current level (`0` to `1` - `0` means fade from centre, `1` would be the outer edge so effectively no fade)
 j
 * @returns {CanvasGradient}
 */
Shape.getObstacleGradient = function (_x, _y, _obstacle) {
  var i,
    gradient,
    gradientType = _obstacle.gradient?.type || GRADIENT_TYPE.RADIAL,
    gradientStop_ar = _obstacle.gradient?.stop_ar || [
      { pos: 0, color: _obstacle.color },
      {
        pos: _obstacle.gradientFadePoint || GAME.OBSTACLE_FADE_GRADIENT_STOP,
        color: _obstacle.color,
      },
      { pos: 1, color: Display.bgColor },
    ],
    directionVector = degreesToVector(
      _obstacle.rotation + (_obstacle.gradient?.rotation || 0)
    );

  if (_obstacle.gradient?.offsetX) {
    _x += _obstacle.gradient.offsetX * _obstacle.radius;
  }
  if (_obstacle.gradient?.offsetY) {
    _y += _obstacle.gradient.offsetY * _obstacle.radius;
  }

  switch (gradientType) {
    case GRADIENT_TYPE.LINEAR:
      gradient = Display.ctx.createLinearGradient(
        _x - directionVector.x * _obstacle.radius,
        _y - directionVector.y * _obstacle.radius,
        _x + directionVector.x * _obstacle.radius,
        _y + directionVector.y * _obstacle.radius
      );
      break;
    case GRADIENT_TYPE.RADIAL:
      gradient = Display.ctx.createRadialGradient(
        _x,
        _y,
        _obstacle.radius / 2,
        _x,
        _y,
        _obstacle.radius
      );
      break;
    case GRADIENT_TYPE.LINEAR_TUMBLING:
      gradient = Display.ctx.createLinearGradient(
        _x - _obstacle.radius,
        _y - _obstacle.radius,
        _x + Math.cos(_obstacle.rotation) * _obstacle.radius * 2,
        _y + Math.sin(_obstacle.rotation) * _obstacle.radius * 2
      );
      break;
    case GRADIENT_TYPE.RADIAL_TUMBLING:
      gradient = Display.ctx.createRadialGradient(
        _x + Math.cos(_obstacle.rotation) * _obstacle.radius * 1.2,
        _y + Math.sin(_obstacle.rotation) * _obstacle.radius * 1.2,
        _obstacle.radius / 2,
        _x + Math.cos(_obstacle.rotation) * _obstacle.radius * 1.2,
        _y + Math.sin(_obstacle.rotation) * _obstacle.radius * 1.2,
        _obstacle.radius
      );
      break;
    default:
      break;
  }

  for (i = 0; i < gradientStop_ar.length; i++) {
    gradient.addColorStop(gradientStop_ar[i].pos, gradientStop_ar[i].color);
  }

  return gradient;
};

/**
 * @function drawSquarcle
 * @static
 *
 * @description
 * ##### Draw a combined square and circle
 * - Used eg for bullets, ghosts
 *
 * @param {number} _x
 * @param {number} _y
 * @param {object} _obstacle
 */
Shape.drawSquarcle = function (_x, _y, _obstacle) {
  var fillStyle = _obstacle.gradient
      ? Shape.getObstacleGradient(_x, _y, _obstacle)
      : _obstacle.color,
    directionVector = degreesToVector(_obstacle.rotation),
    directionVectorNormal = vectorGetUnitNormal(directionVector),
    arcRotation = _obstacle.rotation + 90;

  // Semicircle
  Display.ctx.beginPath();
  Display.ctx.arc(
    _x,
    _y,
    _obstacle.radius,
    degreesToRadians(arcRotation + 180),
    degreesToRadians(arcRotation)
  );

  // Rectangle
  Display.ctx.moveTo(
    _x - directionVectorNormal.x * _obstacle.radius,
    _y - directionVectorNormal.y * _obstacle.radius
  );

  Display.ctx.lineTo(
    _x -
      directionVectorNormal.x * _obstacle.radius -
      directionVector.x * _obstacle.radius,
    _y -
      directionVectorNormal.y * _obstacle.radius -
      directionVector.y * _obstacle.radius
  );

  Display.ctx.lineTo(
    _x +
      directionVectorNormal.x * _obstacle.radius -
      directionVector.x * _obstacle.radius,
    _y +
      directionVectorNormal.y * _obstacle.radius -
      directionVector.y * _obstacle.radius
  );

  Display.ctx.lineTo(
    _x + directionVectorNormal.x * _obstacle.radius,
    _y + directionVectorNormal.y * _obstacle.radius
  );

  if (_obstacle.useDefaultStroke) {
    Shape.defaultStrokeCurrentPath();
  }
  Display.ctx.fillStyle = fillStyle;
  Display.ctx.fill();
};

/**
 * @function drawStar
 * @static
 *
 * @description
 * ##### Draw a star shape
 *
 * @param {number} _x
 * @param {number} _y
 * @param {object} _obstacle
 * @param {number} _obstacle.numAppendages - Number of points of the star
 * @param {number} _obstacle.shapeCenterRadiusDivisor - Size of the central fill based on the radius of the obstacle
 * @param {boolean} _obstacle.useDefaultStroke - If `true` add the standard stroke around the shape
 */
Shape.drawStar = function (_x, _y, _obstacle) {
  var i,
    x1,
    y1,
    x2,
    y2,
    angle1,
    angle2,
    angle3,
    fillStyle = _obstacle.gradient
      ? Shape.getObstacleGradient(_x, _y, _obstacle)
      : _obstacle.color;

  Display.ctx.moveTo(Math.round(_x), Math.round(_y));
  Display.ctx.beginPath();
  for (i = 0; i < _obstacle.numAppendages; i++) {
    angle1 = (GAME.PIx2 / _obstacle.numAppendages) * i;
    angle2 = (GAME.PIx2 / _obstacle.numAppendages) * (i + 1);
    angle3 = (GAME.PIx2 / _obstacle.numAppendages) * (i + 2);

    angle1 += _obstacle.rotation;
    angle2 += _obstacle.rotation;
    angle3 += _obstacle.rotation;

    // TODO degreesToVector?
    x1 =
      (_obstacle.radius / _obstacle.shapeCenterRadiusDivisor) *
        Math.sin(angle1) +
      _x;
    y1 =
      (_obstacle.radius / _obstacle.shapeCenterRadiusDivisor) *
        Math.cos(angle1) +
      _y;
    x2 = _obstacle.radius * Math.sin(angle2) + _x;
    y2 = _obstacle.radius * Math.cos(angle2) + _y;
    x3 =
      (_obstacle.radius / _obstacle.shapeCenterRadiusDivisor) *
        Math.sin(angle3) +
      _x;
    y3 =
      (_obstacle.radius / _obstacle.shapeCenterRadiusDivisor) *
        Math.cos(angle3) +
      _y;

    Display.ctx.lineTo(x1, y1);
    Display.ctx.lineTo(x2, y2);
    Display.ctx.lineTo(x3, y3);
  }
  Display.ctx.closePath();

  if (_obstacle.useDefaultStroke) {
    Shape.defaultStrokeCurrentPath();
  }
  Display.ctx.fillStyle = fillStyle;
  Display.ctx.fill();
};

/**
 * @function defaultStrokeCurrentPath
 * @static
 *
 * @description
 * ##### Add a stroke/outline to a shape
 * The stroke is coloured as a translucent version of the background colour of the current level
 */
Shape.defaultStrokeCurrentPath = function () {
  Display.ctx.lineWidth = Shape.strokeThickness;
  Display.ctx.strokeStyle = hexOpacityToRGBA(
    Display.bgColor,
    GAME.SHAPE_STROKE_ALPHA
  );
  Display.ctx.stroke();
};

/**
 * @function drawFlower
 * @static
 *
 * @description
 * ##### Draw a star shape
 *
 * @param {number} _x
 * @param {number} _y
 * @param {object} _obstacle
 * @param {number} _obstacle.numAppendages - Number of petals of the flower
 * @param {number} _obstacle.shapeCenterColor - Colour to use for the flower centre
 * @param {number} _obstacle.shapeCenterRadiusDivisor - Size of the central fill based on the radius of the obstacle
 * @param {boolean} _obstacle.useDefaultStroke - If `true` add the standard stroke around the shape
 */
Shape.drawFlower = function (_x, _y, _obstacle) {
  var i,
    x1,
    y1,
    x2,
    y2,
    angle1,
    angle2,
    // Collisions with flower petals happened too far away due to bezier curves, this adjusts the draw distance to make things feel more accurate
    petalCurveAllowance =
      (_obstacle.radius / _obstacle.numAppendages) * Math.PI,
    petalRadius = _obstacle.radius + petalCurveAllowance,
    fillStyle = _obstacle.gradient
      ? Shape.getObstacleGradient(_x, _y, _obstacle)
      : _obstacle.color;

  // petals
  Display.ctx.beginPath();
  Display.ctx.moveTo(Math.round(_x), Math.round(_y));
  for (i = 0; i < _obstacle.numAppendages; i++) {
    angle1 = (GAME.PIx2 / _obstacle.numAppendages) * (i + 1);
    angle2 = (GAME.PIx2 / _obstacle.numAppendages) * i;

    angle1 += _obstacle.rotation;
    angle2 += _obstacle.rotation;

    x1 = petalRadius * Math.sin(angle1) + _x;
    y1 = petalRadius * Math.cos(angle1) + _y;
    x2 = petalRadius * Math.sin(angle2) + _x;
    y2 = petalRadius * Math.cos(angle2) + _y;

    Display.ctx.bezierCurveTo(x1, y1, x2, y2, _x, _y);
  }
  Display.ctx.closePath();

  if (_obstacle.useDefaultStroke) {
    Shape.defaultStrokeCurrentPath();
  }
  Display.ctx.fillStyle = fillStyle;
  Display.ctx.fill();

  // center
  Display.ctx.beginPath();
  Display.ctx.arc(
    _x,
    _y,
    _obstacle.radius / _obstacle.shapeCenterRadiusDivisor,
    0,
    GAME.PIx2
  );
  //Display.ctx.closePath();
  Display.ctx.fillStyle = _obstacle.shapeCenterColor || fillStyle;
  Display.ctx.fill();
};

export { Shape };