Source: pm.js

/**
 * Only do rendering here, keep all drawing functions and counter mutators in ui.pm
 * @fileoverview Presentation Manager module abstracts functions common to all games.
 * @author Robert Laing
 * @module pm
 */

 /**
  * The destination portion with only a string key referencing the binary (ie "nontransferable") data stored separately in imagesDict.
  * @typedef {Object} module:pm.renderObject
  * @property {string} name - key to imagesDict, creating a "flyweight" pattern to avoid duplicating image blobs, also allows structuredClone
  * @property {Array} [base] - base in state corresponding to counter
  * @property {pixels} x1 - sprite's current center X, used by ctx.translate
  * @property {pixels} y1 - sprite's current center Y, used by ctx.translate
  * @property {pixels} sx - Best draw() looks for the source corner here in case it's a sprite
  * @property {pixels} sy - Best draw() looks for the source corner here in case it's a sprite
  * @property {pixels} sWidth - Best draw() looks for the source corner here in case it's a sprite
  * @property {pixels} sHeight - Best draw() looks for the source corner here in case it's a sprite
  * @property {radians} angle1 - 0 for stationary drawn images or Math.PI/2 for sprites, Math.atan2(y2 - y1, x2 - x1) for moving images
  * @property {pixels} [x2] - sprite's destination center X, same as x1 if not moving
  * @property {pixels} [y2] - sprite's destination center Y
  * @property {radians} [angle2] - sprite's destination direction, same as angle1 if it's not rotating 
  * @property {pixels} [velocity] - 0.0 if the sprite is stationary
  * @property {pixels} [deltaX] - added to x1 each tick, Math.cos(angle1 + Math.PI/2) * velocity (the 90 degree addition is to remove the sprite facing up)
  * @property {pixels} [deltaY] - added to y1 each tick, Math.sin(angle1 + Math.PI/2) * velocity
  * @property {degrees} [deltaAngle] -- added to angle1 each tick, controls how fast sprites rotate
  * @property {pixels} width - needed by ctx.drawImage
  * @property {pixels} height - needed by ctx.drawImage
  * @property {boolean} live - if false, gets removed from sprites array
  * @property {string} [status] - per game lables such as "idle", "selectable", "selected", "moving", "attacking", "defending", "killed" used to access relevant update function.
  */

/**
 * The source portion and binary (ie "nontransferable") data of an item to be drawn in a canvas.
 * @typedef {Object} module:pm.imageObject
 * @property {Image} image - loaded from a file or offscreen canvas drawing
 * @property {boolean} loaded - a flag to avoid ctx.drawImage crashes
 * @property {pixels} sx - left hand side of source image, usually 0 unless from sprite sheet
 * @property {pixels} sy - top of source image, usually 0 unless from sprite sheet
 * @property {pixels} sWidth - source width
 * @property {pixels} sHeight - source height
 */

/** 
 * A flyweight pattern to hold blobs of images to be used any number of times by renderObjects
 * @member {{name: imageObject}} module:pm.imagesDict - Accessed by clients via upsert_imagesDict(name, imageObject)
 */
const imagesDict = {};

/** 
 * @member {{name: renderObject}} module:pm.renderDict - Accessed by clients via upsert_renderDict(name, renderObject)
 */
const renderDict = {};


/**
 * Exported function to add or replace an imageObject in imagesDict
 * @function module:pm.imagesDict
 * @param {string} key - not necessarily the same as renderObject.name, but can be.
 * @param {imageObject} value - An object with the relevant keys for the given item to be rendered.
 */ 
function upsert_imagesDict(key, value) {
  imagesDict[key] = value;
}

/**
 * Exported function to add or replace a renderObject in renderDict
 * @function module:pm.renderDict
 * @param {string} key - not necessarily the same as renderObject.name, but can be.
 * @param {renderObject} value - An object with the relevant keys for the given item to be rendered.
 */
function upsert_renderDict(key, value) {
  renderDict[key] = value;
}

function mutateRenderDict(key, property, value) {
  renderDict[key][property] = value;
}

/**
 * @function module:pm.render
 * @param {string[]} renderArr
 */
function render(renderArr) {
  renderArr.forEach(function(name) {
    draw(window.ctx, renderDict[name]);
  });
}

/**
 * Moved sx, sy, sWidth, sHeight to renderObject, so no longer reference via imagesDict[renderObject.name] to allow sprites.
 * @function module:pm.draw
 * @param {CanvasRenderingContext2D} ctx - Either for an OffscreenCanvas or browser canvas.
 * @param {renderObject} renderObject - Note the renderObject looks up the source image via the name string.
 */
function draw(ctx, renderObject) {
  if (renderObject && imagesDict[renderObject.name] && imagesDict[renderObject.name].loaded) {
    try {
      ctx.save();
      ctx.translate(renderObject.x1, renderObject.y1);
      ctx.rotate(renderObject.angle1);
      ctx.drawImage(
        imagesDict[renderObject.name].image,
        renderObject.sx, renderObject.sy,
        renderObject.sWidth, renderObject.sHeight,
        -renderObject.width/2, -renderObject.height/2, renderObject.width, renderObject.height);
      ctx.restore();
    } catch (error) {
      window.container.textContent = error;
    }
  }
}


/**
 * @function module:pm.resize
 */
function resize(scale) {
  Object.values(renderDict).forEach(function(renderObject) {
    if (renderObject) {
      renderObject.x1 *= scale;
      renderObject.y1 *= scale;
      renderObject.width *= scale;
      renderObject.height *= scale;
    }
  });
}

export default Object.freeze({imagesDict, renderDict, upsert_imagesDict, upsert_renderDict, mutateRenderDict, render, draw, resize});