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.51k stars 3.29k forks source link

Add a function for interpolating between multiple colors #6959

Closed RandomGamingDev closed 1 month ago

RandomGamingDev commented 5 months ago

Increasing access

While interpolating between 2 colors is easy with the lerpColor() function, interpolating between multiple can be annoying, and very difficult for beginners, especially for how commonly used and popular this feature is.

Most appropriate sub-area of p5.js?

Feature request details

Create a function that accepts a list of colors and a value to interpolate between all of them.

davepagurek commented 4 months ago

Sorry for the delay on this! I'm going to tag the color stewards, what are your thoughts? @paulaxisabel, @SoundaryaKoutharapu, @mrbrack, @TJ723, @Zarkv, @SkylerW99, @ramya202000, @hannahvy, @robin-haxx, @hiddenenigma

Also @limzykenneth, since you've been looking into color module things

limzykenneth commented 4 months ago

I'm thinking it may be more useful to have a gradient implementation that gives extra flexibility for the placement of intermediate colors. With lerp, it is not really defined how lerping with multiple colors should work so I wouldn't necessarily try to invite something here. I had a look at Unity and it also only does the straightforward two colors lerp only, would be great to see how other tools/libraries handle this if they do it at all.

davepagurek commented 4 months ago

One piece of inspiration: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createLinearGradient

They don't have an API for getting a specific color along the gradient, so this API is for something somewhat different, but they have one for specifying color stops at different locations.

RandomGamingDev commented 4 months ago

I'm thinking it may be more useful to have a gradient implementation that gives extra flexibility for the placement of intermediate colors

If the user's already specifying specific positions for each of the colors then using an if ... else ... chain would be more than easily enough for their task. The main issue that paletteLerp's trying to solve is when people want them equally spaced out without having to type out a bunch of extra code just to achieve the same result, especially if they don't have much coding experience and want a solution that works with a list of any size. I think that, while less flexible, paletteLerp still provides significant advantages and the users that require a more flexible solution already have that easily taken care of as well, with even beginners able to do it.

davepagurek commented 4 months ago

I think stops are at least worth considering for the future. At least personally, probably half of the gradients I've done with more than two colors have had non-evenly-spaced stops in order to get the visual effect I'm looking for, and it's probably a concept familiar to people who start in digital design software and then get into coding via p5.

If the colors aren't evenly spaced out, e.g. white at 0, red at 5%, green at 25%, blue at 100%, doing a lookup could look something like this:

let c
if (mix < 0.5) {
  c = lerpColor(white, red, map(mix, 0, 0.05, 0, 1, true))
} else if (mix < 0.25) {
  c = lerpColor(red, green, map(mix, 0.05, 0.25, 0, 1, true))
} else {
  c = lerpColor(green, blue, map(mix, 0.25, 1, 0, 1, true))
}

I could see how having to jump from just lerpPalette([white, red, green, blue], mix) to the above would be a big jump for some, where people might request the ability to specify color stops. I think it's not a bad idea to think ahead about whether or not there could be an easy way in the future to extend it to that so that we aren't locking ourselves in.

One way could be to have overloads like:

lerpPalette(palette: p5.Color[], mix: number)
lerpPalette(palette: p5.Color[], stops: number[], mix: number)

...and default to an even spacing. Do you think something like that would be a good balance of achieving the evenly-spaced use case while letting us grow into the non-even use case? If so, we could start with the simpler one, and move into the latter whenever someone has the bandwidth to implement it.

limzykenneth commented 4 months ago

Also in terms of implementation, instead of a new function, perhaps we can just implement overloading on lerpColor instead so if the first argument is an array, it lerps between all of them.

RandomGamingDev commented 4 months ago

Do you think something like that would be a good balance of achieving the evenly-spaced use case while letting us grow into the non-even use case? If so, we could start with the simpler one, and move into the latter whenever someone has the bandwidth to implement it.

That sounds like a great idea although we might want to play around with the parameters being used a bit since it might be annoying for beginners to have 2 separate arrays that are connected to one another only through their index values especially since it can result in hard to debug bugs.

Also in terms of implementation, instead of a new function, perhaps we can just implement overloading on lerpColor instead so if the first argument is an array, it lerps between all of them.

That could be another possibility, but that would depend on which one p5.js prefers. Personally, I prefer having a separate function made for doing this, but making it an overload instead would be fine as well.

davepagurek commented 4 months ago

That sounds like a great idea although we might want to play around with the parameters being used a bit since it might be annoying for beginners to have 2 separate arrays that are connected to one another only through their index values especially since it can result in hard to debug bugs.

Agreed about keeping the two array indices in sync being hard. One option could be to allow something like:

// evenly spaced
lerpPalette([white, red, green, blue], mix)

// with stops
lerpPalette([
  { color: white, stop: 0 },
  { color: red, stop: 0.05 },
  { color: green, stop: 0.25 },
  { color: blue, stop: 1 }
], mix)

I don't think we have array-of-object parameters for anything else just yet? but for this maybe it's worth trading the argument complexity for the potential bugs in keeping two arrays in sync?

RandomGamingDev commented 4 months ago

That sounds like a great idea although we might want to play around with the parameters being used a bit since it might be annoying for beginners to have 2 separate arrays that are connected to one another only through their index values especially since it can result in hard to debug bugs.

Agreed about keeping the two array indices in sync being hard. One option could be to allow something like:

// evenly spaced
lerpPalette([white, red, green, blue], mix)

// with stops
lerpPalette([
  { color: white, stop: 0 },
  { color: red, stop: 0.05 },
  { color: green, stop: 0.25 },
  { color: blue, stop: 1 }
], mix)

I don't think we have array-of-object parameters for anything else just yet? but for this maybe it's worth trading the argument complexity for the potential bugs in keeping two arrays in sync?

I think that it'd probably be better to do something like this:

lerpPalette(
  red, 0.1,
  orange, 0.2,
  yellow, 0.4,
  green, 0.8,
  blue, 1.0,
  lerpVal
);

What do you think?

davepagurek commented 2 months ago

Hey @nickmcintyre, in other issues you've been thinking about how p5 (and JS in general) deals with array parameters, what do you think of this one? is this a pattern that we've seen elsewhere?

nickmcintyre commented 2 months ago

If I understand the original proposal, the core library probably doesn't need a way to mix multiple colors. I could be wrong, so it'd be interesting to see some example sketches.

+1 for considering color stops similar to createLinearGradient. Evenly-spaced stops seem very limited if they're the only option, so I suggest focusing on the more general case.

@davepagurek Regarding array parameters, different libraries take different approaches. For example, synced arrays for plotting, arrays-of-objects for data wrangling, and so on.

@RandomGamingDev The last syntax you suggested seems like the simplest. For discussion's sake, I've included several options for color stops here so we can consider tradeoffs and/or overloads.

lerpPalette(
  white, 0,
  red, 0.05,
  green, 0.25,
  blue, 1,
  amt);

lerpPalette([
  white, 0,
  red, 0.05,
  green, 0.25,
  blue, 1
], amt);

lerpPalette([
  [white, 0],
  [red, 0.05],
  [green, 0.25],
  [blue, 1]
], amt);

lerpPalette([white, red, green, blue], [0, 0.05, 0.25, 1], amt);

lerpPalette([
  { color: white, stop: 0 },
  { color: red, stop: 0.05 },
  { color: green, stop: 0.25 },
  { color: blue, stop: 1 }
], amt);

This feature could make sense as an overload for lerpColor(), though I'm wary of over-overloading.

davepagurek commented 2 months ago

I think the biggest current technical limitation for interleaving colors and stops (the first two examples above) is just that our current jsdoc-based FES system isn't flexible enough to handle it. That just means that this method would need custom error handling (which also would need to respect the case when FES is disabled and avoid checking to save CPU cycles.)

Do you think we can write the docs in the format they'd appear on the site as a first step and see if that's readable enough? Ideally I would want the parameters section to still be useful. I think I'd be ok with interleaving if we can make it fit into our docs format nicely.

Otherwise, options 3 and 5 above are within what our documented type formats support, but do force the user to provide more structure themselves, so I'd use those as a compromise.

RandomGamingDev commented 2 months ago

I think the biggest current technical limitation for interleaving colors and stops (the first two examples above) is just that our current jsdoc-based FES system isn't flexible enough to handle it. That just means that this method would need custom error handling (which also would need to respect the case when FES is disabled and avoid checking to save CPU cycles.)

Do you think we should wait until the upgraded FES with p5.js 2.0? I think that implementing it manually shouldn't be too much of an issue, especially if we move amt to be the first argument.

Do you think we can write the docs in the format they'd appear on the site as a first step and see if that's readable enough? Ideally I would want the parameters section to still be useful. I think I'd be ok with interleaving if we can make it fit into our docs format nicely.

I think that formatting it similarly to commands that can accept multiple points like bezier (https://p5js.org/reference/p5/bezier/), but ending with a ... and then n- parameter section would be good enough especially since the examples should be more than clear enough. For example: 1st color, 1st placement, 2nd color, 2nd placement, ..., nth color, nth placement.

Otherwise, options 3 and 5 above are within what our documented type formats support, but do force the user to provide more structure themselves, so I'd use those as a compromise.

Yeah, it might be worth it to go with a compromise here, but it'd be best if we could get a general consensus before making it. If we can't settle for the solution I suggested due to current FES issues and don't want to wait until the updated FES, or if the updated FES doesn't support this either, I'd choose option 5 since it's the cleanest one that still works with the current FES.

RandomGamingDev commented 1 month ago

We've decided to go ahead with:

lerpPalette([
  [white, 0],
  [red, 0.05],
  [green, 0.25],
  [blue, 1]
], amt);