d3 / d3-color

Color spaces! RGB, HSL, Cubehelix, CIELAB, and more.
https://d3js.org/d3-color
ISC License
400 stars 91 forks source link

color.fade([delta=0.2]) #32

Closed mbostock closed 5 years ago

mbostock commented 7 years ago

Similar to color.brighter, but for succinctly modifying the opacity channel. It could return a new color whose opacity is max(0, min(1, this.opacity - delta)).

curran commented 7 years ago

Very cool idea! I've come across a need for such a thing when desiring the fill and stroke colors both to be data-driven, but having the fill more opaque than the stroke for a nice "popping" effect.

FWIW, I have been using the following CSS technique to accomplish this:

.mark {
  fill: currentColor;
  stroke: currentColor;
  fill-opacity: 0.3;
}

I learned only recently about the currentColor CSS keyword (HT @seemantk), which will use the value of the color attribute, so you can compute the colors only once with selection.attr("color", ...).

Here's an example: Popping Effect.

image

veltman commented 7 years ago

In the spirit of brighter()/darker(), would it make sense to also have a corresponding method that ups the opacity? (I have no idea what a good verb for "make more opaque" would be)

Alternatively, a single function that takes a factor that multiplies the current opacity:

e.g. color.opacitize(factor)color.opacity = Math.max(0, Math.min(1, color.opacity * factor))

mbostock commented 7 years ago

Perhaps we could have an operator that can apply generically to any channel, including opacity. For example, here’s how you might implement rgb.brighter followed by color.fade:

const blue = rgb("steelblue")
    .withR(r => r / 0.7)
    .withG(g => g / 0.7)
    .withB(b => b / 0.7)
    .withOpacity(o => o - 0.2);

The idea is that the with methods can take either a function or a constant; if a constant, the channel adopts the specified value; if a function, the function is passed the current value, and returns the new value. Here’s another example of “brighter” by converting to Lab:

const blue = lab(rgb("steelblue"))
    .withL(l => l + 18)
    .rgb();

Of course, that’s still quite a bit more verbose than color.brighter. And I wonder if you’d want an operation to modify multiple channels simultaneously. Like, I dunno…

const blue = rgb("steelblue")
    .call(c => rgb(c.r / 0.7, c.g / 0.7, c.b / 0.7, c.opacity - 0.2));

If there were also a generic d3.brighter function that takes a color and returns a brighter color, you could say:

const blue = rgb("steelblue")
    .call(brighter);

You could also easily convert between color spaces while chaining:

const blue = rgb("steelblue")
    .call(lab)
    .call(c => lab(c.l + 20, c.a, c.b, c.opacity))
    .call(rgb);
Fil commented 5 years ago

d3.saturate() / desaturate() would be a useful complement.

there is a version of saturate in https://observablehq.com/@mbostock/working-with-color#saturate

Re: withR/withL, I like the idea, but not much a fan of the syntax.

mbostock commented 5 years ago

I’ve added color.copy which uses Object.assign internally:

function color_copy(channels) {
  return Object.assign(new this.constructor, this, channels);
}

It’s not quite as fancy as the above examples, but you can easily derive a copy of a color with a different opacity as color.copy({opacity: 0.2}).