Mapbox-gl-js: Add color modifying expressions

Created on 14 Nov 2017  Â·  11Comments  Â·  Source: mapbox/mapbox-gl-js

mapbox-gl-js version: 0.42.0

Problem

Developers want to use GL expressions for common color operations. A common use case is to indicate feature selection on a hover or click event of a feature by changing the color saturation.

There are no color functions in Expressions that can calculate changes in colors without using a third-party library such as chroma.js or the built-in color-space interpolations in GL JS Style Spec today.

Proposal

Add two new color expressions:

  1. saturate

    • Changes the saturation of a color by manipulating the Lch chromaticity.

    • ["saturate", color, saturation scale] : color

  2. desaturate

    • Desaturates a color by manipulating the Lch chromaticity. Opposite of saturate.

    • ["desaturate", color, saturation scale] : color

cc @anandthakker

feature

Most helpful comment

While this is getting implemented, here is the workaround expression to implement desaturate with graying, i.e. it decreases the color value and makes the color more mid gray.

"text-color": [
            "let", 
                "rgba", ["to-rgba", ["to-color",  ["get", "color_property"]]],

                ["let",
                    "r", ["number", ["*", 255, ["at", 0, ["var", "rgba"]]]],
                    "g", ["number", ["*", 255, ["at", 1, ["var", "rgba"]]]],
                    "b", ["number", ["*", 255, ["at", 2, ["var", "rgba"]]]],
                    "a", ["number", ["at", 3, ["var", "rgba"]]],
                    ["let",
                      "avg", ["+", ["*", 0.299, ["var", "r"]], ["*", 0.587, ["var", "g"]], ["*", 0.114, ["var", "b"]]],
                      ["let",
                        "desat_r", ["+", ["*", 0.4 , ["var", "avg"]], ["*", 0.4 , 128], ["*", 0.2 , ["var", "r"]]],
                        "desat_g", ["+", ["*", 0.4 , ["var", "avg"]], ["*", 0.4 , 128], ["*", 0.2 , ["var", "g"]]],
                        "desat_b", ["+", ["*", 0.4 , ["var", "avg"]], ["*", 0.4 , 128], ["*", 0.2 , ["var", "b"]]],
                        ["rgba", ["var", "desat_r"],  ["var", "desat_g"],  ["var", "desat_b"], ["var", "a"]]
                      ]
                    ]
                ] 
          ]

All 11 comments

Let's broaden this to cover other color operations we may want -- I think it makes sense to consider the whole set together as we decide on how to add these.

As @davidtheclark mentioned previously, the CSS color modifying spec proposal might be good inspiration: https://drafts.csswg.org/css-color/#modifying-colors

While this is getting implemented, here is the workaround expression to implement desaturate with graying, i.e. it decreases the color value and makes the color more mid gray.

"text-color": [
            "let", 
                "rgba", ["to-rgba", ["to-color",  ["get", "color_property"]]],

                ["let",
                    "r", ["number", ["*", 255, ["at", 0, ["var", "rgba"]]]],
                    "g", ["number", ["*", 255, ["at", 1, ["var", "rgba"]]]],
                    "b", ["number", ["*", 255, ["at", 2, ["var", "rgba"]]]],
                    "a", ["number", ["at", 3, ["var", "rgba"]]],
                    ["let",
                      "avg", ["+", ["*", 0.299, ["var", "r"]], ["*", 0.587, ["var", "g"]], ["*", 0.114, ["var", "b"]]],
                      ["let",
                        "desat_r", ["+", ["*", 0.4 , ["var", "avg"]], ["*", 0.4 , 128], ["*", 0.2 , ["var", "r"]]],
                        "desat_g", ["+", ["*", 0.4 , ["var", "avg"]], ["*", 0.4 , 128], ["*", 0.2 , ["var", "g"]]],
                        "desat_b", ["+", ["*", 0.4 , ["var", "avg"]], ["*", 0.4 , 128], ["*", 0.2 , ["var", "b"]]],
                        ["rgba", ["var", "desat_r"],  ["var", "desat_g"],  ["var", "desat_b"], ["var", "a"]]
                      ]
                    ]
                ] 
          ]

Woah, thanks for sharing @mkv123 !

Looking at this, I'm seeing a bug in our to-rgba expression -- I think it should return the rgb components scaled to 0-255, since that's how the rgba expression consumes them. I'll file that as a separate ticket

Looking over the color-mod spec, one translation into expressions would be something like:

  • ["color-{r,g,b,a,h,s,l}-=", color, number] -- produces a color with the given channel set to the given value
  • ["color-{r,g,b,a,h,s,l}-+", color, number] -- produces a color with the given channel adjusted by the given amount
  • ["color-{r,g,b,a,h,s,l}-*", color, number] -- produces a color with the given channel multiplied by the given amount
  • ["color-rgb{,a}-{+,*}", color, r, g, b] -- shorthand for adding/multiplying in all channels
  • ["color-{tint,shade}", color, percentage] -- implements tint and shade
  • ["color-blend-{rgb,hsl}{,a}", color, color, n] -- implements blend and blenda
  • ["color-contrast", color, percentage?] -- implements contrast

So increasing the saturation by 10 percentage points would be ["color-s-+", color, 10].

This translation:

  • Would obey the semantics of color-mod as far as possible, including the treatment of achromatic colors.
  • Doesn't have an explicit percentage syntax as in CSS. r,g,b values would always be interpreted as numbers in the range 0-255. h values would always be interpreted as angles in the range 0-360. s,l values would always be interpreted as percentages in the range 0-100.
  • Doesn't have subtraction operators. Those would be awkward in a kebab-case naming convention (e.g. color-r--). Add a negative value instead.
  • Doesn't implement hwb-space operations. Are those necessary?

An alternative model is http://lesscss.org/functions/#color-operations.

The proposal in https://github.com/mapbox/mapbox-gl-js/issues/5676#issuecomment-356800938 adds 32 new operators with five distinct syntaxes. It feels like the operator name is being turned into a mini-language, whereas the structured expression syntax exists because we didn’t want to stuff a mini-language inside a string (https://github.com/mapbox/mapbox-gl-style-spec/issues/362#issuecomment-161857532).

What if we add a single new operator that applies a “color transform” or “color filter” to a color? A color transform could take the form of an object specifying various parameters for the transformation.

I have the opposite take, @1ec5 -- stuffing all of the potential color operations inside of a single operator with an object-based parameterization system feels like the sort of meta syntax we're aiming to avoid, while extending the language with additional operators is what we explicitly designed for. (I'm not sure what you mean by "five distinct syntaxes" -- as I see it, my proposal doesn't introduce any new syntax.)

I do think that the color-mod-spec-derived names are cryptic, and would support using more familiar operator names, like "color-saturate" for "color-s-+" and "color-lighten" for "color-l-+".

@jfirebaugh: Why not pre-parse the arguments, in a sense, by making them items in the array? Instead of ["color-r-+", ...] we'd have ["color", "+", "r", ...]. Or instead of ["color-blend-rgb", ...] we'd have ["color", "blend", "rgb", ...]? (Or even ["color", ["blend", "rgb"], ...]?)

I was looking through the spec for something similar that exists and the closest I found was interpolate. There, it seems like we opted for multiple arguments instead of operators like interpolate-linear, interpolate-exponential, interpolate-cubic-bezier. I'm curious if you think there are key features that differentiate this set of expressions from those?

The major difference is that the type signature of interpolate is the same whether interpolation is linear, exponential, or cubic-bezier. (And the fact that step has a different number of arguments is one reason we split curve into step and interpolate.)

In contrast, the number and types of arguments to color operators vary. This would make a single "uber-operator" difficult to implement and document.

In https://github.com/mapbox/mapbox-gl-js/issues/5676#issuecomment-357098168, I was thinking of interpolate, specifically the first argument. It may be that the style specification views linear, exponential, and cubic-bezier as context-specific operators in their own right, but to me (and to the NSExpression translation), it’s not much different than an options argument to a function.

I just wanted to see if there has been any movement on this. I'm currently working on a visualization where the missing piece seems to be the ability to subtract saturation from HSL colors from layers beneath, and I haven't had a lot of success finding what I'm looking for in the official documentation.

@audaciouscode This is not on our roadmap at this time. We have a general interest in extending the expressions system over time but balance that against other necessary work. I'd suggest reaching out to Mapbox Support if you have a specific example as they may be able to help provide an alternate implementation.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

foundryspatial-duncan picture foundryspatial-duncan  Â·  3Comments

iamdenny picture iamdenny  Â·  3Comments

aendrew picture aendrew  Â·  3Comments

mollymerp picture mollymerp  Â·  3Comments

samanpwbb picture samanpwbb  Â·  3Comments