PAIR-code / megaplot

Apache License 2.0
19 stars 5 forks source link

Implement named matplotlib markers #61

Open jimbojw opened 2 years ago

jimbojw commented 2 years ago

Depends on:

Currently, Megaplot can render regular polygons, circles, filled squares and glyphs of text. In addition, Megaplot should offer the ability to easily specify more complex shapes, like those in the list of matplot.markers.

Background: Sprites are objects which allow API users to specify attributes using the enter(), update() and exit() callbacks, each of which take a SpriteView instance which has getters and setters for the list of sprite attributes. One of those attributes is Sides, which determines whether the Sprite should render a circle/ellipse, rectangle, polygon, or sample from a signed distance field texture (SDF) used for text rendering.

An API user could implement a matplotlib marker via a combination of Sprite attributes. For example, to implement the matplotlib filled plus (+), given the proposed PolygonOffsetRadius API described in #60:

const sprite = scene.createSprite();
sprite.enter(s => {
  // A plus sign can be implemented as a 20-sided polygon.
  s.Sides = 20;

  // Where the offset radii jut in and out to trace the shape
  // as distances relative to the origin (O).
  //
  //          B-------A-------D
  //          |               |
  //          |               |
  //  D-------C               C-------B
  //  |                               |
  //  |                               |
  //  A               O               A
  //  |                               |
  //  |                               |
  //  B-------C               C-------D
  //          |               |
  //          |               |
  //          D-------A-------B
  //
  s.PolygonOffsetRadiusA = 0;
  s.PolygonOffsetRadiusB = 0.1180339887498949;  // sqrt(1 + .5^2) - 1.
  s.PolygonOffsetRadiusC = 0.7071067811865476;  // sqrt(2) / 2.
  s.PolygonOffsetRadiusD = 0.1180339887498949;  // sqrt(1 + .5^2) - 1.
});

To simplify specifying markers such as this, Megaplot should offer a Shape and/or ShapeView class which exposes convenient setters at a slightly higher level of abstraction.

Proposed example:

const shape = scene.createShape();

shape.enter((s: ShapeView) => {
  // Attributes of the ShapeView.
  s.Shape = CONSTANTS.SHAPE_PLUS_FILLED;
});

Behind the scenes, the setter for ShapeView's Shape attribute should set the Sides and PolygonOffsetRadius components appropriately.

Performance consideration: In principle, a ShapeView class could be a wrapper class around a SpriteView that the API user creates on demand. However, for performance reasons, this should be discouraged. Creating short-lived wrapper objects is slower than keeping those objects around cached. It also makes frequent garbage collection more likely.

So just as SpriteView objects are created early and cached, ShapeView objects should be created early and cached. And just like with a SpriteView, if the API user attempts to access it after the underlying swatch has been freed, an informative error should be thrown.

Alternative: An alternative API would be one which uses methods that operate directly on SpriteView objects to obtain the desired effect. Consider:

function makePlusFilled(s: SpriteView) {
  s.Sides = 20;
  s.PolygonOffsetRadiusA = 0;
  s.PolygonOffsetRadiusB = 0.1180339887498949;  // sqrt(1 + .5^2) - 1.
  s.PolygonOffsetRadiusC = 0.7071067811865476;  // sqrt(2) / 2.
  s.PolygonOffsetRadiusD = 0.1180339887498949;  // sqrt(1 + .5^2) - 1.
}

// ... later ...

const sprite = scene.createSprite();
sprite.enter(s => {
  makePlusFilled(s);
});

This implementation option has several advantages:

The disadvantage of this implementation approach is divergence from the proposed Text API in #57. While it would be possible to implement the Shape API using utility functions like the above, the same is not true of Text, which requires passing through to multiple Sprite instances which must be created and retired.