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.65k stars 3.32k forks source link

textToPoints() to allow 2D Array of points within paths for separating letters #4086

Open ffd8 opened 5 years ago

ffd8 commented 5 years ago

Nature of issue?

Most appropriate sub-area of p5.js?

Which platform were you using when you encountered this?

Feature enhancement details:

Preparing my first p5.js pull request, so making an issue first.

This will extend textToPoints() to allow the return of a 2D Array, [paths][points], since as a single array of points, there will always be a line drawn between the letters if placed within a begin/endShape().

Figured the smoothest way to implement this, is adding it as an option when calling the function, and imagined that option to be separatePaths : true.

Code is already done.. just have to read up on Docs for contributing and happy for feedback on PR.

dhowe commented 5 years ago

A couple of questions:

  1. How does the 2d return array handle multiple paths for letters? Is there a way for the caller to know which paths correspond to which letters? For example, in the string "%#!"

  2. Does the proposed code still work for all values of the other two options: sampleFactor and simplifyThreshold?

  3. Do we have other functions in p5.js that change return type based on options? If not, we might also consider a new function called 'textToPaths'.

ffd8 commented 5 years ago

1 – At the moment, each piece of a letter/glyph is simply handled as its own path in the first dimension of array, followed by their collection of points in the 2nd dimension. This is how it's handled in Geomerative (Processing library, where there's an example called getPointPaths). The example "%#!" would result in 6 paths (3 for the %), so one uses a loop within loop to avoid the connecting line. Of course with something like a 'B' or 'p' where there's a path over another one.. it gets tricky for which one should be filled and which one empty.. (also an issue in Geomerative). Usually best to just use strokes and avoid the issue. Here it could be possible to use a 3D array – just like how the function breaks things down [glyphs][paths][points] – but that might get too loopy?! 🤪

2 – Yup, it's using the exact same point extraction, so both of those options work smoothly.

3 – That was my question.. of course it's probably not ideal to have a different type of return... so I wondered if there was another function in the library that does so for documentation style guiding. At first I thought of having another function, but it would be 99% copy + paste code to then maintain... OR it's another function that passes a request to the textToPoints() function with this extra option that's not documented on textToPoints() (but is available) and listed as it's own function ie. textToPaths()? Hard to say which is easier or most compatible for adjusting ones code.. changing an option and knowing you get something else out, or having to write a whole different function name? perhaps the later is more clear? Then if making a 3rd option to get the [glyphs][paths][points] = textToGlyphs()? (that's not so clear... also seems like something to embed as an option perhaps even on original function)

ffd8 commented 5 years ago

Hope there's another function with multiple returns, as that might be smoothest implementation IMHO. Then having two additional options to textToPoints(), separatePaths (returns 2d array [paths][points]), separateGlyphs (returns 2d array [glyphs][points].. still connected line within letter), activating them both returns 3d array [glyphs][paths][points].

dhowe commented 5 years ago

@lmccart any thoughts on the API implications here (and on whether any other API functions change return types based on options) ?

@ffd8 have you tested the current implementation with the full set of unusual glyph path types: open, closed, intersecting, etc. ( %, !, g, &, ...)

ffd8 commented 5 years ago

@dhowe Yup, it's working fine with all special characters, since it's the same textToPoints() function, just now returns more detail/depth than currently done.

Here's an example for testing it out: https://editor.p5js.org/ffd8/sketches/jQANRzdQ2

dhowe commented 5 years ago

Right - I guess I was thinking about how to do filled paths... but this is still not possible given the data returned (part 3 of your example). I suppose another option in terms of the API would be to keep the 1D array but separate each path by some constant value, maybe Number.NEGATIVE_INFINITY. This would allow a user to realize your example sketch, but not change the return type for the function (and not introduce a new function to the API).

ffd8 commented 5 years ago

Yeah filled paths are tricky.. Just added a function to export glyphs too and learned that of course this works great for letters like B with inner being opposite/different than outer, however it's tricky for something like %, where the o has two paths.. but the / not... anyways, those are just small details that aren't an issue if one uses outlines.

Here's an example with a custom build for exporting glyphs too: https://editor.p5js.org/ffd8/sketches/eCNcHelzT

I'd propose to have the two options:

Regarding the idea of sending a special flag whenever the path changes, might be smooth for just outlining the paths, though would still require special if() to catch when an extra begin/endShape() are needed – but could make it more difficult once using rotate or translate of the paths.

dhowe commented 5 years ago

could make it more difficult once using rotate or translate of the paths how so?

ffd8 commented 5 years ago

Here's a demo sketch: https://editor.p5js.org/ffd8/sketches/A_1KGFkac

The demo hopefully shows how one more flexible per level of separation. Tried to simulate end of path 'flags' (faked it with dist) in top example, but then in order correctly close each shape, one probably has to refer back to the first vertex in the path (storing var outside loop)... and to specifically talk to a certain path, one would also have to add a counter within the end/beginShape moment.

For type experiments, this separation is really helpful... maybe want to only distort in 2nd path of a glyph.. or fade out letters per path in a glyph/string?

Regarding the return in documentation, adding a few words could make it clear, together with 3 examples showing the difference and brief description in optional params description?

from:

an array of points, each with x, y, alpha (the path angle)

to:

a single or multi-dimensional array of points, each with x, y, alpha (the path angle)

ffd8 commented 5 years ago

@lmccart could we get your insight for this toTextPoints() feature enhancement?

How best to implement the return of points isolated within paths + glyphs for more precise typo experiments (request came from students):

Idea 1

Extend the optional inputs:

let pointsInPaths = font.textToPoints(txt, 0, 0, fSize, {
    sampleFactor: 5,
    simplifyThreshold: 0,
    separatePaths: true // returns: [paths][points]
    separateGlyphs: true // returns: [glyphs][paths][points] (supersedes separatePaths)
  });

So many semantic options... happy for better suggestions:

Idea 2

Add 2 new functions that simply call the original function with additional options: -textToPaths() or textToPointsInPaths() -textToGlyphs() or textToPointsInGlyphs() or looong, textToPointsInPathsInGlyphs() 😬

// p5 coder types:
let pointsInPaths = font.textToPaths(txt, 0, 0, fSize, {
    sampleFactor: 5,
    simplifyThreshold: 0,
});

// behind the scenes, wraps above function with added option (pseudo code):

function textToPaths(inText, inX, inY, inSize, inOptions){
  inOptions.separatePaths = true;
  return textToPoints(inText, inX, inY, inSize, inOptions);
}

Concern is this might produce repeated documentation (since options are the same and require same description as the normal textToPoints().

Personally I'm for option 1. Curious if there are other ideas? In any case, all about adding examples to demonstrate usage of returned 2D and 3D arrays.

lmccart commented 5 years ago

Hi all, interesting discussion here. We're focused on the priorities for the 1.0 release right now so I don't think I'll have the bandwidth to weigh in on this until after that's done.

ffd8 commented 4 years ago

After a few recent messages asking about the status, here's an example, where the linked p5_textToPoints.js file can be snagged and used to temp override textToPoints(): https://editor.p5js.org/ffd8/sketches/TaPWHTaH (fixed a bug I had in previous versions above that killed function w/o options)

With 1.0 released 🎉– I'd like to bring this up again. To keep it a really modest modification, I'd suggest option 1 above, simply adding optional params to the existing function based on the level of detail/separation one wants from the type.

Basic (current):

let points = font.textToPaths('hello', 0, 0, 50);
// returns points[pts]

Options (current):

let points = font.textToPaths('hello', 0, 0, 50, {
    sampleFactor: 5,
    simplifyThreshold: 0
});
// returns points[pts]

Separate Paths Option (proposed):

let points = font.textToPaths('hello', 0, 0, 50, {
    sampleFactor: 5,
    simplifyThreshold: 0,
    separatePaths: true
});
// returns points[paths][pts]

Separate Glyphs Option (proposed):

let points = font.textToPaths('hello', 0, 0, 50, {
    sampleFactor: 5,
    simplifyThreshold: 0,
    separateGlyphs: true
});
// returns points[glyphs][paths][pts]

Examples would be given in the docs for each optional usage.

davepagurek commented 2 years ago

Hi, I'm moving some discussion here from another issue!

As someone who uses Typescript bindings, option 2 would have the benefit of being more easily typed (there are type overloads that one can use, but it means typing of the options argument would be required.) That said, TS support shouldn't be the priority, so I'll defer to someone else for picking between the two. @ffd8 thanks for writing your implementation, it works great!

The one other thing I'd want to bring up is that, in 2D mode, if you want to fill the shapes, you'd have to rely on the even-odd fill rule in order to fill shapes with holes correctly. begin/endContour lets you do that, but it doesn't work in p5 unless there are non-contour vertices too. Here's a version of your older sketch showing how you'd have to use this right now: https://editor.p5js.org/davepagurek/sketches/cUjihInOf Basically, the first path is treated as vertices, and all subsequent paths as contours. Ideally, I'd also want to let users just use contours for all paths. If this blows up the scope too much it can be a follow-up PR, but it should maybe be bundled with this change to avoid having an example that has to explain the workaround.