processing / p5.js

p5.js is a client-side JS platform that empowers artists, designers, students, and anyone to learn to code and express themselves creatively on the web. It is based on the core principles of Processing. http://twitter.com/p5xjs —
http://p5js.org/
GNU Lesser General Public License v2.1
21.38k stars 3.28k forks source link

Add dashed lines to the core API #5670

Open ianstormtaylor opened 2 years ago

ianstormtaylor commented 2 years ago

Increasing Access

Unsure. But I believe it would reduce the mental barriers to a very common goal, which feels like increasing access for people with less math-heavy or programming-heavy backgrounds.

Most appropriate sub-area of p5.js?

Feature request details

P5.js should add dashed lines as a core construct. Dashed lines are really common when working in sketches... either for hierarchical reasons (eg. a less important line) or just pure aesthetics reasons (eg. in generative art).

This has been brought up before in https://github.com/processing/p5.js/issues/3016 and https://github.com/processing/p5.js/issues/3336, but it was waived off then.

There is a "workaround" that involves dipping into the Canvas API's own setLineDash method, but the problem with that is that you can't rely on dashing across rendering contexts... so if you want to switch to rendering with SVG or WebGL you have to find a new approach. And because of this, downstream plugins and addons can't rely on it.

To have a rendering-agnostic approach right now you'd have to do some very heavy math to be able to convert any potential lines, curves, shapes, etc. to a ton of individual segments, which is not only very complex to write, but likely less performant.

I think this is such common functionality—evidenced by both Canvas and SVG (stroke-dasharray) having implemented this natively—that it should be handled in core so that rendering contexts (and external plugins) can build on top of a predictable target.

welcome[bot] commented 2 years ago

Welcome! 👋 Thanks for opening your first issue here! And to ensure the community is able to respond to your issue, be sure to follow the issue template if you haven't already.

davepagurek commented 2 years ago

To have a rendering-agnostic approach right now you'd have to do some very heavy math to be able to convert any potential lines, curves, shapes, etc. to a ton of individual segments, which is not only very complex to write, but likely less performant.

I have an implementation of a fast getPointAtLength method for Bezier curves that I've been using in p5 sketches for a little while now. We could potentially use a method like this to do a rendering-agnostic dash. It would just require knowing how to convert primitives to bezier segments. Catmull-Rom curves already get converted to Bezier, straight lines can be converted easily (and maybe can be special cased in getPointAtLength for even better performance?) Ellipses will already need to be approximated by polygons in WebGL mode so it's maybe not too bad to convert their outlines into polygons with Bezier tangents smoothing them.

golanlevin commented 2 years ago

Respectfully, here's a contrary voice.

In my book for educators, written with Tega Brain, "Code as Creative Medium", we offer the problem of creating one's own dashed line as a suggested assignment for a beginning programmer. Our feeling was that this is comparatively basic problem that integrates a student's understanding of iteration, some basic mathematics, and a good opportunity to use a function like lerp().

Personally, I would be a little sad to see this problem "given away" like this. While having a readymade dashed line would "reduce the mental barriers to a very common goal," I think there's no replacement for people making their own lines in a world of readymade, off-the-shelf solutions — most especially for "pure aesthetics" reasons. Providing a readymade dashed line would likely short-circuit a learner from discovering all of the ways in which self-made dashed lines can be personalized (for example: by varying the dash duty cycle; producing 1D textures by using dashes of multiple different lengths; randomizing subsegment lengths; futzing with the strokeCap on subsegments, etc.).

There are other ways of achieving lightweight lines (thin strokeWeight; transparency).

My recommendation to resolve @ianstormtaylor's request would be to publish a plugin library for dashed lines, which handled more difficult cases (quadratic and cubic Beziers; polyline resampling; ellipses; etc.). I also think that a pointer to the setLineDash Canvas function could be useful in a footnote somewhere in the documentation.

dashed_line

Qianqianye commented 2 years ago

Thank you @ianstormtaylor for sharing the detailed feature request, and @davepagurek and @golanlevin for the comments.

Because we are trying to keep the p5.js API as minimal as possible, the dashed line feature could be a great starting point for an add-on library. Please see the contributor doc of how to create a library. We'd love to include the library in the p5js.org/libraries page if you decide to make one.

So I will close this issue for now for organizational purpose, and please feel free to re-open it when you'd like to share an update of this issue. Thank you!

ianstormtaylor commented 1 year ago

Really disappointing decision IMO—I think P5.js should strive to pave over these rendering layer differences for use cases as common as dashed lines.

@golanlevin I respect your write up, but the reasoning doesn't really make sense to me. It's like arguing that there shouldn't be a rect function because you can just use four lines. Or that there shouldn't be circle because it would "short-circuit a learner from discovering all of the ways in which [arc] can be personalized".

To me it feels like gatekeeping disguised as "helping people learn". I'd argue it's actually a much more compelling learning experience when you can say "you know this simple built-in method? here's how you can recreate it. and now that you've gone to that trouble, here's what you new power you've unlocked".

So many use cases for dashed lines don't need to dive into the intricacies of how dashes can be rendered (eg. imagine a solar system simulation with solid lines for planetary orbits and dashed lines for lunar orbits).

The current state of P5.js requires authors to dip into the Canvas API (and then not being able to use that across contexts) or to go down a big rabbit hole of line dashing algorithms just to get an incredibly basic output that most rendering engines and most graphics applications support natively (eg. Photoshop, Illustrator, Figma, Sketch, etc.).

golanlevin commented 1 year ago

@ianstormtaylor Thanks for your response. I do 100% agree with you that dashed lines feel like a "basic" thing.

But I disagree that they're as simple as four-lines-make-a-rect in their actual implementation. Unlike a rect or circle, there's a lot more possible parameters needed to control them (from the front end) and a lot of computational geometry choices to render them (on the back end) — and therefore many more ways to get dashed lines wrong. Put another way, I submit that Adobe and Figma can afford to throw a team of developers to make dashed lines feel "basic".

In my opinion, I think there's enough subtlety (and enough different points of view) regarding how dashed lines should be "properly" implemented that it would be best to begin with an external library that offers up a possible implementation. Perhaps something similar in operation to p5.scribble.js. In your response, you didn't explicitly address this library suggestion, and I'd be interested to hear your thoughts about this. There's plenty of case history for add-on libraries becoming absorbed into the main body of toolkits, as you probably know.

I don't know enough about how lines/curves are implemented throughout p5. But unless it's trivial to swap in lines drawn with the Canvas-API setLineDash method, I'm concerned that a official implementation made from scratch will open a Pandora's box of complaints—because (for example) it is inefficient, or because it doesn't provide fine-grained control of dash-versus-gap lengths, or because it handles curves and polylines poorly, or because it allows for duty-cycles that don't force a dash on the final point, or because it produces ugly results for ellipses (whose perimeters are non-trivial to calculate), or because the resampled dashes flicker distractingly in response to small coordinate changes, or because the dashes are locked to an invisible lattice (as they were in OpenGLv1).

I regret the way I phrased my reply earlier. I think that making dashed lines is something that feels comparatively basic (and can be learnfully implemented by a bright student to meet their own needs). But this elides a lot of complexity about their actual implementation, and making a solution to meet everyone's needs (including graphics professionals) is another beast entirely.

ianstormtaylor commented 1 year ago

@golanlevin I understand your response, and I agree with you that there are many different ways that dashed lines can be complex-ly defined… People could want every other dash to be rounded, or every fifth dash to be slightly translucent, or dashes to maintain a consistent gap while rounding a curve, etc., etc., etc.

That doesn't mean P5.js needs to throw out the simple case, which solves 90% (or more) of applications, such that beginners are effectively blocked from quickly adding dashed lines to a sketch.

@golanlevin: I don't know enough about how lines/curves are implemented throughout p5. But unless it's trivial to swap in lines drawn with the Canvas-API setLineDash method, I'm concerned that a official implementation made from scratch will open a Pandora's box of complaints…

It should be trivial I believe, yes. It would delegate to Canvas's underlying APIs just like for regular lines and other stroke-related methods.

Other examples of handling these simpler cases... P5 doesn't tell you that to achieve rounded line caps you have to use your own circles. Or to achieve beveled joins you have to mask them yourself. These are all basic stroke primitives.


Examples

This problem is extremely table stakes, evidenced by how often it's implemented in drawing contexts.

Canvas

Here's how Canvas solves it…

ctx.beginPath();
ctx.setLineDash([5, 15]);
ctx.moveTo(0, 50);
ctx.lineTo(300, 50);
ctx.stroke();

SVG

And here's how SVG does it (using the same dash + gap API design):

<line 
  x1="0" y1="5" 
  x2="30" y2="5" 
  stroke="black"
  stroke-dasharray="4 1" 
/>

Figma

Here are the dashed line controls in Figma (same dash + gap API again):

image

Sketch

Here they are in Sketch (same thing):

image

Illustrator

Here is Illustrator (same again):

image

Inkscape

Here it is in Inkscape (same again, and recently added):

image

This is a problem that every vector editing program deals with, and both Canvas and SVG deal with. And they all use a simple but flexible API of dash + gap values with an optional dash offset.


Implementation

I think it should be implemented in P5.js as:

strokeDash(10, 5)
strokeDashOffset(-5) // optional, sets the point at which to start dashes
line(0, 0, 100, 100)

This mimics its other stroke-related APIs which often just call directly into Canvas APIs like strokeCap -> lineCap or strokeJoin -> lineJoin

Which would be equivalent to Canvas's:

ctx.beginPath()
ctx.setLineDash([10, 5])
ctx.lineDashOffset = -5
ctx.moveTo(0, 0)
ctx.lineTo(100, 100)
ctx.stroke()

And SVG's:

<line 
  x1="0" y1="0"
  x2="100" y2="100"
  stroke="black"
  stroke-dasharray="10 5"
  stroke-dashoffset="-5"
/>
golanlevin commented 1 year ago

It would delegate to Canvas's underlying APIs just like for regular lines and other stroke-related methods.

If p5 can indeed delegate dashed lines to the Canvas API, then (FWIW) I've come around and agree that you're right that dashed lines should eventually be implemented into the core. Your proposed API makes sense, too.

Questions remain about the extent to which the p5 WebGL needs to mirror the Canvas implementation. Would its dashes need to resemble the Canvas dashes exactly? I'm also having a hard time even finding an official WebGL solution for dashed lines (though three.js has an attractive demo).

davepagurek commented 1 year ago

Just throwing in my two cents about the technical side of things: I think delegating to the Canvas API in 2D mode should be feasible as I believe we are already drawing everything in one beginPath()/stroke().

WebGL lines already don't mirror 2D, so I think matching exactly isn't doesn't need to prevent adding this if it's plausible we can get something similar working eventually. It definitely isn't trivial to add to WebGL, but I've been looking into getting stroke caps/joins to work more like in 2D mode, and the potential of having dashes can fit into the design. Everything gets converted to line segments right now anyway so it won't take much math to pass the distance along the line in as a vertex attribute, and we can maybe use a fragment shader to hide parts of the dashes. tl;dr it's a lot easier to do this in 2D mode but it's still plausible in WebGL longer term.

ianstormtaylor commented 1 year ago

@golanlevin thanks for keeping an open mind! I really appreciate it.

golanlevin commented 1 year ago

@ianstormtaylor you bet. Though I'm still not sure what happens with more complex types of graphics, such as polylines, when dashing is delegated to Canvas....

limzykenneth commented 1 year ago

I'm reopening this for further discussion from other members of the community and the relevant area stewards. Feel free to hash out some implementation details such as defaults and customisability.

One note from me is that it is good to keep in mind that p5.js don't have to be the first and last thing a beginner coder learn and use, I'd personally much prefer that p5.js can gently lead learners to further explore the rest of the ecosystem and programming in general, even if that means they use p5.js less in their future as a result.

alvinometric commented 10 months ago

A year later, I still think this is a great idea fwiw

sudhanshuv1 commented 3 months ago

If we still prefer to add this feature, I would like to work on this issue! Although I couldn't find any easy to understand implementation of dashed lines in GLSL.

davepagurek commented 3 months ago

Here's a quick sketch with a drawing app that does dashed lines: https://editor.p5js.org/davepagurek/sketches/r6QxdNb5t This GLSL approach is a bit basic and would take extra effort to support caps on the dashes, but could be a starting point.

limzykenneth commented 2 months ago

I'll try to explore this in an addon library for 2D mode and possibly use it to figure out the library interface for 2.0.