Source: src/utils/physics.js

import { utils, mutils, scaler, pixiApp, htmlEle, ui} from './../storymode.js';

let Engine,
    Render,
    Runner,
    Bodies,
    Composites,
    Composite,
    Constraint,
    MouseConstraint,
    Mouse,
    Body;

if (typeof window['Matter'] !== 'undefined'){

  Engine = Matter.Engine;
  Render = Matter.Render;
  Runner = Matter.Runner;
  Bodies = Matter.Bodies;
  Composites = Matter.Composites;
  Composite = Matter.Composite;
  Constraint = Matter.Constraint;
  MouseConstraint = Matter.MouseConstraint;
  Mouse = Matter.Mouse;
  Body = Matter.Body;

}

// Overview:
// Integrates between between matter.js to storymode.
//
//
//
// -

// Requires:
/*
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js" integrity="sha512-5T245ZTH0m0RfONiFm2NF0zcYcmAuNzcGyPSQ18j8Bs5Pbfhp5HP1hosrR8XRt5M3kSRqzjNMYpm2+it/AUX/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
*/

/**
 * Handles translation between `Matter.js` (v0.18.0) and `PIXI` coord space.
 * <br>- See {@link  https://brm.io/matter-js/}
 * <br>- When there is syncing, PIXI display objects sync to the engine bodies.
 * <br>- Modifiers and tweens involving physics bodies are always in physics coords.
 * <br>- Bodies and their synced dispos have center registrations.
 * @namespace physics
 */

// JRunner
// -------

/**
 * Handles the link between the `PIXI` ticker and `Matter.js` time.
 * <br>- Handles the `matter.js` engine time update
 * @memberof physics
 * @example
const {..., physics} = require(`storymode`);
// Create jrender instance:
this.jrender = new physics.JRender({ptsPerMeterFactor:0.5});
this.jrunner = new physics.JRunner(this.engine, this.jrender, this.onRender.bind(this), {fixedTimestep: false});
 */
class JRunner { // } extends PIXI.utils.EventEmitter {

/**
 * Called after every `Matter.js` render.
 * @callback physics.JRunner.onRender
 * @param {number} elapsedMS - Time elapsed in milliseconds from last frame to this frame.
 */

 /**
  * JRunner config options.
  * @typedef {Object} physics.JRunner.Options
  * @property {boolean} [fixedTimestep=true] If timing is fixed, then the apparent simulation speed will change depending on the frame rate (but behaviour will be deterministic).<br>If the timing is variable, then the apparent simulation speed will be constant (approximately, but at the cost of determininism).
  * @memberOf physics.JRunner
  */

 /**
  * @param {Matter.Engine} engine - `Matter.js` engine instance.
  * @param {physics.JRender} render - `JRender` class instance.
  * @param {physics.JRunner.onRender} [renderCallback=null] - Function to be called every render, immediately after the `render.drawRender()` is called.
  * @param {physics.JRunner.Options} [options=null] - Configuration object.
  * @constructor
  */
  constructor(engine, render, renderCallback, options){

    this.engine = engine;

    /**
     * A list of `JRender` instances managed by the runner.
     * @readonly
     * @type {Array}
     */
    this.renders = Array.isArray(render) ? render : [render];
    this.renderCallback = renderCallback;

    let defaults = {
      fixedTimestep: true
    };

    options = utils.extend(defaults, options);

    //if (options.fixedTimestep && PIXI.Ticker.shared.maxFPS === 0){
    //  console.log('Physics: Limiting PIXI.Ticker.shared.maxFPS to targetFPS is reccomended for fixed time step.')
    //}

    let targetFPS = 60.0;
    this.targetDelta = 1000.0/targetFPS;
    this.deltaMin = 1000.0/targetFPS;
    this.deltaMax = 1000.0/(targetFPS * 0.5)
    this.fixedTimestep = options.fixedTimestep;

    this.deltaHistory = [];
    this.timeScalePrev = 1;
    this.ticking = false;
    this.accumulator = 0;

  }

  /**
   * Pauses the ticker.
   * @example
onDidExit(fromModal){
 super.onDidExit(fromModal);
 this.jrunner.pauseTick();
}
   */
  pauseTick(){
    this.ticking = false;
    ticker.remove(this.tickFixed, this);
    ticker.remove(this.tickVariable, this);
  }

  /**
   * Resumes the ticker.
   * @example
onWillArrive(fromModal){
 super.onWillArrive(fromModal);
 this.jrunner.resumeTick();
}
   */
  resumeTick(){
    if (this.ticking){
      return;
    }
    this.pauseTick();
    ticker.add(this.fixedTimestep ? this.tickFixed : this.tickVariable, this);
  }

  /**
   * Internal fixed timestep tick function.
   * <br>- If the frame rate slows below the target rate, more engine updates are called to compensate before calling the render.
   * <br>- See {@link https://gafferongames.com/post/fix_your_timestep/}
   */
  tickFixed(){

    this.accumulator += ticker.elapsedMS; // Time elapsed in milliseconds from last frame to this frame.;

    while ( this.accumulator >= this.targetDelta ){
        Engine.update(this.engine, this.targetDelta, 1.0);
        this.accumulator -= this.targetDelta;
        //t += dt;
    }

    // Render
    for (let i = 0; i < this.renders.length; i++){
      this.renders[i].drawRender(this.engine.world);
    }

    if (this.renderCallback){
      this.renderCallback(ticker.elapsedMS);
    }

  }

  /**
   * Internal variable timestep tick function.
   */
  tickVariable(){

    // https://pixijs.download/dev/docs/PIXI.Ticker.html
    // https://brm.io/matter-js/docs/files/src_core_Runner.js.html

    let delta = ticker.elapsedMS; // this value is neither capped nor scaled

    // optimistically filter delta over a few frames, to improve stability
    this.deltaHistory.push(delta)
    this.deltaHistory = this.deltaHistory.slice(-60); // Limit sample to 60
    delta = Math.min.apply(null, this.deltaHistory);

    // limit delta
    delta = delta < this.deltaMin ? this.deltaMin : delta;
    delta = delta > this.deltaMax ? this.deltaMax : delta;

    // correction = dt // USe this if no filtering and limiting of delta
    let correction  = delta/this.targetDelta;

    // time correction for time scaling
    if (this.timeScalePrev !== 0) {
      correction *= Math.min(3.0, this.engine.timing.timeScale / this.timeScalePrev); // Need to throttle
      //correction *= this.engine.timing.timeScale / this.timeScalePrev
    }

    if (this.engine.timing.timeScale === 0) {
      correction = 0;
    }

    this.timeScalePrev = this.engine.timing.timeScale;

    Engine.update(this.engine, delta, correction); // 60 fps 16.666*dt

    // this.render.world(this.engine.world);
    for (let i = 0; i < this.renders.length; i++){
      this.renders[i].drawRender(this.engine.world);
    }

    if (this.renderCallback){
      this.renderCallback(ticker.elapsedMS);
    }

  }

  /**
   * Clean up method, must be called manually.
   */
  dispose(){

    this.pauseTick();

    this.deltaHistory = null;
    this.engine = null;

    //this.render.dispose();
    for (var i = 0; i < this.renders.length; i++) {
      this.renders[i] = null;
    }
    this.renders = null;

  }

}

// JRender
// -------

//
//


/**
 * Handles the relationship between `PIXI` display objects and `Matter.js` physics objects and engine.
 * <br>- As a rule `PIXI` always syncs to matter.js, rather than the other way around.
 * @memberof physics
 * @example
// Create jrender instance:
this.jrender = new physics.JRender({ptsPerMeterFactor:0.5});
 */
class JRender {

  /**
   * JRender config options.
   * @typedef {Object} physics.JRender.Options
   * @property {number} [ptsPerMeterFactor=1.0] Art points to `matter.js` meters factor. This will affect how heavy / light the simulation will feel.
   * @memberOf physics.JRunner
   */

  /**
   * @param {physics.JRender.Options} [options=null] - Configuration object.
   * @constructor
   */
  constructor(options){

    let defaults = {
      ptsPerMeterFactor: 1.0
    };

    options = utils.extend(defaults, options);

    /**
     * Points to meters: Multiply by this to translate (art) pt to `matter.js` units (in meters).
     * <br>- This property is used by the conversion methods.
     * @type {number}
     * @public
     */
    this.ptm = options.ptsPerMeterFactor;

    /**
     * Meters to points: Multiply by this to translate `matter.js` units (in meters) to (art) pts.
     * <br>- This property is used by the conversion methods.
     * @type {number}
     * @public
     */
    this.mtp = 1.0/options.ptsPerMeterFactor;

    this.valueModifiers = {x:0.0,y:0.0}; // This only affects links

    /**
     * Lookup of `JSyncLink` instances.
     * <br>- Each link will be referenced by its label.
     * @type {Object}
     * @public
     */
    this.links = {};

  }

  /**
   * Static method that converts physics engine coordinate space meters measurement to PSD art pts.
   * @param {number} physMtrs - Physics engine meters.
   * @param {number} ptsPerMeterFactor - Art points to `matter.js` meters factor.
   * @returns {number} artPts - PSD art pts.
   * @private
   */
  static _physMtrsToArtPts(physMtrs, ptsPerMeterFactor){
    const mtp = 1.0/ptsPerMeterFactor;
    return mtp*physMtrs;
  }

  // Tools

  /**
   * Converts PSD art pts measurement to the physics engine coordinate space meters.
   * @param {number} artPts - PSD art pts.
   * @returns {number} physMtrs - Physics engine meters.
   */
  artPtsToPhysMtrs(artPts){
    return this.ptm*artPts;
  }

  /**
   * Converts physics engine coordinate space meters measurement to PSD art pts.
   * @param {number} physMtrs - Physics engine meters.
   * @returns {number} artPts - PSD art pts.
   */
  physMtrsToArtPts(physMtrs){
    return this.mtp*physMtrs;
  }

  /**
   * Convert screen x pt position to physics engine coordinate space meters.
   * <br>- This will take into account the offset of the projected artboard.
   * <br>- Default scaler projection assumed.
   * @param {number} screenX - Screen x position in pts (css).
   * @returns {number} physMtrsX - Physics engine meters.
   */
  screenXToPhysMtrs(screenX){
    return scaler.proj.default.transScreenX(screenX)*this.ptm;
  }

  /**
   * Convert screen y pt position to physics engine coordinate space meters.
   * <br>- This will take into account the offset of the projected artboard.
   * <br>- Default scaler projection assumed.
   * @param {number} screenY - Screen y position in pts (css).
   * @returns {number} physMtrsY - Physics engine meters.
   */
  screenYToPhysMtrs(screenY){
    return scaler.proj.default.transScreenY(screenY)*this.ptm;
  }

  /**
   * Convert screen pts (css) measurement to physics engine coordinate space meters.
   * <br>- Default scaler projection assumed.
   * @param {number} screenPts - Screen pt measurement (css).
   * @returns {number} physMtrs - Physics engine meters.
   */
  screenPtsToPhysMtrs(screenPts){
    return screenPts*(1.0/scaler.scale)*this.ptm;
  }

  /**
   * Convert physics engine coordinate space meters measurement to screen pts (css).
   * @param {number} physMtrs - Physics engine meters.
   * @returns {number} screenPts - Screen pt measurement (css).
   */
  physMtrsToScreenPts(physMtrs){
    return physMtrs*this.mtp*scaler.scale
  }

  // ---

  /**
   * A `matter.js` object defining properties to be used to create a physics body.
   * @typedef {Object} JRender.BodyDef
   * @property {number} width - Width.
   * @property {number} height - Height.
   * @property {number} cx - Center x.
   * @property {number} cy - Center y.
   * @memberOf physics
   */

  /**
   * Converts a PIXI display object to a `matter.js` body definition to be used to create a physics body.
   * @param {PIXI.DisplayObject} dispo - Display Object. Center registration is assumed.
   * @returns {JRender.BodyDef} bodyDef - Body definition.
   */
  dispoToBodyDef(dispo){

    let def = {};

    def.width = this.screenPtsToPhysMtrs(dispo.width);
    def.height = this.screenPtsToPhysMtrs(dispo.height);

    if (dispo.txInfo){ // Consider reg perc
      def.cx = ((this.screenXToPhysMtrs(dispo.x) - def.width*dispo.txInfo.regPercX) + def.width*0.5)
      def.cy = ((this.screenYToPhysMtrs(dispo.y) - def.height*dispo.txInfo.regPercY) + def.height*0.5)
    } else { // Assume center reg
      def.cx = this.screenXToPhysMtrs(dispo.x);
      def.cy = this.screenYToPhysMtrs(dispo.y);
    }

    return def;

  }

  /**
   * Convert `storymode` `txInfo` (or string `txPath`) to a `matter.js` body definition.
   * <br>- Will throw an error if texture path not found.
   * @param {string|Object} txInfo - Texture info path (eg. `myart.psd/mysprite`) or object.
   * @returns {JRender.BodyDef} bodyDef - Body definition.
   */
  txInfoToBodyDef(txInfo){

    if (typeof txInfo === 'string'){
      txInfo = ui.txInfo[txInfo];
      if (!txInfo){
        throw new Error('JRender: Texture path not found `'+txInfo+'`');
      }
    }

    let def = {};
    def.cx = ((txInfo.x - txInfo.width*txInfo.regPercX) + txInfo.width*0.5) * this.ptm;
    def.cy = ((txInfo.y - txInfo.height*txInfo.regPercY) + txInfo.height*0.5) * this.ptm;
    def.width = txInfo.width * this.ptm;
    def.height = txInfo.height * this.ptm;

    return def;

  }

  /**
   * Convert PIXI.Rectangle to a `matter.js` body definition.
   * @param {PIXI.Rectangle} rectangle
   * @returns {JRender.BodyDef} bodyDef - Body definition.
   */
  rectToBodyDef(rect){
    let def = {};
    def.width = this.screenPtsToPhysMtrs(rect.width);
    def.height = this.screenPtsToPhysMtrs(rect.height);
    def.cx = this.screenXToPhysMtrs(rect.x) + def.width*0.5;
    def.cy = this.screenYToPhysMtrs(rect.y) + def.width*0.5;
    return def;
  }

  /**
   * Called by `JRunner` once every tick, after physics engine has updated.
   * <br>- Each link will be synced to the new physics engine state.
   * @param {PIXI.Rectangle} rectangle
   */
  drawRender(world){

    let link;
    let pxScale = 1.0/scaler.artboardScaleFactor;
    for (let label in this.links){
      this.applySync(label);
    }
  }

  /**
   * Syncs the link to the physics engine state.
   * @param {string} label - Label associated with link.
   * @param {boolean} [label=false] - If true will update the link regardless of its `syncEnabled` state.
   */
  applySync(label, force = false){
    const link = this.links[label];

    if (link.syncEnabled || force){
      if (link.syncProps.x){
        // Assumes dispo reg is 0.5,0.5
        link.to.x = scaler.proj.default.transArtX(this.mtp*(link.from.position.x+link.valueModifiers.x + this.valueModifiers.x)) // Convert from matter.js meters to pts
      }
      if (link.syncProps.y){
        // Assumes dispo reg is 0.5,0.5
        link.to.y = scaler.proj.default.transArtY(this.mtp*(link.from.position.y+link.valueModifiers.y + this.valueModifiers.y));
      }
      if (link.syncProps.rotation){
        link.to.rotation = link.from.angle + mutils.degToRad(link.valueModifiers.rotation);
      }
      if (link.syncProps.scale){
        // Assumes uniform density assets. Consider using: let resetingScale = dispo.txInfo.pxtopt*scaler.scale;
        let s = scaler.scale*(1.0/scaler.artboardScaleFactor)*link.from._scale*link.valueModifiers.scale;
        link.to.scale.set(s,s);
      }
      if (link.syncCallback){
        link.syncCallback(link);
      }
    }
  }

  /**
   * Returns the appropriate HMTL element to supply to the constructor of the `matter.js` Mouse.
   * @readonly
   * @type {!DOMElement}
   * @example
this.mouse = Mouse.create(this.jrender.mouseElement);
   */
  get mouseElement(){
    return pixiApp.resizeTo == window ? htmlEle : pixiApp.resizeTo; //pixiApp.resizeTo; // document.body
  }

  /**
   * Adjusts a `matter.js` mouse instance to match the current stage dimensions.
   * @param {Matter.Mouse} mouse - Mouse instance.
   */
  adjustMouseToStage(mouse){
    Mouse.setOffset(mouse, {x:this.ptm*(-scaler.proj.default.topLeft.x*(1.0/scaler.scale)), y:this.ptm*(-scaler.proj.default.topLeft.y*(1.0/scaler.scale))}); //scaler.proj.default.transArtY(0.0)
    Mouse.setScale(mouse, {x:this.ptm*(1.0/scaler.scale), y:this.ptm*(1.0/scaler.scale)})
  }

  // - Sync props: x,y,rotation(in radians),scale

 /**
  * A config object indicating which properties to sync.
  * <br>-If all syncProps are false (or not properties defined), the remaining are assumed true
  * <br>-If any sync props are true, the remaining are assumed false.
  * @typedef {Object} JRender.SyncProps
  * @property {boolean} x - Sync x position.
  * @property {boolean} y - Sync y position.
  * @property {boolean} rotation - Sync rotation.
  * @property {boolean} scale - Sync scale.
  * @memberOf physics
  */

 /**
  * Adjust the values being synced to the display object.
  * @typedef {Object} JRender.SyncValueModifiers
  * @property {number} [x=0] - X value to be added to the sync value (in physics meters).
  * @property {number} [y=0] - Y value to be added to the sync value (in physics meters).
  * @property {number} [rotatio=0] - Rotation to be added to the sync value (in radians).
  * @property {number} [scale=1.0] - Scale to be multiplied by the sync value.
  * @memberOf physics
  */

  /**
   * An optional callback fired at the end of each link sync.
   * @callback JRender.SyncCallback
   * @param {physics.JSyncLink} link - The link being synced.
   * @memberOf physics
   */

  /**
   * Creates a new `JSyncLink` and adds to the JRender's `links`.
   * @param {string} label - Label for the link.
   * @param {Matter.Body} from - Physics body (the leader).
   * @param {PIXI.DisplayObject} to - Display object (the follower).
   * @param {JRender.SyncProps} [syncProps=null] - Which properties to sync.
   * @param {JRender.SyncValueModifiers} [valueModifiers=null] - Sync value modfiers.
   * @param {JRender.SyncCallback} [syncCallback=null] - Optional function to call after each sync.
   * @returns {physics.JSyncLink} link - The newly created link object.
   */
  createSyncLink(label, from, to, syncProps = null, valueModifiers = null, syncCallback = null){

    if (!from.id || !from.type || from.type !== 'body'){
      throw new Error('Jrender: Target must be a physics body');
    }
    from._scale = typeof from._scale !== 'undefined' ? from._scale : 1.0; // Track scale

    syncProps = syncProps ? syncProps : {};

    // If all syncProps are false, the remaining are assumed true
    // If any sync props are true, the remaining are assumed false.
    let syncPropDefault = true
    for (let p in syncProps){
      if (syncProps[p] === true){
        syncPropDefault = false;
        break;
      }
    }

    syncProps = utils.extend({x:syncPropDefault,y:syncPropDefault,rotation:syncPropDefault,scale:syncPropDefault}, syncProps);
    valueModifiers = utils.extend({x:0.0,y:0.0,rotation:0.0, scale:1.0}, valueModifiers); // Note these need to be relative to metter / meters
    let link = new JSyncLink(from, to, syncProps, valueModifiers, syncCallback);
    this.links[label] = link;
    return link;

  }

  /**
   * Adds a sync link to the JRender's `links`.
   * <br>- This is automatically performed by the `createSyncLink()` method.
   * @param {string} label - Label for the link.
   * @param {physics.JSyncLink} link - The link to added.
   * @returns {physics.JSyncLink} link - The newly added link object.
   */
  addSyncLink(label, link){
    this.links[label] = link;
    return this.links[label];
  }

  /**
   * Removes a sync link from JRender's `links`.
   * @param {string} label - Label for the link.
   * @param {boolean} [dispose=true] - Whether to destroy the link.
   * @returns {physics.JSyncLink} link - The removed link.
   */
  removeSyncLink(label, dispose = true){
    if (!this.links[label]){
      return null;
    }
    const link = this.links[label]
    if (dispose){
      this.links[label].dispose();
    }
    this.links[label] = null
    delete this.links[label];
    return link;
  }

  dispose(){
    for (let label in this.links){
      this.links[label].dispose();
      this.links[label] = null
    }
    this.links = null;
  }

}

// JLink
// -----

// Represents a link between PIXI and matterjs
// - Just a light class with refs. Calculations are done on the render.

/**
 * Represents a relationship between a display object and a `matter.js` physics body.
 * <br>- Primarily used for storage.
 * <br>- Created, stored and managed in the `JRender` Class.
 * @memberof physics
 * @example
this.jrender.link.ball_0.autoSync = false;
this.jrender.link.ball_0.from.isStatic = true;
 */
class JSyncLink {

  /**
  * This constructor is handled by `JRender.addSyncLink()` in most cases.
  * @param {Matter.Body} from - Physics body (the leader).
  * @param {PIXI.DisplayObject} to - Display object (the follower).
  * @param {JRender.SyncProps} [syncProps=null] - Which properties to sync.
  * @param {JRender.SyncValueModifiers} [valueModifiers=null] - Sync value modfiers.
  * @param {JRender.SyncCallback} [syncCallback=null] - Optional function to call after each sync.
  * @constructor
  */
  constructor(from, to, syncProps, valueModifiers, syncCallback = null) {

    /**
     * Physics body (the leader).
     * @type {Matter.Body}
     * @public
     */
    this.from = from;

    /**
     * Display object (the follower).
     * @type {PIXI.DisplayObject}
     * @public
     */
    this.to = to;

    /**
     * Whether syncing is enabled. Default is true.
     * @type {boolean}
     * @public
     */
    this.syncEnabled = true;

    /**
     * Which properties to sync.
     * @type {JRender.SyncProps}
     * @public
     */
    this.syncProps = syncProps;

    /**
     * Sync value modfiers.
     * @type {JRender.SyncValueModifiers}
     * @public
     */
    this.valueModifiers = valueModifiers;

    /**
     * An object where any additional data can be defined for later reference.
     * @type {Object}
     * @public
     */
    this.data = {}

    /**
     * Optional function to call after each sync.
     * @type {JRender.SyncCallback}
     * @public
     */
    this.syncCallback = syncCallback;

  }

  /**
  * Manually removes data from the sync link.
  * <br>- This method is called by `JRender` when it is manually disposed.
  */
  dispose(){
    if (this.data){
      for (let p in this.data){
        this.data[p] = null;
        delete this.data[p];
      }
      this.data = null;
    }
    this.from = null;
    this.to = null;
    this.syncCallback = null;
  }
}


// JWireframeRender
// ----------------

/**
 * JWireframeRender config options. *
 * @typedef {Object} physics.JWireframeRender.Options
 * @property {number} [lineThickness=3.0] - The line thickness for the render, in screen pts (css).
 * @property {number} [ptsPerMeterFactor=1.0] - Art points to `matter.js` meters factor. Should match that of the main JRender.
 * @memberOf physics.JWireframeRender
 */


/**
 * A wireframe renderer to be used in development environments.
 * <br>- Will render bodies, springs, pins and mouse.
 * <br>- Needs to be added to the `JRunner` list of renders.
 * @extends PIXI.Graphics
 * @memberof physics
 * @example
didLoad(ev){

 // ...

 this.jrender = ...
 this.jrunner = ...
 this.mouse = ...

 if (true){
   let jwireframeRender =  new physics.JWireframeRender(this.mouse, {lineThickness:1.0, ptsPerMeterFactor: this.jrender.ptm});
   this.addChild(jwireframeRender); // Should be auto disposed on scene dispose
   this.jrunner.renders.push(jwireframeRender); // Attach to runner.render list
 }

 this.ready();

}
 */
class JWireframeRender extends PIXI.Graphics {

  /**
   * @param {Matter.Mouse} mouse - `Matter.js` mouse instance.
   * @param {physics.JWireframeRender.Options} [options=null] - Configuration object.
   * @constructor
   */
  constructor(mouse, options){

    super();

    let defaults = {
      lineThickness: 3.0,
      ptsPerMeterFactor: 1.0
    };
    options = utils.extend(defaults, options);

    this.mouse = mouse;
    this.lineThickness = options.lineThickness;

    this.ptm = options.ptsPerMeterFactor; // points to meter: Multiply by this to translate (art) pt to matter.js units (in meters)
    this.mtp = 1.0/options.ptsPerMeterFactor; // meters to points: Multiply by this to translate matter.js units (in meters) to (art) pts

    this.onStageDimChange();
    this.on('removed',  this.dispose); // Auto dispose

  }

  onStageDimChange(){

    this._stageW = scaler.stageW;
    this._stageH = scaler.stageH;

    this.scale.set(scaler.scale*this.mtp);
    this.x = scaler.proj.default.topLeft.x; // scaler.proj.default.transArtX(0.0)
    this.y = scaler.proj.default.topLeft.y; // scaler.proj.default.transArtY(0.0)

  }

  // Entry point to render
  drawRender(world){

    // https://github.com/liabru/matter-js/blob/master/src/render/Render.js Line 319
    // https://pixijs.download/dev/docs/PIXI.Graphics.html

    if (this._stageW !== scaler.stageW || this._stageH !== scaler.stageH){
      // Detect stage change
      this.onStageDimChange();
    }

    this.clear();

    this.bodies(Composite.allBodies(world))
    this.constraints(Composite.allConstraints(world));

    if (this.mouse){
      this.mousePosition(this.mouse)
    }

  }

  bodies(bodies){

    this.lineStyle(this.lineThickness/this.scale.x, body.isSensor ? 0x00ffff : 0xffffff, body.isSleeping ? 0.5 : 1.0);

    let part, k;

    for (let body of bodies){

      for (k = body.parts.length > 1 ? 1 : 0; k < body.parts.length; k++) {

        part = body.parts[k];

        if (part.circleRadius) {

          this.drawCircle(part.position.x, part.position.y, body.circleRadius);
          this.moveTo(part.position.x, part.position.y);
          this.lineTo(part.position.x + part.circleRadius*Math.cos(body.angle), body.position.y + body.circleRadius*Math.sin(body.angle));

        } else {

          if (part.vertices.length > 0){
            this.moveTo(part.vertices[0].x, part.vertices[0].y);
            for (let v of part.vertices){
              this.lineTo(v.x, v.y);
            }
            this.closePath();
          }
        }
      }
    }

  }

  // https://github.com/liabru/matter-js/blob/master/src/render/Render.js#L1
  // Line 633
  constraints(constraints){

    for (var i = 0; i < constraints.length; i++) {

      var constraint = constraints[i];

      if (!constraint.pointA || !constraint.pointB){
        continue;
      }

      var bodyA = constraint.bodyA,
        bodyB = constraint.bodyB,
        start,
        end;

      if (bodyA) {
        start = {x:bodyA.position.x+constraint.pointA.x,y:bodyA.position.y+constraint.pointA.y}
      } else {
        start = constraint.pointA;
      }

      this.lineStyle(this.lineThickness/this.scale.x, 0x1b8ce3, 1.0);

      if (constraint.render.type === 'pin') {
          this.drawCircle(start.x, start.y, body.circleRadius);
      } else {

          if (bodyB) {
              end = {x:bodyB.position.x+constraint.pointB.x,y:bodyB.position.y+constraint.pointB.y}
          } else {
              end = constraint.pointB;
          }

          this.moveTo(start.x, start.y);

          if (constraint.render.type === 'spring' && false) {

              let delta = {x:end.x-start.x,y:end.y-start.y},
                  normal = this._vectorPerp(this._vectorNormalise(delta)),
                  coils = Math.ceil(this._commonClamp(constraint.length / 5, 12, 20)),
                  offset;

              for (let j = 1; j < coils; j += 1) {
                  offset = j % 2 === 0 ? 1 : -1;

                  this.lineTo(
                      start.x + delta.x * (j / coils) + normal.x * offset * this.ptm*(4.0),
                      start.y + delta.y * (j / coils) + normal.y * offset * this.ptm*(4.0)
                  );
              }
          }

          this.lineTo(end.x, end.y);

      }

      if (true) {

        this.lineStyle(this.lineThickness/this.scale.x, 0x1b8ce3, 1.0);
        this.drawCircle(start.x, start.y, this.ptm*(5.0));
        this.drawCircle(end.x, end.y, this.ptm*(5.0));

      }
    }

  }

  mousePosition(mouse){

    this.lineStyle(this.lineThickness/this.scale.x, 0x000000, 1.0);
    this.drawCircle(mouse.position.x, mouse.position.y, this.ptm*(5.0));
    this.drawCircle(mouse.position.x, mouse.position.y, this.ptm*(2.5));

  }

  // https://github.com/liabru/matter-js/blob/master/src/geometry/Vector.js

  _vectorNormalise(vector) {
      let magnitude = this._vectorMagnitude(vector);
      if (magnitude === 0) {
        return { x: 0, y: 0 };
      }
      return { x: vector.x / magnitude, y: vector.y / magnitude };
  };

  _vectorMagnitude(vector) {
    return Math.sqrt((vector.x * vector.x) + (vector.y * vector.y));
  };

  _vectorPerp(vector, negate) {
    negate = negate === true ? -1 : 1;
    return { x: negate * -vector.y, y: negate * vector.x };
  };

  _commonClamp(value, min, max) {
    if (value < min){
        return min;
    }
    if (value > max){
        return max;
    }
    return value;
  };

  dispose(){
    this.off('removed',  this._dispose);
    this.mouse = null;
    this.clear();
    // this.destroy(); // Already being called by scene.destroy chldren:true
  }

}


// Extensions
// ----------


/**
 * This class acts as a proxy for `gsap` to animate basic properties of `matter.js` bodies and composites.
 * @hideconstructor
 * @alias physics.jgsap
 * @memberof physics
 * @example
physics.jgsap.to(this.box, 2.0, {scale:0.5, x:this.jrender.screenXToPhysMtrs(this.art.ball_3.x),y:this.jrender.screenYToPhysMtrs(this.art.ball_3.y), rotation:180.0, ease:Back.easeInOut, yoyo:true, repeat:-1});
 */
class Jgsap {

  constructor(){
   this.c = 0;
   this.tweens = {};
   this.delays = {};
   this._tweenBind = this._tween.bind(this)
  }

  /**
   * Replacement for `gsap.fromTo` for targeting `Matter.js` bodies, with the same arguments.
   * @param {Matter.Body|Matter.Composite} target - The body or composite to animate.
   * @param {number} dur - Duration, in seconds.
   * @param {Object} twFrom - From gsap tween properties.
   * @param {Object} twTo - To gsap tween properties.
   */
  fromTo(target, dur, twFrom, twTo){
    this._tween(target, dur, twFrom, twTo);
  }

  /**
   * Replacement for `gsap.from` for targeting `Matter.js` bodies, with the same arguments.
   * @param {Matter.Body|Matter.Composite} target - The body or composite to animate.
   * @param {number} dur - Duration, in seconds.
   * @param {Object} tw - From gsap tween properties.
   */
  from(target, dur, tw){
    if (tw.delay && tw.delay > 0.0){
      let delay = tw.delay;
      tw.delay = 0.0;
      let delayID = this.generateID();
      let delayedCall = gsap.delayedCall(delay, this._tweenBind, [target, dur, tw, null, delayID]);
      if (!this.delays[target.id]){
        this.delays[target.id] = {};
      }
      this.delays[target.id][delayID] = delayedCall;
    } else {
      this._tween(target, dur, tw, null);
    }
  }

  /**
   * Replacement for `gsap.to` for targeting `Matter.js` bodies, with the same arguments.
   * @param {Matter.Body|Matter.Composite} target - The body or composite to animate.
   * @param {number} dur - Duration, in seconds.
   * @param {Object} tw - To gsap tween properties.
   */
  to(target, dur, tw){
    if (tw.delay && tw.delay > 0.0){
      let delay = tw.delay;
      tw.delay = 0.0;
      let delayID = this.generateID();
      let delayedCall = gsap.delayedCall(delay, this._tweenBind, [target, dur, null, tw, delayID]);
      if (!this.delays[target.id]){
        this.delays[target.id] = {};
      }
      this.delays[target.id][delayID] = delayedCall;
    } else {
      this._tween(target, dur, null, tw);
    }
  }

  /**
   * Creates a new ID for each tween.
   * @private
   */
  generateID(){
    this.c++;
    return this.c;
  }

  /**
   * Internal method that performs the tween, registering listeners to apply the changes to the physics body.
   * @private
   */
  _tween(target, dur, twFrom, twTo, delayID = null){

    if (!target.id || !target.type){
      throw new Error('jsap: Target must be a physics body');
    } else if (!(target.type == 'body' || target.type == 'composite')){
      throw new Error('jsap: Unsupported target');
    }

    let cmd = 'fromTo';
    let tw;
    if (!twFrom){
      cmd = 'to'
      tw = twTo;
    } else if (!twTo){
      cmd = 'from'
      tw = twFrom;
    }

    if (delayID){
      if (this.delays[target.id][delayID]){
        this.delays[target.id][delayID].kill();
      }
      delete this.delays[target.id][delayID];
    }

    // Create a temporary prop object to tween
    if (!this.tweens[target.id]){
      this.tweens[target.id] = {};
    }

    let twProps = {};
    twProps._syncProps = {}; // Track which props to update

    let twID = this.generateID();
    this.tweens[target.id][twID] = twProps

    // Assign callbacks
    let _tw = (cmd === 'fromTo') ? twTo : tw;
    _tw.onUpdateParams = [target, twID, _tw.onUpdate, _tw.onUpdateParams]
    _tw.onUpdate = this.onTwUpdate.bind(this);
    _tw.onCompleteParams = [target, twID, _tw.onComplete, _tw.onCompleteParams]
    _tw.onComplete = this.onTwComplete.bind(this);

    // Record starting value
    if (cmd === 'fromTo'){

      if (target.type == 'composite'){
        throw new Error('jsap: Composite tweens can only be relative');
      }

      // Set starting values

      if ('x' in twFrom){
        twProps._syncProps.x = true;
        twProps.x = twFrom.x
      }
      if ('y' in twFrom){
        twProps._syncProps.y = true;
        ttwProps.y = twFrom.y
      }

      if ('rotation' in twFrom){
        twProps._syncProps.rotation = true;
        twFrom.rotation = mutils.degToRad(twFrom.rotation);
        twTo.rotation = mutils.degToRad(twTo.rotation);
        twProps.rotation = twFrom.rotation;
      }

      gsap.fromTo(twProps, dur, twFrom, twTo);

    } else {

      // Set starting values - only when tween starts
      if ('x' in tw){
        twProps._syncProps.x = true;
        if (target.type == 'composite'){
          twProps.x = 0.0
          twProps._x = 0.0
        } else {
          twProps.x = target.position.x;
        }
      }

      if ('y' in tw){
        twProps._syncProps.y = true;
        if (target.type == 'composite'){
          twProps.y = 0.0
          twProps._y = 0.0
        } else {
          twProps.y = target.position.y;
        }
      }

      if ('rotation' in tw){
        twProps._syncProps.rotation = true;
        if (target.type == 'composite'){
          if (tw._rotationOrigin){
            let _rotationOrigin = tw._rotationOrigin;
            delete tw._rotationOrigin;
            twProps._rotationOrigin = _rotationOrigin;
          } else {
            twProps._rotationOrigin = {x:0.0,y:0.0}
          }
          twProps.rotation = 0.0;
          twProps._rotation = 0.0; // Previous value
        } else {
          twProps.rotation = target.angle;
        }
        tw.rotation = mutils.degToRad(tw.rotation); // Convert from degs to radians
      }

      if ('scale' in tw){

        twProps._syncProps.scale = true;
        target._scale = typeof target._scale !== 'undefined' ? target._scale : 1.0; // Track scale
        twProps.scale = target._scale;
        twProps._scale = twProps.scale;

        if (target.type == 'composite'){
          if (tw._scaleOrigin){
            let _scaleOrigin = tw._scaleOrigin;
            delete tw._scaleOrigin;
            twProps._scaleOrigin = _scaleOrigin;
          } else {
            twProps._scaleOrigin = {x:0.0,y:0.0}
          }
          twProps._compBodies = Matter.Composite.allBodies(target); // Store these bodies
          for (let b of twProps._compBodies){
            b._scale = typeof b._scale !== 'undefined' ? b._scale : 1.0; // Track scale
          }
        }


      }

      gsap[cmd](twProps, dur, tw);

    }
  }


  /**
   * Internal method receiving update events.
   * @private
   */
  onTwUpdate(target, twID, _onUpdate = null, _onUpdateParams = null){

    let twProps = this.tweens[target.id][twID];

    if (twProps._syncProps.x || twProps._syncProps.y){

      if (target.type == 'composite'){

        Composite.translate(target, {
          x: twProps._syncProps.x ? twProps.x-twProps._x : 0.0,
          y: twProps._syncProps.y ? twProps.y-twProps._y : 0.0,
        }, false);

        // Store previous values;
        if (twProps._syncProps.x){
          twProps._x = twProps.x;
        }
        if (twProps._syncProps.y){
          twProps._y = twProps.y;
        }

      } else {

        let _x = twProps._syncProps.x ? twProps.x : target.position.x;
        let _y = twProps._syncProps.y ? twProps.y : target.position.y;
        Body.setPosition(target, {x:_x, y:_y});

      }
    }

    if (twProps._syncProps.rotation){
      if (target.type == 'composite'){
        Matter.Composite.rotate(target, twProps.rotation-twProps._rotation, twProps._scaleOrigin);
        twProps._rotation = twProps.rotation;
      } else {
        Body.setAngle(target, twProps.rotation);
      }
    }

    if (twProps._syncProps.scale){
      let s = twProps.scale / Math.max(0.00001, twProps._scale);
      if (target.type == 'composite'){
        Matter.Composite.scale(target, s, s, twProps._scaleOrigin);
        for (let b of twProps._compBodies){
          b._scale = twProps._scale; // Track scale
        }
      } else {
        Body.scale(target, s, s); // Apply *relative* scale
      }

      target._scale = twProps.scale; // Keep this prop up to date
      twProps._scale = twProps.scale
    }

    if (_onUpdate){
      _onUpdate.apply(null, _onUpdateParams);
    }

  }

  /**
   * Internal method receiving complete events.
   * @private
   */
  onTwComplete(target, twID, _onComplete = null, _onCompleteParams = null){

    if (!target){
      return;
    }

    // Remove reference to this tween
    if (this.tweens[target.id][twID]._compBodies){
      this.tweens[target.id][twID]._compBodies = null;
      delete this.tweens[target.id][twID]._compBodies
    }
    delete this.tweens[target.id][twID];

    let anyTweens = (this.tweens[target.id] && Object.keys(this.tweens[target.id]).length > 0) || (this.delays[target.id] && Object.keys(this.delays[target.id]).length > 0); // Delays may not have ref to target if it never had a delay
    if (!anyTweens){
      this.killTweensOf(target); // Kill all associated with physics body
    }

    if (_onComplete){
      _onComplete.apply(null, _onCompleteParams);
    }

  }

  /**
   * Removes all pending and current animations and delayed calls.
   */
  killAll(){
    gsap.killTweensOf(this._tweenBind); // Removes all pending delayed calls
    for (var targetID in this.tweens){
      for (let twID in this.tweens[targetID]){
        if (this.tweens[targetID][twID]._compBodies){
          this.tweens[targetID][twID]._compBodies = null;
          delete this.tweens[targetID][twID]._compBodies
        }
        gsap.killTweensOf(this.tweens[targetID][twID]);
      }
    }
    this.tweens = {};
  }

  /**
   * Removes all pending and current animations and delayed calls for the given physics body or composite.
   * @param {Matter.Body|Matter.Composite} target - The body or composite to target.
   */
  killTweensOf(target){

    if (this.delays[target.id]){
      for (let delayID in this.delays[target.id]){
        this.delays[target.id][delayID].kill();
      }
    }
    delete this.delays[target.id];

    if (this.tweens[target.id]){
      for (let twID in this.tweens[target.id]){
        if (this.tweens[target.id][twID]._compBodies){
          this.tweens[target.id][twID]._compBodies = null;
          delete this.tweens[target.id][twID]._compBodies
        }
        gsap.killTweensOf(this.tweens[target.id][twID]);
      }
    }
    delete this.tweens[target.id];
  }

}

let jgsap = new Jgsap();

// Body and Composite - tracking position and scale attributes

export default { JRunner, JRender, JWireframeRender, jgsap}