Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[p5.js 2.0 RFC Proposal]: API audit #7460

Open
3 of 21 tasks
GregStanton opened this issue Jan 8, 2025 · 2 comments
Open
3 of 21 tasks

[p5.js 2.0 RFC Proposal]: API audit #7460

GregStanton opened this issue Jan 8, 2025 · 2 comments

Comments

@GregStanton
Copy link
Collaborator

GregStanton commented Jan 8, 2025

Increasing access

This proposal would make p5.js 2.0 and later versions more beginner friendly, in three stages:

  1. Establish consensus on a set of criteria and guidelines for the API.
  2. Resolve complications in the current API.
  3. Implement a simple process for preventing complications in the future.

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?

The p5.js API contains quite a few inconsistencies and other forms of complexity. This complexity makes p5.js harder to learn and harder to use, since users experience it directly through its API. While some parts of the API have been simplified for 2.0, such as the API for vertex functions, other complexities remain. This is because identifying and preventing complexities in a large API is difficult without clear guidelines and a practical quality assurance process.

What's the solution?

1. Establish criteria and guidelines

In order to identify and prevent unnecessary complexity in the p5 API, we can develop a list of criteria we want it to satisfy. We can also provide guidelines for common types of features, such as getters and setters. Initial criteria and guidelines are listed below.

Criteria

  1. Consistency: Provide a similar interface for similar features and different interfaces for different features.
  2. Readability: Design features that are easy to read and understand.
  3. Writability: Make features easy to use and hard to misuse.
  4. Predictability: Respect the principle of least surprise, so that users can guess how features work.
  5. Extensibility: Hide implementation details and lay a foundation for add-on libraries.
  6. Economy: Limit redundant features, and consider add-on libraries for new features.

For a given feature, these design criteria may be considered with respect to naming, syntax, and behavior.

Guidelines

  • Getters and setters:
    • A function or method with the same name as a setting, such as stroke() and fill(), should act jointly as a getter (when no argument is passed) and a setter.
    • General setters such as vertexProperty(name, value) should act as getters when value is omitted.
    • For consistency with commonly used functions such as stroke(), joint getters/setters should be used when possible. However, if separate getters and setters are to be used, they should begin with “get” and “set,” except for getters for Booleans, which should have names such as isLooping().
    • Something about setting fields directly? (Guidance might prevent inconsistencies like using the field disableFriendlyErrors to toggle friendly errors and using functions like fullscreen() and autoSized() to toggle other settings.)

These guidelines are included as examples. During the second phase, when specific kinds of issues are dealt with, the guidelines can be fleshed out.

References:

2. Perform an audit (a scavenger hunt!) and fix problems

Under ideal circumstances, the community could hold a scavenger hunt for features that don't adhere to p5's API criteria. Beginners could make significant contributions by spotting naming inconsistencies, for example. However, since p5.js 2.0 will be released relatively soon, I performed a rapid audit of the full API, to get the ball rolling.

Methodology

I skimmed through the whole reference and looked for issues that jumped out to me as potential areas of concern (I didn’t systematically apply all criteria to each feature). I also skipped over some things that I didn’t suspect were problematic.

Caveats:

Results

For illustration, only a partial sample of results from the rapid audit are included here. Skimming this section should give a rough sense of what's at stake.

Note: The sample includes examples of potential problems with naming, syntax, and semantics (broadly defined). Each example is tagged with the criteria that fail to be satisfied.

This partial sample of API problems can be replaced with separate GitHub issues, with one issue for each affected section of the reference. Those issues can be linked from here, in a checklist.

Note: The sample only includes issues that may require breaking changes, but the audit did reveal other issues that could be fixed with non-breaking changes after the release of p5.js 2.0.

Naming

  • color()
    • Relevant criteria: Consistency, predictability
    • Problem: Unlike other constructor functions in p5, this function does not start with a “create” prefix, leading users to guess that it’s a setter such as stroke().
    • Potential fix: Replace color() with createColor().
  • getTargetFrameRate()/frameRate()
    • Relevant criteria: Consistency, predictability
    • Problem: In this getter-setter pair, the getter and setter use different names for the same setting. The getter uses the name “target frame rate” but the setter uses the name “frame rate.” Also, the getter is prefixed with “get” but the setter is not prefixed with “set.”
    • Potential fix: Remove getTargetFrameRate() and turn frameRate() into a joint getter/setter.
  • setAttributes()
    • Relevant criteria: Consistency, readability, predictability
    • Problem: This feature has a couple of issues. First, it has a plural name, but setAttributes(key, value) only sets a single attribute. Second, the name is ambiguous, since it only sets attributes of the WebGL drawing context, but this isn't advertised in the name.
    • Potential fix: Both of these issues could potentially be fixed by replacing setAttributes() with, say, canvasProperty(key, value).
  • onended()
    • Relevant criteria: Consistency, readability
    • Problem: This feature’s name doesn’t use camel case, the name is awkward linguistically, and it’s inconsistent with similar methods of p5.Element, like touchEnded().
    • Potential fix:: Replace onended() with playEnded(). (However, there is a deeper issue, which is that these functions take a callback function, whereas event handlers like doubleClicked() are defined directly by the user. If one of these two behaviors cannot be used in all cases, then they should be distinguished by their names. For example, a name like onTouchEnd() could be used for one type of behavior and a name like touchEnded() could be used for another.)

Syntax

  • createModel(), loadModel()
    • Relevant criteria: Consistency, readability, writability
    • Problem: The same parameters in these functions appear in different orders (normalize and fileType are out of order in one signature, fileType is out of order in another, and fileType is missing from one signature).
    • Potential fix: One option is to make the parameter lists consistent. Another is to choose a different API that allows the parameters to be specified in any order (and there are potentially multiple such APIs).
  • spotLight(), directionalLight() and pointLight()
    • Relevant criteria: Readability, economy
    • Problem: As an example, spotLight() has eight signatures. It accepts three vectors as input, but in some signatures, some of these vectors are specified as p5.Vector objects whereas others are not. This makes the documentation unnecessarily complicated and leads to code that’s hard to read. The other listed features have the same type of issue.
    • Potential fix: A potential fix is to remove the signatures that mix componentwise specifications and p5.Vector specifications. The pure componentwise signature of spotLight() has 11 parameters and is very hard to read, but keeping both the pure componentwise and pure p5.Vector signatures reduces breaking changes and keeps these features consistent with other functions that take multiple vectors as input, such as quad(). (Since quad() takes up to a whopping 14 parameters, and since point() already accepts both componentwise and p5.Vector input, it makes sense to add support to quad() and all other 2D primitive functions for both types of input.)
  • p5.Graphics, p5.Framebuffer
    • Relevant criteria: Consistency, predictability
    • Problem: The reference says these classes are “similar,” and it’s true. Semantically, they are similar. However, syntactically, they’re significantly different. Requiring users to learn different interfaces for the same kinds of features reduces accessibility. If these aren’t reconciled, complications are likely to compound (e.g. the p5.Shape class developed for 2.0 has an interface that’s closer to that of p5.Graphics, and it may be exposed to users in a future version).
    • Potential fix: Requires discussion.

Semantics

  • addClass(), class()
    • Relevant criteria: Readability, economy
    • Problem: The p5.Element class has addClass() and class(), both of which add a class to the element. The difference is that class() is a joint getter/setter that seems to make addClass() unnecessary. Having both is likely to confuse users.
    • Potential fix: Remove addClass().
  • millis()
    • Relevant criteria: Consistency, readability, predictability
    • Problem: Unlike all other time and date functions, millis() returns the time since the sketch started running; all other functions, including second(), return the current time (e.g. if a sketch is run 32 seconds after noon, then when the sketch starts running, second() will return 32 whereas millis() will return 0). Also, “millis” is an abbreviation, unlike the other function names; that doesn’t seem like enough to convey the difference in behavior.
    • Potential fix: Requires discussion.
  • debugMode()
    • Relevant criteria: Readability, writability, extensibility
    • Problem: This function has multiple signatures, which take up to nine numerical parameters. Extending this feature is desirable but impractical, given that it’s already complex. Also, “mode” is a problematic description of this feature, since it actually provides different helpers (a grid helper and an axes helper) which can be combined, rather than modes, which are typically mutually exclusive.
    • Potential fix: The three.js library provides a GridHelper, an AxesHelper, and a variety of other helpers (a camera helper, a spotlight helper, etc.). In p5, having separate axisHelper() and gridHelper() functions should be an improvement. We might also consider functions like axesHelperProperty(key, value) for greater readability and writability (effectively allowing named parameters).
  • 2D Primitives
    • Relevant criteria: Consistency, predictability
    • Problem: Whereas quad() has multiple signatures and can create a shape in 2D or 3D, triangle() only works in 2D. Other issues include shapes that can be drawn in WebGL mode but only by specifying xy-coordinates rather than xyz-coordinates. Some of these shapes support a detail parameter while others don’t. Some support p5.Vector arguments and others don’t. Rounding corners in rect() and square() adds another complication.
    • Potential fix: The first thing is to make a table with one row per feature, and columns for dimensions, vector arguments, and so on. This will help us identify the exact issues that need to be addressed, and which of them would require breaking changes to fix (@davepagurek and I mostly finished the table).

3. Implement quality assurance process via issue templates

Bugs can be fixed in any version of p5. API problems cannot be fixed outside of a major version release (if at all), if they're like any of the problems listed above. To avoid introducing such problems in the future, we can implement a basic quality assurance process via the issue templates for new features and existing feature enhancements:

  1. Include a section for code examples. The community can help with filling this in, so that early-stage ideas are not discouraged.
  2. Include a checklist, to be completed before releasing the feature and closing the issue. The checklist can indicate criteria and guidelines. For each checklist item, “good” and “bad” examples can be provided (either directly on the issue template, or via a link to the contributor docs).
  3. Indicate what to expect after submitting a new-feature request:
    a. Documentation will be required before release (and encouraged before implementation).
    b. Review will be required by multiple stakeholders.

Pros (updated based on community comments)

  1. Consistency: Inconsistencies will be fixed or prevented.
  2. Readability: Confusion about the meaning of code will be reduced or prevented.
  3. Writability: Features will be easier to use and harder to misuse.
  4. Predictability: Unpleasant surprises will be eliminated or prevented.
  5. Extensibility: Features will be more extensible, both for the core p5 library and for add-ons.
  6. Economy: Confusion or cognitive overload from redundant features will be reduced or prevented.

Cons (updated based on community comments)

  1. Breaking changes: Existing code may break. (However, we can address this with a compatibility add-on. Also, since we've already made some breaking changes, the marginal cost of new breaking changes for users is lower: users who are aware of the breaking changes we've already made will already know about the compatibility add-on and may already have a process for fixing legacy code.)
  2. Time expenditure: Resolving this issue will take some time. (However, we can speed it up by inviting more contributors. This issue is largely beginner friendly. Beginners can help by telling us when features are confusing to them, by identifying complexities, or by implementing changes such as name changes.)

Proposal status

Under review

@davepagurek
Copy link
Contributor

Thanks for getting these all together Greg! Generally agreed with everything. Here are a few comments on some of the points:


color()

  • Potential fix: Replace color() with createColor().

This definitely makes sense consistency, but this is also a pretty old API that has been around since the start of Processing. It likely not called create* then because it would return a value data type instead of an object data type, but now in p5, it is in fact an object with state. Although we generally shy away from aliases, this one might be important enough to keep an alias for it if we change it.

frameRate() vs getTargetFrameRate

  • Potential fix: Remove getTargetFrameRate() and turn frameRate() into a joint getter/setter.

frameRate() with no arguments is actually already a getter, but it returns the actual frame rate of the sketch, not the frame rate you asked for (e.g. if you called frameRate(60) but your sketch is lagging, frameRate() might return something lower like 10, but getTargetFrameRate() would return 60 still.) For consistency, we'd maybe want to make frameRate() return whatever target rate you asked for, remove getTargetFrameRate, and make a separate method called measureFrameRate() or something like that to indicate it's different?

media.onended()

  • Problem: This feature’s name doesn’t use camel case, the name is awkward linguistically, and it’s inconsistent with similar methods of p5.Element, like touchEnded().

  • Potential fix:: Replace onended() with playEnded(). (However, there is a deeper issue, which is that these functions take a callback function, whereas event handlers like doubleClicked() are defined directly by the user. If one of these two behaviors cannot be used in all cases, then they should be distinguished by their names. For example, a name like onTouchEnd() could be used for one type of behavior and a name like touchEnded() could be used for another.)

Agreed that this should be camel-cased at least, and maybe have the on removed. I don't think the callback behaviour is inconsistent though: you can define function doubleClicked() { ... } globally to handle double clicks anywhere, and you can also call it with a callback on a specific p5.Element to handle the event on that element alone with yourElement.doubleClicked(() => { ... }).

millis()

  • Problem: Unlike all other time and date functions, millis() returns the time since the sketch started running; all other functions, including second(), return the current time (e.g. if a sketch is run 32 seconds after noon, then when the sketch starts running, second() will return 32 whereas millis() will return 0). Also, “millis” is an abbreviation, unlike the other function names; that doesn’t seem like enough to convey the difference in behavior.

This is a good point -- millis() is used to control the timing of your animation in real time as an alternative to frameCount, which drops frames, but it's grouped with a bunch of other date and time related methods.

Context: why you might want to use millis vs frameCount Say you want it to take 5s for a circle to move across the canvas. If your animation is supposed to be 60fps but it can only keep up at 30fps, if you time everything based on `millis()`, after 5s have passed, the circle will still be where you expect it, it'll just look choppier as it animates there since it will only have redrawn 150 times instead of 300. If you instead say that the circle should be at the other side after 5\*60=300 frames, then if frames are drawing more slowly, it takes 10s to complete the animation, but it still draws all 300 frames.

The former is good if you want realtime playback. The latter is good if you want to do an export and ensure every frame gets rendered.

Maybe the thing to do here is keep millis() for its current purpose, and make millisecond() for date/time related things, to be consistent with the naming of the others? And then move millis() to the Environment category (where frameCount is), and keep millisecond() in the date/time category?

2D vs 3D in primitives

  • Problem: Whereas quad() has multiple signatures and can create a shape in 2D or 3D, triangle() only works in 2D. Other issues include shapes that can be drawn in WebGL mode but only by specifying xy-coordinates rather than xyz-coordinates. Some of these shapes support a detail parameter while others don’t. Some support p5.Vector arguments and others don’t. Rounding corners in rect() and square() adds another complication.

If we want to add 3D coordinate support as overloads to these methods, I believe the only one that would cause problems is rect(), because of the corner radii. e.g. these two signatures have the same number of arguments, so we can't tell them apart:

rect(x, y, z, w, h)
rect(x, y, w, h, r)

This could maybe be addressed by changing the syntax for corner radius, e.g.:

rect(x, y, z, { r: 4 }) // set all at once
rect(x, y, z, { tl: 4, tr: 4, br: 8, bl: 8 }) // set individually

p5.Framebuffer vs p5.Graphics APIs

  • Problem: The reference says these classes are “similar,” and it’s true. Semantically, they are similar. However, syntactically, they’re significantly different. Requiring users to learn different interfaces for the same kinds of features reduces accessibility. If these aren’t reconciled, complications are likely to compound (e.g. the p5.Shape class developed for 2.0 has an interface that’s closer to that of p5.Graphics, and it may be exposed to users in a future version).

For some extra context for others reading this on why framebuffers have a different syntax than graphics:

  • There is a performance cost when switching between different draw targets, so the API encourages drawing to framebuffers in batches
  • Framebuffer as a concept live within the rendering context and share all other state. To avoid setting lots of state back and forth to make it appear to the user that they are separate, the API also is set up to share state with the main context
  • Not having to prefix every call is a feature too:
    • it's easy to forget the prefix
    • not having them makes it easier to refactor code that used to draw to the main canvas to now draw to a layer
    • for addons, it's possible to transparently draw to a layer under the hood without the user's involvement at all (see the p5.FilterRenderer addon for examples of APIs that take advantage of this)

The first two points are framebuffer-specific, and probably mean framebuffers won't get an API that looks like graphics. The latter could apply to both, and is possibly a reason to give graphics a framebuffer-like API in the future (people asking how to do that for p5.Graphics comes up in the Discord every once in a while, anecdotally.)

@GregStanton
Copy link
Collaborator Author

GregStanton commented Jan 8, 2025

Thanks @davepagurek! I really appreciate your thorough explanations.

In case anyone else has comments, we're planning to move discussion of particular API changes to separate issues for each affected section of the API. I'll also make a separate issue for the quality assurance process and link to it from this issue. We'll dedicate discussion in the current issue to the API design criteria and guidelines.

P.S. I like your use of a dropdown for extra context. I've been using footnotes, which violate the spatial contiguity principle that we discussed haha

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants