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

Add arcVertex() for creating arcs with beginShape()/endShape() #6459

Open
1 of 17 tasks
GregStanton opened this issue Oct 7, 2023 · 33 comments
Open
1 of 17 tasks

Add arcVertex() for creating arcs with beginShape()/endShape() #6459

GregStanton opened this issue Oct 7, 2023 · 33 comments

Comments

@GregStanton
Copy link
Collaborator

GregStanton commented Oct 7, 2023

Increasing Access

This feature increases access in two main ways:

  1. It addresses an inconsistency that may be an obstacle for beginners.
  2. It allows the user to perform common tasks with less mathematical knowledge.

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
  • Internalization
  • Friendly Errors
  • Other (specify if possible)

Feature request details

Proposal

Add arcVertex() to the set of vertex functions in p5, to address a limitation of beginShape()/endShape().

The problem

Two of p5’s curve-vertex pairs are incomplete. In the table below, asterisks indicate missing items.

p5 curve p5 vertex
line/polyline line() vertex()
quadratic Bézier quadratic()* quadraticVertex()
cubic Bézier bezier() bezierVertex()
Catmull-Rom spline curve() curveVertex()
circular/elliptical arc arc() arcVertex()*

Of the missing items, arcVertex() is the most important. Arcs are the only type of SVG subpaths not supported by p5’s beginShape()/endShape(). Without them, p5's most powerful shape-making feature cannot make some of the most basic shapes (e.g. lines and Bézier curves can only approximate circles). This leads to complications for both users and developers.

The solution

Provide a new function called arcVertex() based on SVG’s arc command. Applications include the following:

  • Rounded corners: Allow users to round corners of polygons (not just rectangles) without approximations.
  • Shape objects: Retain exact data for built-in and custom shapes in a common format usable by beginShape()/endShape().
  • SVG paths: Work with SVG paths by translating SVG path commands directly to p5 vertex commands.

Specification of arcVertex()

The following may serve as a draft reference page and usage tutorial.

Description

Used to draw an arc along a circle or ellipse (oval), from one vertex (point) to another. The arc may be drawn along any ellipse, even one that’s been rotated. (If the ellipse is too small to connect the two vertices, its size is automatically adjusted.)

The first vertex is specified with vertex(). The second vertex is specified with arcVertex(). Both are specified within beginShape() and endShape(), with no parameter passed to beginShape().

The arc itself is specified by indicating the shape and rotation of the ellipse (w, h, and angle), together with its type (MINOR or MAJOR) and its direction (COUNTERCLOCKWISE or CLOCKWISE).

If DEGREES is passed to angleMode(), angle may be specified in degrees; otherwise, angle should be specified in radians. If RADIUS is passed to ellipseMode(), the shape parameters are interpreted as half of the width and half of the height: arcVertex(20, 50, ... ) results in an arc drawn along an ellipse of width 40 and height 100.

Usage

beginShape();
vertex(x1, y1);
arcVertex(w, h, angle, type, direction, x2, y2);
endShape();

Syntax

arcVertex(w, h, angle, type, direction, x2, y2)

Parameters

Name Description
w Number: width of ellipse along which arc is drawn
h Number: height of ellipse along which arc is drawn
angle Number: angle by which ellipse is rotated relative to x-axis
type Constant: either MAJOR or MINOR
direction Constant: either COUNTERCLOCKWISE or CLOCKWISE
x2 Number: x-coordinate of ending vertex
y2 Number: y-coordinate of ending vertex

Tutorial

arcVertex tutorial

Related issues

@welcome
Copy link

welcome bot commented Oct 7, 2023

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!

@Qianqianye
Copy link
Contributor

Thanks @GregStanton for the suggestion. I'm inviting the current p5.js Core Stewards to this discussion @limzykenneth, @davepagurek, @ChihYungChang, @teragramgius, @tuminzee, @Zarkv, @robin-haxx, @Gaurav-1306. Please feel free to leave your comments about this feature request. Thank you!

@Gaurav-1306
Copy link
Contributor

Great suggestion.

I would like to work on this one. Let me get back with details of implementation.

@GregStanton
Copy link
Collaborator Author

GregStanton commented Oct 14, 2023

Amazing! Thank you so much @Gaurav-1306! And thank you to @Qianqianye for bringing the others into the conversation!

Verbal version of the tutorial
In case it helps anyone, I'll elaborate slightly on the tutorial. We want to draw an arc along the specified ellipse, from the first vertex to the second vertex. But we need to decide how to do that. We can start by imagining moving the ellipse onto the two vertices. There are two ways to do this; in the example shown, we get one copy of the ellipse attached from below and one attached from above. The vertices divide each ellipse copy into two arcs (major and minor). This means that there are four possible arcs. The type and direction parameters allow the user to specify which one they want (e.g. the minor clockwise arc is one option). Here's a quick p5 sketch that shows how the type and direction parameters allow you to specify four possible arcs (with different colors for each arc).

Specification for out-of-range parameters
It seems like a good idea to be consistent with how the SVG specification handles parameters that are out of range. This comes from the W3C Candidate Recommendation for SVG 2.

I hope this helps!

@davepagurek
Copy link
Contributor

I think this makes sense as a way to increase consistency between modes of drawing shapes.

Do you have thoughts on the current API, based on SVG path arc commands, vs something more like the vanilla J's canvas arcTo API? https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#elliptical_arc_curve The API for this one seems a bit more approachable at first glance (although your naming is much clearer than the flag names in the SVG spec!)

@GregStanton
Copy link
Collaborator Author

GregStanton commented Oct 15, 2023

Thanks for your feedback @davepagurek! I'm glad you like the naming. I'm also glad you asked about other APIs. I asked myself the same question, and I think it's good to document the rationale here.

The problem with SVG's arc command, and how to mitigate it
It's commonly acknowledged that SVG's arc command is not the simplest. Yet, it does work, and we can mitigate any difficulties. The friendlier API I proposed should help. It should also be possible to improve the graphic tutorial I shared.

The problem with canvas's arcTo(), and why we can't mitigate it
A circular arc made with arcTo() may end at an unspecified point. (Really, arcToward() is a more accurate name than arcTo().) This conflicts with p5's functions, which are explicitly vertex based1: quadraticVertex(), bezierVertex() etc. all have "vertex" in the name, and the eponymous vertex is always represented by the final parameters.

The problem with canvas's ellipse(), and why we can't mitigate it
The native canvas API also has a method that creates an elliptical arc as a sub-path. It's simply called ellipse(). However, its API has the same problem as arcTo()'s.2

The benefits of SVG's arc command
SVG's arc command is vertex based, so it's consistent with p5's commands. By adopting a version of it, p5 is not just internally consistent, but it's also consistent with all of SVG's path commands.3 This has benefits for both users and library developers. Users familiar with SVG will immediately understand how arcVertex() works, and library developers should be able to directly translate SVG path commands to p5 vertex commands.

Footnotes

  1. The curveVertex() function of p5 introduces a subtle variation, in which the vertex is actually a control point (it guides the curve, but the curve does not pass through it). Nonetheless, with a separate function call, the user is able to specify where the curve will end.

  2. The SVG arc command and the canvas ellipse() method implement two different parameterizations of elliptical arcs. The the SVG implementation notes helpfully refer to these as the endpoint parameterization and the center parameterization, respectively. I thought a little about whether a simpler endpoint parameterization is possible, but I'm not sure if it could be simple enough to justify diverging from both the canvas and SVG APIs.

  3. Internal consistency is actually cited in the SVG implementation notes as a reason for the design of its arc command. The notes are clearly written and address the same issue that we face.

@davepagurek
Copy link
Contributor

That makes sense. In the MDN example, they also follow up arcTo with lineTo to ensure it always ends at the target point. That might be a way of using an API like the native canvas one while still preserving the quality of ending at the specified point?

Aside from the API of arcTo appearing simpler, I'm mostly considering it further because I assume we'll have to end up translating our API into an arcTo command for 2D mode anyway. Although it'll have to be turned into line segments for WebGL, so we'll be doing that part from scratch regardless. I don't have a strong preference, so if there are more issues with a combined arcTo + lineTo, I'm happy to go with the API that you suggested!

@GregStanton
Copy link
Collaborator Author

GregStanton commented Oct 15, 2023

@davepagurek I really appreciate you helping to vet this idea! I think it's always good to thoroughly vet an addition to an API, especially a core feature like this.

I'll respond with some additional downsides of arcTo() that I see, followed by one approach that may address your concern about the implementation of arcVertex().

Downsides of arcTo()
Here are a few issues in addition to it not ending at a specified point:

  • It doesn't actually create an arc. It creates a piecewise path made up of linear and circular pieces. It already does this without a follow-up call to lineTo(), since the arc may need to be connected to the most recent path point with a line segment. So, it's not a curve primitive in the same way that a pure circular arc is.
  • It only makes circular arcs, rather than elliptical arcs, so it's less general. If we try to generalize it and also take away the linear pieces, we end up with something like the SVG arc command.
  • It cannot be used with p5's existing vertex commands to reproduce general SVG paths, since it only produces circular arcs. That seems to limit applications.1

In general, I think arcTo() is mainly provided as a more convenient way to round corners. Its behavior can be achieved in principle with canvas's ellipse() and lineTo() methods. The more general, primitive method is ellipse(), which actually draws not just ellipses but also elliptical arcs. My feeling is that the priority is to add general primitives first.2

Implementing arcVertex()
Instead of using arcTo() for 2D mode, one option is to convert the endpoint parameterization of SVG's arc command to the center parameterization of canvas's ellipse(), and then draw it with canvas's ellipse().3 For the mathematical details on this exact conversion, see the SVG implementation notes. The authors of the SVG spec anticipated the need to convert to other APIs.

As always, I'm eager to hear feedback on any of this!

Footnotes

  1. For example, the p5.teach add-on library by @two-ticks adds a dependency (anime.js) in order to animate typeset math expressions in the style of Manim. It uses MathJax to convert the expressions to SVG, which is then animated with anime.js. I suspect this requirement could be satisfied with p5 alone if it were possible to reproduce SVG paths with beginShape()/endShape(). I'm very curious to hear others' thoughts on this.

  2. If we want to add a utility function that creates composite paths like arcTo(), its convenience would need to be weighed against the complication it introduces: it wouldn't correspond neatly to any p5 curve primitive. The convenience might well outweigh that complication! But it's something to consider.

  3. When I was looking into this originally, one thing that surprised me is that p5's own arc() and ellipse() are not implemented with the canvas's ellipse(). The implementation approximates the ellipse with Beziér curves. Maybe this was easier to use with WebGL, since the Beziér curves had already been adapted for that?

@davepagurek
Copy link
Contributor

Instead of using arcTo() for 2D mode, one option is to convert the endpoint parameterization of SVG's arc command to the center parameterization of canvas's ellipse(), and then draw it with canvas's ellipse(). For the mathematical details on this exact conversion, see the SVG implementation notes.

Thanks for the link, this will be helpful for implementing later! It looks like ellipse() will be able to do what we need it to.

It only makes circular arcs, rather than elliptical arcs, so it's less general. If we try to generalize it and also take away the linear pieces, we end up with something like the SVG arc command.

This is a good point. Do you think we can make a version of the API where the elliptical aspects of the command come last and are optional, so that if they're missing we can default them to circular arcs? I assume that will likely be the most common use.

When I was looking into this originally, one thing that surprised me is that p5's own arc() and ellipse() are not implemented with the canvas's ellipse(). The implementation approximates the ellipse with Beziér curves. Maybe this was easier to use with WebGL, since the Beziér curves had already been adapted for that?

I'm actually not sure why the 2D mode uses Bezier paths. The WebGL mode version just uses trig functions.

@GregStanton
Copy link
Collaborator Author

GregStanton commented Oct 17, 2023

Replying to #6459 (comment) by @davepagurek:

Thanks for the link, this will be helpful for implementing later! It looks like ellipse() will be able to do what we need it to.

Sure thing! I'm glad that helped.

Do you think we can make a version of the API where the elliptical aspects of the command come last and are optional, so that if they're missing we can default them to circular arcs? I assume that will likely be the most common use.

The arcVertex API that I proposed accommodates the circle case in the same way that p5's arc does. As in the first example of the p5 reference page for arc, the user can create circular arcs by choosing the width and height parameters to be equal. I think it's nice to be as consistent as possible, so this seems like a good solution to me.

The only other option that's obvious to me is to do something like p5's ellipse. In that case, it's possible to omit the height parameter, in which case it works just like circle, since the height is automatically set equal to the width. If we try this with arcVertex by moving the width parameter to the end, we'd introduce inconsistencies. Most significantly, arcVertex wouldn't end in vertex parameters like p5's other vertex functions.

I'm actually not sure why the 2D mode uses Bezier paths. The WebGL mode version just uses trig functions.

I got curious, so I did a little digging. It looks like the use of Bézier curves in p5's 2D implementation of arc and ellipse dates to p5's addition of ellipse on July 4, 2013. At that time, the canvas didn't have a native ellipse method.1

Would it make sense to create a separate issue to update p5's implementation of arc and ellipse, as part of modernization efforts? The current implementation is complicated enough that the source comments include a link to a paper on the underlying math; using the canvas's native ellipse should be a pretty significant simplification.

Footnotes

  1. According to MDN's browser compatibility table for CanvasRenderingContext2D, bezierCurveTo was implemented in browsers as early as 2005, whereas the ellipse method didn't appear in any of the listed browsers until November 2013.

@davepagurek
Copy link
Contributor

I think that all makes sense, thanks for bearing with me and fur such thorough explanations! I think this API looks good to me then.

For the 2d implementation of ellipse, making another issue for that would be great, thanks!

@GregStanton
Copy link
Collaborator Author

GregStanton commented Oct 18, 2023

Replying to #6459 (comment) by @davepagurek:

I think that all makes sense, thanks for bearing with me and fur such thorough explanations! I think this API looks good to me then.

Absolutely! I'm glad I'm not the only one considering the design from various perspectives.

For the 2d implementation of ellipse, making another issue for that would be great, thanks!

Done: #6485


Do you have a recommendation for the next step? I'm not sure if @Gaurav-1306 has been following the discussion, but they expressed interest in working on the implementation. Hopefully, our discussion will help with that. Here are the most relevant parts:

Specification for out-of-range parameters
It seems like a good idea to be consistent with how the SVG specification handles parameters that are out of range. This comes from the W3C Candidate Recommendation for SVG 2.

Implementing arcVertex()
One option is to convert the endpoint parameterization of SVG's arc command to the center parameterization of canvas's ellipse(), and then draw it with canvas's ellipse(). For the mathematical details on this exact conversion, see the SVG implementation notes.

@Gaurav-1306
Copy link
Contributor

Gaurav-1306 commented Oct 19, 2023

Hey I have been following your discussions and would still like to implement this.

Let me understand all that you guys discussed and get back with the implementation details.

@Gaurav-1306
Copy link
Contributor

Just a little update
I have been really busy with my university exams. I will start working on this from the day after tomorrow.
I will get back on it really soon. Thanks for the patience 😁

@Gaurav-1306
Copy link
Contributor

hello

So I have been reading your discussions. I have a few doubts that can help me write a better code.

Provide a new function called arcVertex() based on SVG’s arc command. Applications include the following:

@GregStanton in your original implementation detail you stated to use the SVG arc command but after the discuss has been over, the conclusion was to use canvas's api

Instead of using arcTo() for 2D mode, one option is to convert the endpoint parameterization of SVG's arc command to the center parameterization of canvas's ellipse(), and then draw it with canvas's ellipse().3 For the mathematical details on this exact conversion, see the SVG implementation notes. The authors of the SVG spec anticipated the need to convert to other APIs.

Now the Implementation.

  • we are using the SVG arc command and then we are using the canvas' ellipse() to draw the actual arc. Also in loss terms when we need to draw a circle, we just equal the value of height and width.
    are we on the same page?

Now regarding the 2D and 3D implementation of the function, do we need to add more parameter(a third vertex to know the location to end in 3D space) to the function or this implementation detail can be understood with SVG api?
my doubt basically is how are we planning to implement the 2d and the 3d versions together?

again sorry for so many doubts and thanks for the help!

@GregStanton
Copy link
Collaborator Author

GregStanton commented Oct 30, 2023

Hi @Gaurav-1306!

Thanks for taking a look, and I appreciate your questions! The short answer: I originally suggested that we base the arcVertex() API on SVG, and that is still our plan; later, I proposed one way to implement arcVertex() using a native canvas method. I know that may be confusing, and you had several questions. So, I'll try to explain everything in a longer answer below. If you have any follow-up questions, please don't hesitate to ask!

Parameterizations of elliptical arcs: SVG vs. canvas

Elliptical arcs can be drawn with SVG. They can also be drawn with the native canvas API. However, SVG and the canvas use different sets of parameters.

  • In the SVG command for elliptical arcs, the user describes the arc with the endpoint parameterization: the horizontal and vertical radii of the ellipse, the angle by which the ellipse is rotated, the type of arc to be drawn along the ellipse (major or minor), the direction of the arc (clockwise or counterclockwise), and the endpoint of the arc. 1
  • In the canvas method for drawing elliptical arcs, the user describes the arc with the center parameterization: the horizontal and vertical coordinates of the center of the ellipse, its horizontal and vertical radii, the angle by which the ellipse is rotated, and the starting and ending angle of the arc.

arcVertex(): API vs. implementation

For arcVertex() in p5, the user will enter one parameterization and the implementation will use a different one.

  • API: We want the user to enter parameters from the endpoint parameterization. This is because all of p5's vertex commands take endpoint coordinates as the final parameters.
  • Implementation: The code that makes arcVertex() work will use the native canvas API, since p5 draws shapes to the canvas.

This means that we'll have to convert from one parameterization to the other.

Implementation options

I'll describe two possible approaches for the conversion from the endpoint parameterization to the center parameterization. In the preceding comments, I only described the first option, but the second option is likely easier.

  • Option 1: Do the conversion mathematically. This conversion is described in the SVG implementation notes I linked to earlier. Then, pass the converted parameters to the canvas ellipse() method (that method draws full ellipses and elliptical arcs along part of an ellipse).
  • Option 2: Do the conversion automatically. This is possible because the canvas's native Path2D() constructor accepts SVG path definitions as input, and it outputs a path that can be drawn to the canvas. 2 In other words, it does the conversion for us.

In Option 2, you'd still need to convert the arcVertex arguments into an SVG path definition; however, that basically just requires you to combine them into a string with the correct format. 3 Here's a quick sketch I made that illustrates the use of Path2D() for drawing elliptical arcs from SVG path data.

Out-of-range parameters

For the sake of completeness, I'll include the earlier point about out-of-range parameters:

It seems like a good idea to be consistent with how the SVG specification handles parameters that are out of range. This comes from the W3C Candidate Recommendation for SVG 2.

2D vs. 3D

I'll share my current understanding, but I think @davepagurek will be able to help more with this.

To start, p5 has separate renderers for 2D (p5.Renderer2D) and 3D (p5.RendererGL). SVG paths and the Path2D paths only apply to 2D. If you want to just start with the 2D implementation, I think that would be great! For 3D, I suspect we'd do Option 1 (the mathematical conversion from the endpoint parameterization to the center parameterization); then I imagine we'd use cosine and sine to generate a sequence of vertices along the ellipse, and connect those with tiny approximating line segments.

Finding where to put your code is another issue. The vertex functions are defined in vertex.js. For the 3D versions, vertex.js calls out to the 3D renderer's vertex functions, which are defined in 3d_primitives.js. There may be other relevant code elsewhere in the codebase.

Special case

To create a circular arc, the user would just use equal values for the width and the height. as you said. This shouldn't require any extra programming on your part.

Footnotes

  1. For the SVG arc command, the starting point is specified separately. Basically, SVG paths are built out of multiple pieces, and the starting point of an elliptical arc is taken to be the endpoint of the most recent piece that was added.

  2. The Path2D interface also supports all the usual path methods.

  3. When converting the arcVertex() arguments to an SVG string, you'd include an A or a for arc, you'd use 0 and 1 instead of MINOR and MAJOR, etc. The MDN reference is a good place to learn about the format.

@Gaurav-1306
Copy link
Contributor

Thanks, @GregStanton for such a great replay, all my confusion is gone.

I will start working on the 2D implementation as of now.
Where to put the code is a good question but I always thought that it should go in vertex.js, I will look more into this.

One last thing, do you think choosing option 1 can help with performance in any way, in option 2 we are using the API two times one for conversation and another one for drawing ellipses. I am comfortable either way but I feel that direct conversation can benefit performance because we are not calling the API two times.

@Gaurav-1306
Copy link
Contributor

Gaurav-1306 commented Nov 1, 2023

Hello everyone, while I have been working on the implementation I came up with the following code. Do give your feedback. a request form @davepagurek can you explain to me how should I draw on the canvas, more formally when we use Canvas API, we have to call the this._renderer._context.stroke and make sure that it gets rendered on the canvas. I wanted to ask which .html file is responsible for the final rendering. the following snippet may make it more clear
Screenshot 2023-11-01 at 10 26 46 PM

p5.prototype.arcVertex = function(w, h, angle, type, direction, x2, y2) {
  p5._validateParameters('arcVertex', arguments);

  // Convert endpoint parameterization to SVG path definition
  const arcPath = `M ${x1} ${y2} A ${w} ${h} ${angle} 0 ${type === MAJOR ? 1 : 0} ${direction === COUNTERCLOCKWISE ? 1 : 0} ${x2} ${y2}`;

  // Create a Path2D object from the SVG path definition
  const path2D = new Path2D(arcPath);

// i think this is where the draw the ellipse from canvas api will go.

  // Draw the path to the canvas
  this._renderer._context.stroke(path2D);

  return this;
};

@Gaurav-1306
Copy link
Contributor

@davepagurek
I have been working on the implementation and I came up with a function. I think the logic is right here, but you can double-check it once.

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

p5.prototype.arcVertex = function(w, h, angle, type, direction, x2, y2) {
  const x1 = vertex[0];
  const y1 = vertex[1];
  const centerX = (x1 + x2) / 2;
  const centerY = (y1 + y2) / 2;

  const path = new Path2D();
  const rotation = angle * (Math.PI / 180);
  const radiusX = type === 'MAJOR' ? w / 2 : h / 2;
  const radiusY = type === 'MAJOR' ? h / 2 : w / 2;

  const startAngle = Math.atan2(y1 - centerY, x1 - centerX);
  const endAngle = Math.atan2(y2 - centerY, x2 - centerX);

  path.ellipse(centerX, centerY, radiusX, radiusY, rotation, startAngle, endAngle, direction === 'COUNTERCLOCKWISE');

  ctx.stroke(path);
};

there are few think i don't understand, like if i return this in the code will the p5renderer be able to handle the ctx.stroke, i am saying this because other drawing curves do the same. also where should the code go is codebase. anything that can help with this.
thanks!

@Gaurav-1306
Copy link
Contributor

Gaurav-1306 commented Nov 4, 2023

@GregStanton can you can pitch in too, need help🙂

@GregStanton
Copy link
Collaborator Author

GregStanton commented Nov 4, 2023

Hi @Gaurav-1306!

I'll try to answer your questions by walking you through the steps I took to understand the relevant parts of the codebase. I haven't studied all the code in detail yet, but hopefully this will point you in the right direction.

Note: I wish I could explain these implementation details more concisely! To help you understand it all, I'll include links to the exact lines of code that I refer to, when possible.

Where to put the code

We can start by investigating the implementations of the existing functions: beginShape(), endShape(), vertex(), quadraticVertex(), bezierVertex(), and curveVertex(). These are all defined in vertex.js. However, when we read their definitions, we notice that these functions don't directly draw anything. They do contain logic that separates the 2D and 3D cases. Let's consider these cases one at a time.

The 2D case

The main job of each function:

To be consistent with this organization, you would define arcVertex() in vertex.js, just like the vertex functions above. The actual drawing code would go elsewhere...

Where the drawing happens:

To understand how things actually get drawn, we need to track down the 2D version of endShape() that's called from vertex.js. In that file, we see the 2D version of endShape() is simply called endShape(); we know it's the 2D version because it belongs to this._renderer, which in this case is the 2D renderer. To find out what the 2D version of endShape() does, we can try looking for its definition in p5.Renderer2D.js.

When we find it, we see that it contains a lot of code. However, much of the code is dedicated to the different modes (POINTS, LINES, TRIANGLES, etc.). Those apply only to vertex(). The modes won't apply to arcVertex(), so we can ignore that part of the code. The cases most relevant to us are isCurve, isBezier, and isQuadratic. In each of these cases, the corresponding method of the native canvas API is called. You'll most likely add an isArc case that will call the canvas's native ellipse() method.

What this means for you:
You'll need to put code in multiple files.

  • You'll define arcVertex() in vertex.js.
  • You'll probably define isArc at the top of vertex.js and incorporate it into arcVertex() and endShape() in the same file.
  • You'll include the 2D drawing code in the definition of endShape() in p5.Renderer2D.js, as explained above.
  • You may want to check that no other code needs to be modified.

Note: The existing implementation seems to contain dead code.
The code repeatedly checks if shapeKind is equal to constants.POLYGON, but there appears to be no such constant defined in constants.js. If this is old code left over from an earlier implementation, it seems we could simply remove these checks.

The 3D case

Since you're just working on the 2D case (at least for now), I won't say much about this. I'll just mention where most of the relevant code lives, so that we have a convenient reference for both the 2D and 3D cases. Each function in vertex.js calls out to an internal 3D version by the same name, defined on p5.RendererGL, but these definitions are found in different places:

The 3D versions of quadraticVertex(), bezierVertex(), and curveVertex() call on internal properties and methods of the 3D renderer, such as _bezierCoefficients(). Those are defined in p5.RendererGL.js.

How to implement the actual drawing code

Earlier, you asked about performance in relation to the two options I proposed above for converting between parameterizations. In both options, the conversion has to occur. It's just a matter of whether you do it yourself, or whether you call a native canvas function that does it for you. I'm not sure that performance is a major consideration here, but others may have more insight. Regarding your code snippets, I'll make separate comments on each of your approaches below.

Automatic conversion: Your snippet with the automatic conversion, which we previously called Option 2, looks like it may work with a few changes:

  • Your SVG path definition starts by moving to $(x_1, y_2)$. I think you meant $(x_1, y_1)$; however, you need to think about where $(x_1, y_1)$ comes from. The user doesn't pass that information into arcVertex(). It comes from a call to vertex() that precedes the call to arcVertex().
  • The SVG command doesn't take width and height. It takes radii. So you'll need to cut those in half.
  • The SVG command takes the angle in degrees. You need to account for p5's angle mode, which defaults to radians (I just updated the specification in the original post to make this explicit).
  • You seem to have an extra argument of 0 preceding the flag parameters.
  • In the SVG path definition, CLOCKWISE is represented by 1.
  • Using your variable names, you should be able to call this.drawingContext.stroke(path2D) in the appropriate place, rather than this.renderer._context.stroke(path2D).

Mathematical conversion: Your snippet with the mathematical conversion, which we previously called Option 1, is more problematic. One of the problems is that the calculation you're using for centerX, centerY gives the midpoint of the segment joining the endpoints, but that isn't always the same as the center of the ellipse. You'll want to use the conversion that's described in the SVG implementation notes. If you haven't learned enough math to understand that yet, let us know.

Thanks so much for working on this! I know I didn't address every detail, but I hope this comment clears up some confusion!

@Gaurav-1306
Copy link
Contributor

@GregStanton I can't thank you enough you take the time to explain things in so much detail. This is not going to be as easy as I thought it would be. Let me read and understand everything. ✌🏻

@GregStanton
Copy link
Collaborator Author

@Gaurav-1306 I'm really glad to help!

@GregStanton
Copy link
Collaborator Author

Hi @Gaurav-1306!

I just wanted to let you know that I submitted issue #6560, which should help us simplify this part of the codebase. It will probably make sense to resolve that issue first. It'll make it easier to implement arcVertex(). If you were to implement arcVertex() right now, it'd probably end up needing to be refactored to be consistent with the changes proposed in #6560 anyway.

Also, if you check out the proof of concept that I shared along with that issue, you'll see that I did already include an implementation of arcVertex() to test out my ideas. But there's still work to be done. The code I wrote only handles the 2D case right now, and it will eventually need to be taken out of the informal proof of concept and incorporated directly into p5.

@Gaurav-1306
Copy link
Contributor

Thanks for the update @GregStanton, i will work on that issue and then as you said will come back for the arcvector.

@GregStanton
Copy link
Collaborator Author

GregStanton commented Jan 27, 2024

Update: Oh, wow. I just had a new idea for a different arcVertex() API that would be more consistent with #6766.

Specifically, we should be able to let the user specify an elliptical arc with commands as simple as arcVertex(x, y). They'd just need to specify where they want it to start and end, along with three intermediate points that they want the arc to pass through. Mathematically, that data is enough to uniquely determine the arc passing through those points. Altogether, it'd look like this:

beginShape();
arcVertex(x0, y0);
arcVertex(x1, y1);
arcVertex(x2, y2);
arcVertex(x3, y3);
arcVertex(x4, y4);
endShape();

(Just for now, I'm ignoring the 3D case and texture coordinates, for simplicity.)

This would be a very nice simplification, and if we adopt the proposal in #6766, it'd mean that arcVertex() would take the same input as all the other vertex functions. We'd need to think through the implications, though. Some initial questions to consider:

  1. Although the syntax is easier to remember and understand, would this be easier or harder for users in common situations? For example, if they want to create a rectangle with rounded corners, which of these APIs would make that easier? What about other common uses?
  2. This API is different from the SVG API. It shouldn't be hard to convert between formats, though. For example, if we use a bit of linear algebra to determine the general equation of the ellipse from the user-provided vertices, we can easily convert back and forth between the general equation and the center parameterization. And here's how we can convert back and forth between the center parameterization and the endpoint parameterization that's used by the SVG spec.
  3. Are there any problematic cases to consider, in terms of how the user-supplied points are ordered or arranged in space?

@davepagurek
Copy link
Contributor

I think this is a cool idea, I like the symmetry it has with other APIs! Maybe for comparison, how would it look to draw a rounded rectangle corner with this API? Does it offload a lot of math onto users to generate the three points in a case like that?

Also, since quadraticVertex generally starts with a regular vertex, would your example be something like this to match?

beginShape();
vertex(x0, y0);
arcVertex(x1, y1);
arcVertex(x2, y2);
arcVertex(x3, y3);
arcVertex(x4, y4);
endShape();

@GregStanton
Copy link
Collaborator Author

GregStanton commented Jan 31, 2024

Thanks for your feedback @davepagurek! I'll try to answer your questions concisely.

First question

Maybe for comparison, how would it look to draw a rounded rectangle corner with this API? Does it offload a lot of math onto users to generate the three points in a case like that?

Task 1: Round the corner of a rectangle

The original API seems easier to use in this case, as we’d expect, but it's still manageable with the new API for users who know a bit of math. The essential lines of code in each case are copied below, from a couple of quick sketches I made. (The second sketch just uses the point() function to visualize points that would be passed to arcVertex() if the new API were implemented.)

Original API: arcVertex(d, d, 0, MINOR, CLOCKWISE, x + d / 2, y); [full sketch]
New API: arcVertex(cX - r * cos(i * dt), cY - r * sin(i * dt)); [full sketch]

In general, when the user is already thinking of an ellipse in terms of parameters like width, height, and rotation angle, the new API will require them to use extra math to generate points along that ellipse.

Task 2: Click on points, and generate an ellipse that passes through them

This would be simple with the new API, but for most users, it would be prohibitively difficult with the original API.

Summary: Although the new API offloads more math in a common use case, it makes it more likely that users will be able to accomplish both tasks above.

Second question

Also, since quadraticVertex generally starts with a regular vertex, would your example be something like this to match?

For the sake of this discussion, I'm assuming we'll implement the proposal for the new vertex function API. In that proposal, there is no quadraticVertex(), since bezierVertex() can be used for Bézier curves of all orders. Also, in that proposal, it's no longer necessary to mix commands to create a single primitive (e.g. all Bézier curves can be created using only bezierVertex() commands and no vertex() commands, just as Catmull-Rom splines can already be created entirely with curveVertex() commands); for details, see the third problem in the list of ten problems solved by that proposal. In the same way, the idea here is to be able to create arcs using only arcVertex() commands.

Another possible benefit

With the new API for arcVertex() and the API in #6766, vertex() works with any number of vertices greater than one, bezierVertex() works with any number of vertices greater than two, curveVertex() works with any number of vertices greater than three, and arcVertex() could actually work with any number of vertices greater than four! When more than five vertices are specifed to arcVertex(), we could fit the points with an approximating arc. This is an important task in computer vision, so there’s plenty of literature on it. For example, based on a quick glance, this paper may provide a good solution.

Edit: Added the vertex() case (supports any number of vertices greater than one). Note that with the proposal in #6766, we no longer need to use vertex() to start a new primitive like a Bézier curve, so I don't think we'd ever call it just once.

@davepagurek
Copy link
Contributor

For the first question, it seems like we won't really be able to handle both use cases in the same API cleanly, and neither choice would be a regression, so I'm good going with either option.

For the second question, I think I may have missed some details about connected segments of different types, so I've left a comment on the vertex function proposal with more info there. The main concern is that by having variable numbers of function calls per segment, it requires something else to mark the start/end of segments (and I think begin/endContour in its current form describes something else, so it'd mean having a separate set of markers for separate paths that clip each other with winding order than our markers to separate segments.) I like the idea of a closest-fit, although it has the same issue of requiring markers. Anyway, we can talk about that more on the other PR; as long as we're consistent with arc vertices then we're good!

@GregStanton
Copy link
Collaborator Author

Thanks @davepagurek! I'll respond to your points and summarize some of the discussion so far.

arcVertex(x, y) vs. arcVertex(w, h, angle, type, direction, x2, y2)

Either way, it will at least be possible to create elliptical arcs with beginShape()/endShape(), so both are an improvement over the current API. I'm currently leaning toward the arcVertex(x, y) API.

Benefits of arcVertex(x, y)

Here's an updated list of benefits, including some of the ones I mentioned above:

  1. Conceptually, the API is easier to understand (even if sometimes it requires more math).
  2. It's more consistent with [p5.js 2.0 RFC Proposal]: New API for vertex functions #6766.
  3. It supports a new use case that was previously impractical for users (generating an ellipse through any five points).
  4. It opens up the possibility of fitting more than five points (an entirely new use case), which further improves consistency with the other vertex functions. (None of them has an upper bound on the number of vertices that may be specified. More precisely, vertex() works with any number of vertices greater than one, bezierVertex() works with any number of vertices greater than two, curveVertex() works with any number of vertices greater than three, and arcVertex() could work with any number of vertices greater than four!)
  5. We'd avoid having to support multiple function signatures (Problem 5 in #6766). Otherwise, we'd need to support arcVertex(x, y) for the starting vertex and arcVertex(w, h, angle, type, direction, x2, y2) for the rest of the segment, in order to avoid having to mix vertex types for a single primitive (Problem 3 in #6766). (We could allow mixing just for arc segments, but that would be an inconsistency, since the hope is to eliminate mixing for all the other segment types.)

It'd be nice to avoid having to do the extra math for rounded corners, but right now, I'm thinking these other points are pretty compelling. And, at least it'll be possible to make rounded corners with elliptical arcs; in the current version of p5.js, it's not possible at all.

Disambiguating segments

I replied about this point in #6766. Thanks!

Closest fit

Glad you like the idea! Regarding distinct markers for (a) distinguishing primitives (e.g. segments) in composite shapes and (b) distinguishing contours, I think we've found a solution in the discussion on #6766.

@GregStanton
Copy link
Collaborator Author

Reminder: This p5.js tutorial opens by saying "This short tutorial introduces you to the three types of curves in p5.js: arcs, spline curves, and Bézier curves." In the summary, it specifies that "You can’t make continuous arcs or use them as part of a shape." This is something we'd want to update.

@TheWhoove
Copy link

Any progress to push this forward? It would be so useful.

@limzykenneth
Copy link
Member

@TheWhoove We currently plan to implement this feature along with larger vertex refactor in p5.js 2.0. What can be helpful would be to work on a potential proof of concept in the dev-2.0 branch so some of the larger details can be worked out before we confirm the implementation.

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