[css-houdini-drafts] Proposal: Custom functions for modifying CSS values (#857)

AmeliaBR has just created a new issue for https://github.com/w3c/css-houdini-drafts:

== Proposal: Custom functions for modifying CSS values ==
One thing I haven't seen yet in Houdini is a proposal for declaring generic custom functions that modify an input set of CSS tokens to return a new arbitrary CSS value.  In other words, custom versions of `calc`, `min`, and so on. Even `env` and `var` functions fit that definition.

@TabAtkins has a section on custom functions in his [CSS Extensions](http://tabatkins.github.io/specs/css-aliases/#custom-functions) brainstorm, but it doesn't go as far as an actual proposal.

This proposal was sparked by [this thread](https://mobile.twitter.com/Rumyra/status/1091985177205723137) by Ruth John about custom color modifier functions.  Native color modifier functions are a long-requested CSS feature that keeps getting tangled in debates about the best options and syntax and so on. But colors aren't the only area. There are lots of open issues on CSS WG for new funtions, e.g., for [a `random()` function](https://github.com/w3c/csswg-drafts/issues/2826) or [trigonometric functions](https://github.com/w3c/csswg-drafts/issues/2331) or conditionals or the long-spec'd/rarely-implemented `toggle()` and so on.

So here's a "collection of interesting ideas" for further discussion…

## Strawman API:

Defining and registering a custom function would look a lot like the [Paint API](https://drafts.css-houdini.org/css-paint-api/); this discussion focuses on the unique aspects.

Calculations would happen in a `ValueWorklet` (name to be bikeshod).

For now, the ValueWorklet global scope would only need the registration functions and core JS / globals that are always exposed to worklets.

Custom functions would be defined within classes like

```js
class MyColorModifiers {
    static get inputProperties() { return ['--color-space']; }
    static get inputArguments() { return ['<color>', '<percentage-number>']; }
    static get returnType() { return '<color>'; }

    darker([startingColor, delta], styleMap) {
       /* convert percentage delta values into numbers */
       /* find the correct modifier function based on the color space */
      /* return a new CSSColorValue object.
          ...which first needs to be defined in Typed OM */
    }
    lighter([startingColor, delta], styleMap) {
       /* same structure as before, but a different operation */
    }
}
```

The `inputProperties` and `inputArguments` getters work the same as in the Paint API, except that I'd expect arguments to be more important than properties (but I've given an example here of a property that you might want to declare once to affect all functions).

One issue with `inputProperties` is what to do with cycles, if custom functions can be used in any property.  My first instinct is to say "use the inherited or initial value instead", but I haven't figured out the details.  If that doesn't work, there may need to be a more explicit way for a custom function to access the inherited value of at least the property where it is being used (so that you could create a `toggle()` style function).

The `returnType()` getter is new, since we need to define what the output will be for the parser to know if we're using our function in a sensible way.  Output is a single CSS Type, defined using the same [set of syntax strings](https://www.w3.org/TR/css-properties-values-api-1/#supported-syntax-strings) as for the `inputArguments` method.

In my demo, I've created a single class with two different functions that both have the same signature and return type, because why not? That means that the method name would need to be part of the registration function:

```js
registerCustomFunction("--darker", MyColorModifiers, "darker");
registerCustomFunction("--lighter", MyColorModifiers, "lighter");
```

Alternatively, we could stick with the Paint API model: each registered function could be required to have its own class (which maybe extends a superclass that defines the input and return type getters), and the calculations would always use a function with a standard name (`calculate`, `value`, or something like that).

In the CSS, it could be used anywhere the return type is expected:

```css
color-space: lab;
background-color: --darker(var(--theme-color), 70%);
border: 2px solid --lighter(var(--theme-color), 1.5);
```

An alternative would be to register the custom function as a regular ident (no `--` at the beginning), and then have a single standard wrapper function where the specific function name is the first argument to the wrapper, similar to how `paint()` works:  `f(<ident>, <arguments>#)`

```css
color-space: lab;
background-color: f(darker, var(--theme-color), 70%);
border: 2px solid f(lighter, var(--theme-color), 1.5);
```

The registered function is called whenever either the function arguments or the input properties are changed.  It's passed the arguments as an array (in my example code, destructured in the function signature) and then the properties in a map.

Things I haven't thought of yet:

- At what level should input argument types and return types be enforced. If they aren't enforced by the parser, what is the fallback behavior?

- How does this interact with computed values and inheritance?


Please view or discuss this issue at https://github.com/w3c/css-houdini-drafts/issues/857 using your GitHub account

Received on Sunday, 3 February 2019 17:05:52 UTC