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

Allow object literals for 2D-Shape functions in additional to positional parameters #7609

Open
jht9629-nyu opened this issue Mar 7, 2025 · 10 comments

Comments

@jht9629-nyu
Copy link

jht9629-nyu commented Mar 7, 2025

  • functions with more than a few parameters are hard to remember how to use
  • object literals to the rescue!
  • allow object literal parameter in p5.js Shape functions for readability
  • leveraging api patterns from SmallTalk, Objective-C, Swift, and modern JavaScript frameworks (eg. react)
  • arc function example
// arc(x, y, w, h, start, stop, [mode], [detail])
  arc(50, 50, 80, 80, 0, PI + HALF_PI);
-- ++ allow object literal option
  arc( {x: 50, y:50, w:80, h:80, start:0, stop:PI + HALF_PI } );
  • change is totally opt-in, existing syntax is still allowed
  • document change could be limited to one extra line in syntax summary for each Shape function

function arc() Syntax

arc(x, y, w, h, start, stop, [mode], [detail])
arc({ x, y, w, h, start, stop, mode, detail })
  • Other Shapes updates for object literal parameter:
ellipse({ x, y, w, h})

circle({ x, y, d })

rect({ x, y, w, h, tl, tr, br, bl })

triangle({ x1, y1, x2, y2, x3, y3 })

triangle([{ x, y }, { x, y }, { x, y }])

line({ x1, y1, x2, y2 })
line([{ x, y }, { x, y }])
line({ x1, y1, z1, x2, y2, z2 })
line([{ x, y, z }, { x, y, z }])

  • Details:
This proposal would affect the following Shape functions:
arc()
circle()
ellipse()
line()
point()
quad()
rect()
square()
triangle()
Copy link

welcome bot commented Mar 7, 2025

Welcome! 👋 Thanks for opening your first issue here! And to ensure the community is able to respond to your issue, please make sure to fill out the inputs in the issue forms. Thank you!

@afthal-ahamad01
Copy link

This proposal makes p5.js more beginner friendly and improves readability. especially for new users unfamiliar with function parameters. Should we add validation when using object literals? For example, if someone forgets a required key
(e.g: x in rect({ y: 50, w: 100, h: 100 })), should we throw an error or use a default value???

@jht9629-nyu
Copy link
Author

For missing parameter key we can issue an error, similar to what's done now.
eg:

  rect()
-->
🌸 p5.js says: [sketch.js, line 3] rect() was expecting at least 3 arguments, but received only 0. (http://p5js.org/reference/p5/rect) 

--

something like this for your example:

rect({ y: 50, w: 100, h: 100 }))
-->
"expected value for argument key x"

@leey611
Copy link

leey611 commented Mar 28, 2025

i agree. the parameters are hard to recognize and remember for new p5 users. using object literals can improve the readability.
if the proposal is approved, i would love to contribute to it

@ksen0 ksen0 changed the title [p5.js 2.0 RFC Proposal]: update Shape api for readability using object literals update Shape api for readability using object literals Mar 28, 2025
@ksen0 ksen0 changed the title update Shape api for readability using object literals Allow object literals for functions instead of parameters Mar 28, 2025
@ksen0
Copy link
Member

ksen0 commented Mar 28, 2025

Thanks for the idea! I've removed the 2.0 phrasing from the title as no API extensions are considered for the upcoming major release (see timeline). After this, new features will be considered for p5.js 2.1 (or later minor releases, like 2.2 and 2.3).

Except bugfixes (or similar exceptions), new features will not be considered for p5.js 1.x (even though it will continue to be supported and available until summer 2026).

For all those reading: The prioritization of a feature, especially API extension, is not based on popularity directly, but if you want to see this implemented, please do comment! Besides popularity: it matters what the benefits/drawbacks of adding the feature, and the benefits/drawbacks of not adding it. @GregStanton also has a great set of API extension criteria:

Since this proposal would extend the API, I think this is a good test case for the #7460 I proposed previously:
Consistency: Provide a similar interface for similar features and different interfaces for different features.
Readability: Design features that are easy to read and understand.
Writability: Make features easy to use and hard to misuse.
Predictability: Respect the principle of least surprise, so that users can guess how features work.
Extensibility: Hide implementation details and lay a foundation for add-on libraries.
Economy: Limit redundant features, and consider add-on libraries for new features.

In this case, I think although it improves beginner-friendliness for people who prefer to use objects, I think it's important to make the case that this approach wouldn't add new challenges for maintainability (on the code side), readability/consistency/predictability (why are there multiple ways to call basic functions? which is better?), extension/economy (what about when new users, using objects in params like this, start to use add-on libraries that use established style? It could be an unnecessarily confusing transition.)

I'm open to a discussion here, but many (maybe most!) users who are starting with p5.js are new to programming and JavaScript generally, so they may not be comfortable with objects.

@davepagurek
Copy link
Contributor

davepagurek commented Mar 28, 2025

this one is a tough call for me, and it's about balancing readability and writability. As a code reader, I'd rather read objects, since the keys basically act as labels. As a code writer, it's extra overhead and syntax.

Here are some of the design principles I've been using so far for 2.0, which attempt to address some of the same underlying problems:

  • limit the number of parameters in a function, and the number of overload variants. 1-3 positional parameters doesn't seem too bad, and the function name helps act as a label here too. For that to work, the positional parameters have to be mandatory or super common for people to use.
  • For optional and less common options, and as a way to limit the number of overloads, have an options object as the last parameter (e.g. what we do for textToPoints.) You then get labels on the more obscure settings, and it simplifies how we handle different combinations of optional parameters being provided.
  • Break up complex operations into multiple function calls. In 2.0, we've split up bezierVertex so that you have a separate call for each control point (so each call just has x,y,z now, which is fairly readable without extra labels). To me, this logical grouping into chunks makes it easier to read than the same big function call but with an object parameter with levels for x1, y1, z1, x2, y2, z2, etc.

I'm not sure yet if this handles cases like arc, as mentioned in the issue, since it has a large number of required parameters, which don't have a natural order to them like x,y,z parameters. For that, it might make sense to move to object parameters for at least some of them in the future?

For cases like line and triangle, we kind of get the chunking I described by using beginShape/vertex, so I'm also not sure where that leaves us for the all-in-one variants.

@jht9629-nyu
Copy link
Author

@kens0 re your comment:

I'm open to a discussion here, but many (maybe most!) users who are starting with p5.js are new to programming and JavaScript generally, so they may not be comfortable with objects.

This is ment to be a new user friendly optional syntax. New users don't have to be comfortable with objects to benefit from this proposal. It's an alternative to the existing overload of parameters which can be confusing
Take rect, here's the current syntax summary:

  • rect(x, y, w, [h], [tl], [tr], [br], [bl])
  • rect(x, y, w, h, [detailX], [detailY])

note: a new user has to understand [h], [tl], etc.. is not valid javascript.

Migrating to object literal syntax summary could look like this:

  • rect({x, y, w, h, tl, tr, br, bl, detailX, detailY })
    The accompanying explicit example could look like this:
  • rect({x: 0, y: 0, w: 100, h: 100})

@jht9629-nyu
Copy link
Author

@ksen0 I disagree with new title: "Allow object literals for functions instead of parameters"
revised: "Allow object literals for functions in additional to positional parameters"

@ksen0 ksen0 changed the title Allow object literals for functions instead of parameters Allow object literals for functions in additional to positional parameters Mar 29, 2025
@GregStanton
Copy link
Collaborator

Hi all!

Thanks @jht9629-nyu for sharing your proposal! I'm really glad to see this issue because I think it's a good starting point for a discussion of object-literal parameters in general. This is a big and important topic.

A lot of good points have already been made here. Before I add anything, it might be helpful to figure out how to approach a topic as large as this one. I'll start by outlining the breadth and depth of the issue, as I see it, and then I'll propose some possible next steps.

Breadth

One reason for a wider discussion is that a change to one part of the API invariably affects other parts: users who become accustomed to using object literals in one area of the API will naturally expect them to be supported in related areas. For example, if we support object literals for 2D primitives, users will almost certainly expect that we support object literals for 3D primitives. If we don't, we'd introduce inconsistencies and hurt predictability. If the issue ended there, we might not need a larger discussion, but there's a reasonable chance that further review would reveal a much larger scope.

More generally, discussions of the pros and cons of object-literal syntax tend to recur in a variety of areas. Here are just a few examples where the trade-offs of this syntax played a role:

  • [Async/await setup()] Here's a comment I made that touches on the usage of object-literal syntax in relation to async/await.
  • [Custom shapes][Typography][noise] Based on a discussion starting with this comment from @dhowe, we introduced splineProperty(key, value) / splineProperties(properties), where properties is an object literal, partly for consistency with textProperty(key, value)/textProperties(properties). The same pattern is being considered for noise.
  • [p5.Element] Here's an older issue with a proposal to add object-literal parameters to the style() method of p5.Element (coincidentally, I could've used that feature about an hour ago haha). One option here could be to follow the singular-plural pattern used for text, splines, and possibly noise: we could add a plural styles() method that takes an object as a parameter (that'd add an extra method, but it'd eliminate an overload).

The last issue is a good example for a couple of reasons. First, I think it was mostly tabled due to a lack of clarity about the pros and cons. Second, I was able to quickly come up with an alternative solution that might be more consistent with other parts of the p5 API, but only because I wasn't viewing that issue in isolation. A broader discussion of object literals, and a summary of the key points, could be a helpful resource for making decisions in these kinds of situations.

Depth

Here, I'll return to 2D primitives, to indicate some of the complexities involved. I'm about to make a big list of problems, so before I do that... I want to acknowledge that the contributors who worked on this area moved p5 forward. It's just that this area is in need of some attention.

A sample of problems to fix before we consider extending the API

In its current state, this may be one of the most problematic areas of the p5 API, so I think @jht9629-nyu is right to propose improvements of some kind. However, I think it'd make sense to resolve the existing bugs and inconsistencies before we consider adding overloads; otherwise, we'd just propagate the bugs to new parts of the API. For now, I'll share an excerpt of a comment I made on another issue, which contains a few examples of the problems (I've edited it lightly):

  • [Inconsistency] Some 2D primitives support detail in WebGL mode, but not all. For example, ellipse() does, but circle() doesn't. Similarly, rectangles do according to the reference, but square doesn't.
  • [Inconsistency] Triangle doesn't work in WebGL mode at all.
  • [Inconsistency] Point allows vector arguments, but the other primitives don't.
  • [Complexity] The 2D primitives can take up to 14 parameters, which makes some of them extremely hard to read.
  • [Bug] None of the examples in the reference demonstrate detailX or detailY for rect(), but we can modify them to see what happens. For rectangles, we get rounded corners instead of detail (I don't think it's possible to distinguish detail arguments from rounding arguments, so this bug is inherent to the API).
  • [Bug] For quadrilaterals, you either get nothing (e.g. if the detail is set to 1), or you seem to get only part of the stroke.

Unfortunately, some of these issues may require breaking changes to fix, including the bugs. This is an extreme scenario where the API actually makes it impossible for some features to work. Fortunately, I proposed an initial idea for fixing these bugs, and it seems unlikely the changes would affect a lot of users (if no one posted bug reports, then these features are probably underused). I'll quote @davepagurek on this, from a separate conversation:

That might justify allowing a breaking change in a minor version, if the benefit is high, the current implementation is not fully functional everywhere, and the change doesn't affect much?

Any thoughts, @ksen0? Of course, time is running out before the 2.0 release, but for a case like this, maybe there's a way to schedule a breaking change after the initial release, as long as it's before 2.0 becomes the default version in the Web editor? After the initial breaking change, there'd be more time to address the other problems. Otherwise, I suppose we could wait until a potential 3.0 release.

No obvious one-size-fits-all solution

TL;DR When it comes to readability, p5.Vector objects might make the most sense in some cases, and object literals might make the most sense in others.

If we can clean things up, then I think it'd make sense to consider how to make the API more readable, which appears to be the intent of the current issue. Parameter lists that contain a mixture of different kinds of data, like in arc(), present one challenge. Another challenge is parameter lists that are simply long.

A good rule of thumb for the maximum length of parameter lists is to use the limit on human working-memory capacity, which recent estimates suggest is (very roughly) four chunks of information.

The quad() function takes up to fourteen parameters. There are different ways we might try to address this. To start, two of the parameters don't seem to work. So there may only be 12 that we really need to consider. And these 12 parameters fall naturally into four chunks! Exactly as we might've wished. That leads to a partial solution.

p5.Vector objects

Continuing with the quad() example, each chunk has three parts: $x$, $y$, and $z$ (in WebGL mode). The problem is that these chunks aren't represented as such in the code. This is an instance of the data clump code smell, and in this case, I think the smell really does indicate a problem. The typical solution is to replace each data clump with a meaningful object. There's an off-the-shelf solution here: we could replace each chunk with a p5.Vector object.

Object literals probably make less sense here, since we already have a built-in object with a user-friendly interface for representing exactly this kind of data. Moreover, using p5.Vector arguments would improve consistency with point(), which already accepts a p5.Vector argument as input. Adding an extra overload by itself adds extra complexity, but in this case, that loss may be outweighed by the gain in consistency and the option to write more readable code.

Even so, this is only a partial solution. It works well for point(), line(), triangle(), and quad(). It doesn't work as well with some of the other functions.

Object literals

For the other 2D primitives, object literals may be a better solution. We could try to ensure consistency by having each 2D primitive support primitive arguments, p5.Vector arguments, and object-literal arguments... but that adds a lot of overloads, and all the costs that go along with that.

Another option might be to represent coordinates using p5.Vector arguments in some places and using object literals in other places (e.g. {x, y, d} for a circle). That really doesn't seem ideal, but it might work as a compromise, if the syntactic grouping aligns with a logical grouping. Specifically, it might help that point(), line(), triangle(), and quad() naturally go together. But it's possible that a little thought will show this approach to be too problematic.

Next steps

Currently, object literals are used in the p5 API, but typically only in more advanced features. There doesn't seem to be a consistent guideline for when to use them, but Dave has already kicked off the discussion by sharing his own guidelines. I propose that after 2.0 is released, we pick up the conversation there, and then proceed in stages:

  1. Once I restart the API audit discussion, I can create a sub-issue (or a similar forum) where we can discuss guidelines for including object literals in the p5 API more generally; that discussion might start with a review of the existing use of object literals as well as candidate features that might benefit from them. If anyone in the current discussion wants me to ping them once I do that, they can let me know here, and I can add to them list of usernames in the Volunteers section of the audit proposal.
  2. Create a separate issue to address bugs and inconsistencies in the existing API for 2D and 3D primitives.
  3. Restart discussion on the current issue about extending the API for 2D and 3D primitives with object literals, p5.Vector objects, or maybe some other solution to the readability issues.

Note: For now, I'm assuming we'll have a little leeway to make breaking changes after 2.0 is released, in the extreme case I mentioned above (where existing features can't be fixed without an API change). If that's not the case, I can try to adjust this proposal.

@jht9629-nyu jht9629-nyu changed the title Allow object literals for functions in additional to positional parameters Allow object literals for 2D-Shape functions in additional to positional parameters Mar 31, 2025
@jht9629-nyu
Copy link
Author

Thank you @GregStanton, @ksen0, @davepagurek for your detailed feedback. I'm new to p5 development process so all your comments are very helpful.

I've changed title to emphasize the focus of this proposal is on the place where must p5 beginners start.

I mistakenly had 2.0 in the original title thinking it was the best way to express what I'd find most useful in the next major upgrade based on my teaching experience.

I plan to update the proposal with these important points:

  • these set of improvements be done in a fully backward compatible way so it does not have to be tied to a 2.0 release.
  • 3D primitives will be considered but the focus of this proposal is 2D shape

Again I appreciate all the feedback and I'll be reviewing them all and updating this proposal overtime.

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

6 participants