larrybotha
11/25/2016 - 11:25 AM

Two.js Renderer for PhysicsJS

Two.js Renderer for PhysicsJS

// has a dependency on PhysicsJS and Two.js

Physics.renderer('Two', function(parent) {
  if (!document) {
    // must be in node environment
    return {};
  }

  const metaStyles = {
    alignment: 'left',
    size: 12,
  };
  const defaults = {
    metaEl: null,
    offset: { x: 0, y: 0 },

    styles: {
      'point': {},

      'circle' : {},

      'rectangle' : {},

      'convex-polygon' : {}
    }
  };

  return {
    // extended
    init(options) {
      const self = this;
      let el;

      if (typeof Two === 'undefined') {
        throw "Two.js not present - cannot continue";
      }

      parent.init.call(this, options);

      this.options.defaults(defaults, true);
      this.options.onChange(function() {
        self.options.offset = new Physics.vector(self.options.offset);
      });
      this.options(options, true);

      this.two = new Two({
        width: options.width,
        height: options.height,
        type: options.type || Two.Types.svg,
      });

      this.meta = {};

      this.two.appendTo(this.el || document.body);

      if (this.options.autoResize) {
        this.resize();
      } else {
        this.resize(this.options.width, this.options.height);
      }
    },

    createView(geometry, styles, parent) {
      const aabb = geometry.aabb();
      const hw = aabb.hw + Math.abs(aabb.x);
      const hh = aabb.hh + Math.abs(aabb.y);
      const name = geometry.name;
      let view = null;

      styles = styles || this.options.styles[name] || this.options.styles.circle || {};

      if (name === 'circle') {
        view = this.createCircle(0, 0, geometry.radius, styles);
      } else if (name === 'rectangle') {
        view = this.createRect(-geometry.width/2, -geometry.height/2, geometry.width, geometry.height, styles);
      } else if (name === 'convex-polygon') {
        view = this.createPolygon(geometry.vertices, geometry.options.pathParams, styles);
      } else if (name === 'compound') {
        view = this.createCompound(geometry.children, styles);
      } else {
        view = this.createCircle(0, 0, 1, styles);
      }

      if (parent) parent.add(view);

      return view;
    },

    // extended
    resize(width, height) {
      parent.resize.call(this, width, height);
      this.two.width = width;
      this.two.height= height;
    },

    // extended
    connect(world) {
      world.on('add:body', this.attach, this);
      world.on('remove:body', this.detach, this);
    },

    // extended
    disconnect(world) {
      world.off('add:body', this.attach, this);
      world.off('remove:body', this.detach, this);
    },

    /**
     * TwoRenderer#detach(data) -> this
     * - data (Two.Path|Object): Graphics object or event data (`data.body`)
     *
     * Event callback to detach a child from the stage
     **/
    detach(data) {
      // interpred data as either dom node or event data
      const el = (data instanceof Two.Path && data) || (data.body && data.body.view);

      if (el) this.two.remove(el);

      return this;
    },

    /**
     * TwoRenderer#attach(data) -> this
     * - data (Two.Path|Object): Graphics object or event data (`data.body`)
     *
     * Event callback to attach a child to the stage
     **/
    attach(data) {
      // interpred data as either dom node or event data
      const el = (data instanceof Two.Path && data) || (data.body && data.body.view);

      if (el) this.two.add(el);

      return this;
    },

    /**
     * TwoRenderer#drawBody(body, view)
     * - body (Body): The body to draw
     * - view (DisplayObject): The pixi display object
     *
     * Draw a Two.js path
     **/
    drawBody(body, view) {
      const pos = body.state.pos;
      const v = body.state.vel;
      const os = body.offset;
      const t = this._interpolateTime || 0;
      let x;
      let y;
      let ang;

      // interpolate positions
      x = pos._[0] + v._[0] * t;
      y = pos._[1] + v._[1] * t;
      ang = body.state.angular.pos + body.state.angular.vel * t;

      view.translation.set(pos.x, pos.y);
      view.rotation = ang;
    },

    // extended
    render(bodies, meta) {
      parent.render.call(this, bodies, meta);

      if (!this.two.width) {
        this.two.width = this.options.width;
        this.two.height = this.options.height;
      }

      this.two.update();
    },

    /**
     * TwoRenderer#setStyles(path, styles) -> Two.Path
     * - path (Two.Path): The path object to set styles on
     * - styles (Object): The styles configuration
     * + (Two.Path): A path object
     *
     * Set styles on a Two path
     **/
    setStyles(path, styles) {
      Object.keys(styles).map(key => path[key] = styles[key]);

      return path;
    },


    /**
     * TwoRenderer#createCompound(children) -> Two.Group
     * - children (Array): An array of bodies to be added to a group
     * - styles (Object): An object defining the styles to apply to all children
     * + (Two.Group): A Two.Group object
     *
     * Create a Two.Group where multiple bodies / paths can be added to
     **/
    createCompound(children, styles) {
      const group = this.two.makeGroup();

      children.map(child => {
        const childView = this.createView(child.g, styles, group);
        childView.translation.set(child.pos.x, child.pos.y);
        childView.rotation = child.angle;
      });

      return group;
    },

    /**
     * TwoRenderer#createCircle(x, y, r, styles) -> Two.Circle
     * - x (Number): The x coord
     * - y (Number): The y coord
     * - r (Number): The circle radius
     * - props (Object): The styles configuration
     * + (Two.Path): A graphic object representing a circle.
     *
     * Create a circle for use in PIXI stage
     **/
    createCircle(x, y, r, styles) {
      let circle = this.two.makeCircle(x, y, r);
      circle = this.setStyles(circle, styles);

      return circle;
    },

    /**
     * TwoRenderer#createRect(x, y, width, height, styles) -> Two.Rectangle
     * - x (Number): The x coord
     * - y (Number): The y coord
     * - width (Number): The rectangle width
     * - height (Number): The rectangle height
     * - styles (Object): The styles configuration
     * + (Two.Rectangle): A Two rectangle.
     *
     * Create a rectangle for use in Two.js
     **/
    createRect(x, y, width, height, styles) {
      let rect = this.two.makeRectangle(x, y, width, height);
      rect = this.setStyles(rect, styles);

      return rect;
    },

    /**
     * TwoRenderer#createPolygon(verts, styles) -> Two.Path
     * - verts (Array): Array of [[Vectorish]] vertices
     * - styles (Object): The styles configuration
     * + (Two.Path): A graphic object representing a polygon.
     *
     * Create a polygon for use in PIXI stage
     **/
    createPolygon(verts, pathParams, styles) {
      const anchors = verts.map((vert, i) => {
        const { lx, ly, rx, ry, command } = pathParams.anchors[i];

        return new Two.Anchor(vert.x, vert.y, lx, ly, rx, ry, command);
      });
      const path = this.two.makePath(anchors, pathParams.open);

      this.setStyles(path, styles);

      return path;
    },

    /**
     * TwoRenderer#createLine(from, to, styles) -> Two.Path
     * - from (Vectorish): Starting point
     * - to (Vectorish): Ending point
     * - styles (Object): The styles configuration
     * + (Two.Path): A graphic object representing a polygon.
     *
     * Create a line for use in PIXI stage
     **/
    createLine(from, to, styles) {
      let line = this.two.makeLine(from.x, from.y, to.x, to.y);
      line = this.setStyles(line, styles);

      return line;
    },

    // extended
    drawMeta(meta) {
      if (!this.meta.loaded) {
        const offset = 5;
        this.meta.fps = new Two.Text(
          `FPS: ${meta.fps.toFixed(2)}`, offset,
          metaStyles.size + offset
        );
        this.setStyles(this.meta.fps, metaStyles);

        this.meta.ipf = new Two.Text(
          `IPF: ${meta.ipf}`, offset,
          (metaStyles.size * 2) + offset
        );
        this.setStyles(this.meta.ipf, metaStyles);

        this.two.add(this.meta.fps);
        this.two.add(this.meta.ipf);
        this.meta.loaded = true;
      } else {
        this.meta.fps.value = 'FPS: ' + meta.fps.toFixed(2);
        this.meta.ipf.value = 'IPF: ' + meta.ipf;
      }
    },
  };
});

// extend convex polygon to allow Two.Polygon params to be passed
Physics.body('convex-polygon', function(parent) {
  var defaults = {};

  return {
    // extended
    init: function( options ){
      // call parent init method
      parent.init.call(this, options);

      options = Physics.util.extend({}, defaults, options);

      this.geometry = Physics.geometry('convex-polygon', {
        vertices: options.vertices,
        pathParams: options.pathParams,
      });

      this.recalc();
    },
  };
});

Two.js Renderer for PhysicsJS

A starting point for a Two.js renderer for PhysicsJS.