Skip to content

[p5.js 2.0 RFC Proposal]: New API for vertex functions #6766

Closed
@GregStanton

Description

@GregStanton

Increasing access

Implementing this proposal would

  • decrease complexity
  • increase consistency
  • enable new features

As a result, p5.js would be more accessible to beginners, while offering others access to a wider feature set.

Which types of changes would be made?

  • Breaking change (Add-on libraries or sketches will work differently even if their code stays the same.)
  • Systemic change (Many features or contributor workflows will be affected.)
  • Overdue change (Modifications will be made that have been desirable for a long time.)
  • Unsure (The community can help to determine the type of change.)

Most appropriate sub-area of p5.js?

  • Accessibility
  • Color
  • Core/Environment/Rendering
  • Data
  • DOM
  • Events
  • Image
  • IO
  • Math
  • Typography
  • Utilities
  • WebGL
  • Build process
  • Unit testing
  • Internationalization
  • Friendly errors
  • Other (specify if possible)

What's the problem?

Short version

The current API for vertex functions contains complexities, inconsistencies, and hard-to-extend features. It's included below for easier comparison with the proposed API.

// vertex functions (brackets indicate optional parameters)
// copied from the syntax section of the p5.js reference
// note that arcVertex() is planned but not currently included

vertex(x, y, [z], [u], [v])
// arcVertex(x2, y2, x3, y3, x4, y4, x5, y5)
// arcVertex(x2, y2, z2, x3, y3, z3, x4, y4, z4, x5, y5, z5) 
quadraticVertex(cx, cy, x3, y3)
quadraticVertex(cx, cy, cz, x3, y3, z3)
bezierVertex(x2, y2, x3, y3, x4, y4)
bezierVertex(x2, y2, z2, x3, y3, z3, x4, y4, z4)
curveVertex(x, y, [z])

Long version

Eleven distinct problems arise from the current API for vertex functions. Below, each problem is listed and tagged with a problem type. (In case we want to modify the proposed solution, we can check the new version against this list.)

  1. Texture coordinates: Texture coordinates are currently supported inconsistently, and it's infeasible to fix this without a change to the API. Specifically, the user can specify texture coordinates to vertex() but not to quadraticVertex(), curveVertex(), or bezierVertex(), as noted in #5722. Under the current API, specifying texture coordinates would require up to fifteen parameters in a single function call, with the following syntax: bezierVertex(x2, y2, z2, u2, v2, x3, y3, z3, u3, v3, x4, y4, z4, u4, v4). [Inconsistency/Inflexibility]
  2. Parameter grouping: The last example may look like it contains a typo, but it doesn't. The current parameter lists for quadraticVertex() and bezierVertex() really do start with x2, y2/x2, y2, z2. That's because the first set of parameters x1, y1/x1, y1, z1 must be specified separately, and we need a way to distinguish the second, third, and fourth points since they're grouped together in the same function call. [Complexity]
  3. Mixed commands: If we want to make a quadratic or cubic Bézier curve, that first point x1, y1/x1, y1, z1 gets passed to vertex(). But that's just for Bézier curves. If we want to make a Catmull-Rom spline, the first point and all successive points get specified directly to curveVertex(). In other words, the current API allows the user to call vertex() to start and continue a polyline, and to call curveVertex() to start and continue a Catmull-Rom spline; however, to start a shape with a quadratic or cubic Bézier curve, the user needs to mix commands. [Inconsistency]
  4. Singularity of "vertex": A function like bezierVertex() is named after a single vertex, but it accepts coordinates for three points. We may try to reconcile this by identifying the first two of those points as "control points" and the last point as a "vertex," but this is an uncommon distinction, and it doesn't hold up: we cannot reconcile it with the meaning of "vertex" in curveVertex(). [Inconsistency]
  5. Multiple signatures: With the current API, it's necessary to specify multiple signatures for the same function, which increases complexity. For example, we have both bezierVertex(x2, y2, x3, y3, x4, y4) and bezierVertex(x2, y2, z2, x3, y3, z3, x4, y4, z4). Each of these signatures looks rather complicated by itself. [Complexity]
  6. Missing primitives: Right now, bezierVertex() and bezier() are supported, and curveVertex() and curve() are supported. We also have quadraticVertex(), but quadratic() is not supported. (I've proposed separately that we should introduce a new function arcVertex() to address a similar inconsistency.) [Inconsistency]
  7. "Quadratic" confusion: The name quadraticVertex() is confusing for multiple reasons. Both quadraticVertex() and bezierVertex() produce Bézier curves, but only one of them contains the name "Bézier"; in other words, one function is named after the type of curve while the other is named after the order of the curve. Also, many p5 users are students who will have recently learned in algebra that "the vertex of a quadratic" (a parabola) corresponds to its lowest or highest point; that's not generally true of a vertex specified with quadraticVertex(). [Inconsistency]
  8. Higher-order Bézier curves: With the current API, supporting general higher-order Bézier curves is not possible. (Even if p5.js may not want to implement higher-order Bézier curves, add-on libraries may want to.) [Inflexibility]
  9. Bézier surfaces: Right now, supporting Bézier surfaces (in p5.js or in an add-on library) would introduce more complications to the API. For example, a quadratic Bézier triangle is defined by six control points. If we specify the first of these with vertex() and specify the next four with quadraticVertex(), we only have five vertices. We could potentially use vertex() to specify the sixth vertex at the end, but this is inconsistent with how vertex() is used everywhere else, and it still requires us to mix command types to create a single primitive. [Inflexibility]
  10. Bézier syntax vs. other syntax: The syntax for the vertex functions is inconsistent. The current API bundles multiple points together into a single function call for quadratic and cubic Bézier curves, but it uses one function call per point for polylines and Catmull-Rom splines. [Inconsistency]
  11. Two meanings of "curve": The name curveVertex() uses a general term for a special shape, which leads to inconsistencies. For example, in the p5 reference, “Curves” is used as a general section heading that includes Béziers, while curve() specifically creates Catmull-Rom splines only. This may lead to confusion. For instance, if we introduce a function such as curveType() for different spline implementations, it will be hard to guess whether it applies to all curves or only curves made with curveVertex(). [Inconsistency]

What's the solution?

The main idea is to pass only one vertex into each vertex function.

Proposed API

The proposed API for p5.js 2.0 is indicated below.1 The splineType() function may be added in a future release. A sample of spline types has already been discussed, for illustrative purposes.

// mode function (could default to n = 3, with native support for n = 2 and 3, at least)
bezierOrder(n)

// type setter (allows spline types other than Catmull-Rom to be specified, each with their own properties)
// may be added after 2.0
splineType(type)

// property setters (these replace curveTightness() and allow other properties, like interpolated ends)
// plural version accepts an object with key-value pairs
splineProperty(key, value)
splineProperties(options)

// vertex functions (brackets indicate optional parameters)
vertex(x, y, [z], [u], [v])
arcVertex(x, y, [z], [u], [v])
bezierVertex(x, y, [z], [u], [v])
splineVertex(x, y, [z], [u], [v])

Amazingly, this simple API solves all eleven problems listed above 🤯The basic design was inspired by this comment from @davepagurek.

Notes:

  • bezierOrder() eliminates the need for quadraticVertex()
  • arcVertex() is based on the latest iteration of #6459
  • splineVertex() replaces curveVertex()2

Code examples

Example 1: Consistency across all types of curves
Note: Conceptually, polylines can be viewed as linear splines or as chained first-order Béziers.

Polylines (no change proposed) Splines (proposed) Bézier curves (proposed)
beginShape();

// polyline 
// (with three segments)

vertex(x0, y0, z0);
vertex(x1, y1, z1);
vertex(x2, y2, z2);
vertex(x3, y3, z3);

endShape();
beginShape();

// Catmull-Rom spline
// (a type of cubic spline)

splineVertex(x0, y0, z0);
splineVertex(x1, y1, z1);
splineVertex(x2, y2, z2);
splineVertex(x3, y3, z3);

endShape();
beginShape();

// cubic Bezier curve
bezierOrder(3);

bezierVertex(x0, y0, z0);
bezierVertex(x1, y1, z1);
bezierVertex(x2, y2, z2);
bezierVertex(x3, y3, z3);

endShape();

Example 2: Clear function names, readable parameter lists, and flexible design
Note: Chaining quadratic and cubic Bézier curves does not currently work in p5, but issue #6560 aims to address this.

Chained Bézier curves (current) Chained Bézier curves (proposed)
beginShape();

// two quadratic Bezier curves
// explicit starting vertex required

vertex(x0, y0, z0);

quadraticVertex(x1, y1, z1, x2, y2, z2);
quadraticVertex(x3, y3, z3, x4, y4, z4);

// one cubic Bezier curve
// implicit starting vertex at x4, y4, z4

bezierVertex(x5, y5, z5, x6, y6, z6, x7, y7, z7);

endShape();







beginShape();

// two quadratic Bezier curves
// explicit starting vertex required

bezierOrder(2);
bezierVertex(x0, y0, z0);

bezierVertex(x1, y1, z1);
bezierVertex(x2, y2, z2);

bezierVertex(x3, y3, z3);
bezierVertex(x4, y4, z4);

// one cubic Bezier curve
// implicit starting vertex at x4, y4, z4

bezierOrder(3);
bezierVertex(x5, y5, z5);
bezierVertex(x6, y6, z6);
bezierVertex(x7, y7, z7);

endShape();

Example 3: Consistency across all chained shapes (curves, triangles, and quads)
Note: Bézier control points are rendered below as a visual aid, but the code for that is not shown.

Quad strip (no change proposed) Bézier curves (proposed)
A quad strip consisting of three adjacent quadrilaterals. A chain of three quadratic Bézier curves, with their control points.
beginShape(QUAD_STRIP);

// first quad
// four vertices required
vertex(30, 20);
vertex(30, 75);
vertex(50, 20);
vertex(50, 75);

// one more quad
// two vertices required (two reused)
vertex(65, 20);
vertex(65, 75);

// one more quad
// two vertices required (two reused)
vertex(85, 20);
vertex(85, 75);

endShape();
beginShape();

// first quadratic Bezier curve
// three vertices required
bezierOrder(2);
bezierVertex(5, 12);
bezierVertex(41, 12);
bezierVertex(23, 30);

// one more quadratic Bezier curve
// two vertices required (one reused)
bezierVertex(5, 48);
bezierVertex(41, 48);

// one more quadratic Bezier curve
// two vertices required (one reused)
bezierVertex(182, 54);
bezierVertex(41, 57);

endShape();

Pros (updated based on community comments)

  • Simplicity: All the complexities noted in the problem statement are eliminated.
  • Consistency: All inconsistencies noted in the problem statement are eliminated.
  • Flexibility: Multiple new features are possible, with a more intuitive API.
  • Readability: Code is easier to read since long lists of positional parameters are eliminated.
  • Predictability: An arc starts with arcVertex(), rather than vertex(). Same for other curve types.

Cons (updated based on community comments)

  • Breaking changes: Breaking changes will always cause difficulties when they are first introduced, for some people. For example, YouTubers may need to update their video tutorials, and add-on library authors may need to update their code if they want to use the most recent version of p5.js. However, the proposal could be implemented without breaking changes, at a small cost.3 Another option is to support the 1.x API via a compatibility add-on. Based on further discussion, we've decided to use a compatibility add-on.
  • Differences with other APIs: Changing the p5.js API would mean a bigger departure from the Processing API, the native canvas API, and the SVG API. The latter two API's are similar to p5's, but they sidestep some of the problems by referring to their commands as "curve" commands rather than "vertex" commands. In the context of p5, beginners won't tend to know those other APIs, and more experienced users may have less trouble adapting, so this seems like a smaller concern.

Proposal status

Under review

Updates

This proposal has been updated to reflect some small adjustments, based on the discussion in the comments section:

  1. bezierOrder() was added so that chained segments of different orders can be distinguished
  2. arcVertex() was added based on the current consensus in #6459
  3. curveVertex() was replaced with splineVertex() based on Problem 11, which was added to the problem list
  4. splineProperty() was added as a more extensible way to set spline properties (this replaces curveTightness())
  5. splineProperties() was added based on a discussion starting with this comment
  6. splineType(type) was added as a potential future addition (in a later version of p5.js)

Additional code examples have also been included.

Footnotes

  1. We may also want to support texture coordinates when a z-coordinate is not passed. This is currently supported by vertex() (see the last example for texture() in the reference); however, it requires a separate signature for vertex() that is not documented on its reference page (i.e. vertex(x, y, [u], [v])). We'd need to discuss whether the benefits justify the extra complexity (namely, documenting and supporting an extra signature for every type of vertex function).

  2. For consistency, we could also rename curve() as spline().

  3. We can distinguish new and old usage of bezierVertex() by detecting the number of arguments, and we can deprecate the old usage. We could deprecate quadraticVertex() and curveVertex() entirely with the @deprecated tag in the inline documentation.

Metadata

Metadata

Assignees

Type

No type

Projects

Status

Completed

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions