jonobr1 / two.js

A renderer agnostic two-dimensional drawing api for the web.
https://two.js.org
MIT License
8.31k stars 455 forks source link

[Enhancement] Add Two.Arc #605

Closed jonobr1 closed 2 years ago

jonobr1 commented 2 years ago

Is your feature request related to a problem? Please describe. Add Two.Arc which draws an arc between two points or draws an arc at a specific midpoint with a radius, startAngle, and endAngle.

jonobr1 commented 2 years ago

Pseudo code:

import Two from 'two.js';

export class Arc extends Two.Path {

  _flagRadius = false;
  _flagStartAngle = false;
  _flagEndAngle = false;

  _radius = 0;
  _startAngle = 0;
  _endAngle = 0;

  constructor(x, y, radius, startAngle, endAngle, resolution) {

    if (typeof resolution !== 'number') {
      resolution = Two.Resolution;
    }

    var points = [];
    for (var i = 0; i < resolution; i++) {
      points.push(new Two.Anchor());
    }

    super(points);

    for (var j = 0; j < Arc.Properties.length; j++) {
      var prop = Arc.Properties[j];
      Object.defineProperty(this, prop, protos[prop]);
    }

    this.curved = true;

    if (typeof x === 'number') {
      this.position.x = x;
    }
    if (typeof y === 'number') {
      this.position.y = y;
    }

    if (typeof radius === 'number') {
      this.radius = radius;
    }
    if (typeof startAngle === 'number') {
      this.startAngle = startAngle;
    }
    if (typeof endAngle === 'number') {
      this.endAngle = endAngle;
    }

    this._update();

  }

  static Properties = ['radius', 'startAngle', 'endAngle'];

  _update() {

    if (this._flagVertices || this._flagRadius || this._flagStartAngle
      || this._flagEndAngle) {

      var vertices = this.vertices;
      var radius = this._radius;
      var startAngle = this._flagStartAngle;
      var endAngle = this._endAngle;

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

        var v = vertices[i];
        var pct = i / (vertices.length - 1);
        var theta = pct * (endAngle - startAngle) + startAngle;

        v.x = radius * Math.cos(theta);
        v.y = radius * Math.sin(theta);

      }

    }

    super._update.call(this);

    return this;

  }

  flagReset() {

    super.flagReset.call(this);

    this._flagRadius = this._flagStartAngle = this._flagEndAngle = false;

    return this;

  }

};

var protos = {
  radius: {
    enumerable: true,
    get: function() {
      return this._radius;
    },
    set: function(v) {
      if (v !== this._radius) {
        this._radius = v;
        this._flagRadius = true;
      }
    }
  },
  startAngle: {
    enumerable: true,
    get: function() {
      return this._startAngle;
    },
    set: function(v) {
      if (v !== this._startAngle) {
        this._startAngle = v;
        this._flagStartAngle = true;
      }
    }
  },
  endAngle: {
    enumerable: true,
    get: function() {
      return this._endAngle;
    },
    set: function(v) {
      if (v !== this._endAngle) {
        this._endAngle = v;
        this._flagEndAngle = true;
      }
    }
  }
}
dickinson0718 commented 2 years ago

I like the direction that this going. I have a suggestion to make this a little more general so that it will draw both circular arcs and elliptical arcs. If the Two.arc command had both and x radius and y radius, then the user could draw both (and if the y radius isn't supplied it could default to the x radius). Using your pseudocode, the change appear to be small (maybe 13 lines of code, at least in the pseudocode you posted) and relatively simple.

  _flagRadius = false;
  _flagStartAngle = false;
  _flagEndAngle = false;

  _radiusX = 0; <-------------- NEW
  _radiusY =0; <-------------- NEW
  _startAngle = 0;
  _endAngle = 0;

  constructor(x, y, radiusX, startAngle, endAngle, resolution, radiusY) { <-------------- NEW

    if (typeof resolution !== 'number') {
      resolution = Two.Resolution;
    }

    var points = [];
    for (var i = 0; i < resolution; i++) {
      points.push(new Two.Anchor());
    }

    super(points);

    for (var j = 0; j < Arc.Properties.length; j++) {
      var prop = Arc.Properties[j];
      Object.defineProperty(this, prop, protos[prop]);
    }

    this.curved = true;

    if (typeof x === 'number') {
      this.position.x = x;
    }
    if (typeof y === 'number') {
      this.position.y = y;
    }

    if (typeof radiusX === 'number') { <---------- NEW
      this.radiusX = radiusX; <-------------- NEW
    }

------------ START NEW ------------
   if (typeof radiusY === 'undefined') { 
      this.radiusY = radiusX;                
    } else if ( typeof radiusY === 'number') {
      this.radiusY = radiusY;
    } 
------------ END NEW ------------

    if (typeof startAngle === 'number') {
      this.startAngle = startAngle;
    }
    if (typeof endAngle === 'number') {
      this.endAngle = endAngle;
    }

    this._update();

  }

Then update would look like this

_update() {

    if (this._flagVertices || this._flagRadius || this._flagStartAngle
      || this._flagEndAngle) {

      var vertices = this.vertices;
      var radiusX = this._radiusX; <---------------NEW
      var radiusY = this._radiusY; <----------------NEW
      var startAngle = this._flagStartAngle; <-------------TYPO? I think this should be this._startAngle
      var endAngle = this._endAngle;

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

        var v = vertices[i];
        var pct = i / (vertices.length - 1);
        var theta = pct * (endAngle - startAngle) + startAngle;

        v.x = radiusX * Math.cos(theta); <-------------- NEW
        v.y = radiusY * Math.sin(theta); <---------------NEW

      }

    }

    super._update.call(this);

    return this;

  }
dickinson0718 commented 2 years ago

@jonobr1 Another suggestion -- It would also be great to have a rotation parameter also, like this: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/ellipse I'm wondering if you have a timeline for when you might start implementing such feature.

Thanks for all you work on this package - it is centrally important to my application.

jonobr1 commented 2 years ago

All objects have a rotation property. Would this be sufficient?

dickinson0718 commented 2 years ago

@jonobr1 Yes!  That would work fine for rotation. Are you going to expand to elliptical arcs instead of just circular ones? Is Two.Arc in production in V.0.8.x? Did I miss the release?

jonobr1 commented 2 years ago

Two.Arc is not implemented anywhere yet, but it's not a problem to expand to elliptical arcs.

dickinson0718 commented 2 years ago

Thanks for the update. Do you have a timeline in mind?

jonobr1 commented 2 years ago

This branch has it implemented, but not tested: https://github.com/jonobr1/two.js/tree/issue-605

Let me know if you give it a try.

jonobr1 commented 2 years ago

This is added to the latest npm package: https://www.npmjs.com/package/two.js

jonobr1 commented 2 years ago

Example: https://codepen.io/jonobr1/pen/jOZxEYm