w3c / css-houdini-drafts

Mirror of https://hg.css-houdini.org/drafts
https://drafts.css-houdini.org/
Other
1.84k stars 141 forks source link

Proposal: Custom functions for modifying CSS values #857

Open AmeliaBR opened 5 years ago

AmeliaBR commented 5 years ago

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 brainstorm, but it doesn't go as far as an actual proposal.

This proposal was sparked by this thread 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 or trigonometric functions 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; 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

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 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:

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:

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>#)

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:

dead-claudia commented 5 years ago

Could this not use functions and named parameters instead to registerCustomFunction? Also, I see a golden opportunity here to call the function with the values as direct parameters since the parameter list is already necessarily static.

function registerColorModifier(name, impl) {
    registerCustomFunction(name, {
        inputProperties: ['--color-space'],
        inputArguments: ['<color>', '<percentage-number>'],
        returnType: '<color>',
        call: impl,
    })
}

registerColorModifier("--darker", (styleMap, startingColor, delta) => {
    /* 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 */
})

registerColorModifier("--lighter", (styleMap, startingColor, delta) => {
    /* same structure as before, but a different operation */
})

It's slightly more boilerplatey, but it simplifies the model a bit and leaves it a bit more streamlined and easy to use, especially when you're defining a lot of them.

tabatkins commented 5 years ago

Overall, this looks very close to what I'd put together; great job!

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).

Yeah, separate class is my plan. Anything else is just adding complexity to the model without much gain, since one can just inherit from a base class, as you say.

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:

I favor defining a -- name directly; I think it looks better and is clear enough. paint(), being specialized for 'display', is well-suited to having a single function name with the author-specified ident being an argument; custom functions, being usable anywhere, are well-suited for their author-chosen names being top-level.

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?

Enforcing at parser level would be ideal, but it means we have to reparse the stylesheet when a function is registered. We purposely chose not to do that for registered custom properties, so I suspect we'll want to be consistent with that.

In other words, a custom function will be, for most purposes, exactly equivalent to a var() function; any property using one will be assumed to be valid at parse time, and flip into "invalid at computed-value time" if something is wrong - either a bad return type, or the JS throws, or whatever.

It would be best if we could avoid this, but we'd have to make an explicit decision to go against our previous precedent. I'll def try.

How does this interact with computed values and inheritance?

Same as var() with a registered custom property, basically: filled in as part of the computed-value process.

Could this not use functions and named parameters instead to registerCustomFunction?

This is a really interesting idea; coming up with a named-parameter syntax and exposing that to the JS as an options bag could skip around a lot of the issues with the more limited syntax rules that custom functions are allowed.


There's a bit more detail specific to functions that we want to expose, which are needed for a good --random() function; you need the ability to specify whether you'll get re-called based on being applied to different properties/elements, in addition to different arguments.

AmeliaBR commented 5 years ago

@isiahmeadows Sorry I missed your comment when you made it. So your idea is to skip the Class declaration and just use an options dictionary instead? I don't see any problem with that at a glance, but I was following the pattern of the existing Houdini APIs. Other people could explain better why the class pattern was chosen for the Paint and Layout APIs. That said, your example is fairly close to the custom property API, so maybe that's a better model.

There's a bit more detail specific to functions that we want to expose, which are needed for a good --random() function

Ah, yes. I had thought of that, but it didn't make it to my write-up.

Giving the custom function access to the property name shouldn't be too much trouble. There'd need to be another static function that declares the dependency on property name which warns the browser that it can't reuse the returned value across properties. And then pass in the property name as an additional parameter in the JS function.

Similarly, another static function (or a registration option, if we go with the class-free proposal) could request that the inherited value gets passed in to the JS function, while warning that the function needs to be re-calculated when the inherited value changes.

Making the function result dependent on the element is a little trickier, since we can't expose raw DOM objects to the worklet. But maybe each unique element could be represented by some sort of hash value that has no meaning of itself but could be used as a seed to a random function to ensure that you always get the same result for the same element but different results for different elements.

tabatkins commented 5 years ago

Yeah, thinking on it a bit, I think it would be another set of dependency specifiers, letting you depend on "element identity" or any specific "element attribute".

If you ask for attributes, you get invalidated when those attributes change, and are passed a Map of those attr value/names. (You can probably also register for "*", so you get all attributes; you'd need this to create something attr()-like, for instance.)

If you ask for "element identity", you get called separately on each element, and get passed, say, a unique integer for each element (with a guarantee that a given element will always give the same integer, and no two elements alive on the page at the same time will give the same integers). That way you can mix the integer into the starting entropy for your --random() function, ensuring you get a different value for each element.

FremyCompany commented 5 years ago

@tabatkins All these "dependency specifiers" are great, but if they don't match what is already implemented by engines, it can be a lot of work to get them implemented. Things like attr are really specific and only work on content for before/after and I would be surprised if the way they work is actually generic enough to hook functions in any property.

So, all in all, while I don't want to be understood as saying that things like random() aren't worth pursuing, it certainly doesn't feel like it is a v1 requirement to me. Let's keep it simple and iterate.

FremyCompany commented 5 years ago

Another reason the element specifier is problematic is that styles are usually shared across elements to save memory, and if any property value can now depend on the specific element it is applied to, then these optimizations will have to be disabled pretty aggressively.

Plus there is a lot of spec work required to manage expectations as elements get removed from the dom, moved somewhere else, or even transferred into another document. These things seem like a lot of spec details and I'm concerned that this could slow down the main feature by a lot.

AmeliaBR commented 5 years ago

As much as I like the idea of eventually being able to polyfill every type of function we might want in CSS, I agree with @FremyCompany that it makes sense to focus on the simpler cases first.

A reasonable division:

  1. Custom Functions Level 1 would cover functions that are context independent & idempotent: you get the same result for the same parameters wherever & whenever it is called.

    Maybe extend "parameter" to also include dependencies on other declared properties, since that is already supported in the Paint and Layout APIs (although that does introduce circularity issues that need to be addressed).

  2. Custom Functions Level 2 could address functions that depend on the context, whether that is the inherited value for the property, the name of the property or its allowed types, attributes on the element, or some element-specific identifier.

tabatkins commented 5 years ago

Maybe extend "parameter" to also include dependencies on other declared properties, since that is already supported in the Paint and Layout APIs (although that does introduce circularity issues that need to be addressed).

No, Paint/Layout have the same styles regardless of what properties they depend on, you just invoke separate painters/layouters for each element. Custom functions would output different styles, tho.

THAT SAID, assuming that custom functions are resolved at variable-resolution time, so they only affect computed values, the entire problem is moot; computed values aren't nearly as shareable as specified values. So there's no problem in having them resolve differently based on the element, any more than there's a problem with em lengths resolving to different px lengths in computed style.

I just checked with @bfgeek on the details of Blink's style system, and he confirms this: while specified styles are heavily shared, computed styles are generally only shared when they're "initial"; as soon as you set a property's value to something else, the element will clone off a new computed style object just for itself. So a custom function won't have a noticeable impact here.

AmeliaBR commented 5 years ago

OK, thinking through all the implications of Tab's comment, I think I have another item to add to the list of things that won't be tackled until level 2:

tabatkins commented 5 years ago

Yeah, that's definitely a v2 feature, since it needs a substantially more complex framework to operate on, expressing that a given value is going to be a length evaluated against some particular other length of an element.

FremyCompany commented 5 years ago

@AmeliaBR Oh yeah that sounds like another fun problem indeed :)

I didn't consider the fact this happened at computation time; in that case it's indeed way more reasonable to perform this per-element in some fashion.

tabatkins commented 5 years ago

I didn't consider the fact this happened at computation time; in that case it's indeed way more reasonable to perform this per-element in some fashion.

Yeah, custom functions are gonna basically just be JS-backed var()s. Getting any fancier with them isn't worth it unless we can get really fancy and do things like globally reparsing style when a custom function is registered and we know what its type will end up being. ^_^

FremyCompany commented 5 years ago

LGTM. Note for future self though: we could eventually get rid of the reparse requirement by doing explicit typing for the most common types, something like this:

div {
    width: length(--f(x));
}

or going for some form of type inference and fallback values:

div {
   border-width: --(f(x) || 0px); 
}

(the latter being what I wanted var to act like initially, so at least I'm kinda consistent with myself)

AmeliaBR commented 5 years ago

For type parsing, whatever future solution is chosen, I hope it has a way to apply to variables as well as to functions.

But for our Level 1 Custom Functions, if type checking isn't handled until the function returns a (TypedOM) value, then we don't need a declared returnType on the class or configuration option object.

Which gets rid of another issue I'd thought of — how to represent functions where the return type is dependent on the input parameter types?


I'm adding this to the next Houdini F2F agenda, to get consensus from the larger group on the idea & the Level 1 limitations to it. I think the main question still to debate is whether to base the API around a class object or around a configuration dictionary.

tabatkins commented 5 years ago

But for our Level 1 Custom Functions, if type checking isn't handled until the function returns a (TypedOM) value, then we don't need a declared returnType on the class or configuration option object.

Right. It might still be useful to have the option of providing a return type, as a check that allows us to throw if you violate it, but probably we'll just avoid the issue entirely for now.

majido commented 5 years ago

We have an open issue in CSSWG for having more complex and expressive timing functions. I feel it may be valuable to also have an extensibility point for these as well. Relevant issues: csswg#1012, and csswg#229.

The current idea here is mostly focused on functions that operate on CSS values and take style map as input but I think most of the fundamentals apply to other idempotent pure functions such as easing. So if we decide to do this, I suggest considering to design the API in such as way that it is more generic and usable in both these domain.

AmeliaBR commented 5 years ago

@majido I was also thinking about that during yesterday's discussion on easing functions, but it is a much more complex case, because the ”value” returned from the CSS function would need to be another function.

We don't currently have a TypedOM way of representing that easing function. Even web animations just represents them as the string value of the CSS notation. But, it should definitely be added in the future. (It would be nice to be able to use the built-in CSS functions as arbitrary interpolator functions in JS!)

Once there is a TypedOM way to represent an easing function, it's probably a reasonable extension to web animations make it possible to subclass and override the interpolation function with a custom JS f(x) interpolator. Once that happens, it would also be a natural extension to this proposal: you define a custom CSS function that returns a custom easing function object.

css-meeting-bot commented 5 years ago

The Houdini Task Force just discussed Custom functions, and agreed to the following:

The full IRC log of that discussion <TabAtkins> Topic: Custom functions
<fantasai> ScribeNick: fantasai
<TabAtkins> https://github.com/w3c/css-houdini-drafts/issues/857
<fantasai> AmeliaBR: This is a proposal for a new module to have a way to have custom functions that you could use in CSS values
<fantasai> AmeliaBR: Math functions, color functions, things that you put in a CSS value and it evaluates to an existing value type
<fantasai> AmeliaBR: I sketched up a rough API in the proposal
<fantasai> AmeliaBR: There's been good discussion over the past few months
<fantasai> AmeliaBR: We separated things out into "stuff that can be easily isolated" vs. "stuff that is complicated"
<fantasai> AmeliaBR: Looking at existing functions we have in CSS
<fantasai> AmeliaBR: We have ones like the new math functions
<fantasai> AmeliaBR: where they take an input value and output value
<fantasai> AmeliaBR: end result is always the same for the parameter you gave
<fantasai> AmeliaBR: sin(90deg) will always be sin(90deg), doesn't depend on the element
<fantasai> AmeliaBR: On the other handl calc(percent + pixels) can't be evaluated until layout
<fantasai> AmeliaBR: So that's simple idempotent functions that can do isolated
<fantasai> AmeliaBR: vs ones that dpeend on DOM tree, inheritance, etc.
<fantasai> AmeliaBR: proposal at the time is to create Level 1 custom functions that only address the isolated idempotent case
<fantasai> AmeliaBR: always gives the same result for same parameters
<fantasai> AmeliaBR: Except that we also allow putting in custom properties
<fantasai> TabAtkins: Also allow *
<heycam> q+
<fantasai> AmeliaBR: There will be arguments to the function, when you use the function you will know what they are
<fantasai> AmeliaBR: One other issue that came up in the discussion
<fantasai> AmeliaBR: what is the structure for declaring your custom function
<fantasai> AmeliaBR: I based it on Paint API
<fantasai> AmeliaBR: valid suggestion that maybe that's overkill and doesn't need a custom class
<fantasai> AmeliaBR: and maybe just need a registration object
<fantasai> AmeliaBR: in this case one of the items of those configuration object would be an actual JS function
<fantasai> AmeliaBR: that's a point of debate
<fantasai> AmeliaBR: Qeustion is do we go with class-based API or simple config option
<fantasai> myles: Runs in a worklet?
<fantasai> AmeliaBR: Yes, actual evaluation of function would be in a worklet
<fantasai> AmeliaBR: either way we need some isolated module so the function is defined in the global scope
<fantasai> AmeliaBR: So doing custom property where you're registering all the data in the main thread doesn't work because the function you're including has to be in a separate module
<fantasai> myles: Style resolution can be multithreaded
<fantasai> myles: Is that compatible with this proposal?
<fantasai> heycam: Our style resolution is multithreaded
<fantasai> myles: potentially one day ours might be too
<fantasai> AmeliaBR: So long as you do dependency calculation for cross-references
<fantasai> AmeliaBR: have to think of those dependencies
<fantasai> AmeliaBR: but function itself is very isolatable
<fantasai> TabAtkins: Ths is basically JS-computed var functions
<fantasai> TabAtkins: ...
<fantasai> emilio: Style resolution includes value computation
<fantasai> iank_: Architecturally this would be impossible
<fantasai> myles: Paint has the same requirements
<fantasai> iank_: Paint has the ability to run on multiple hreads
<TabAtkins> q?
<emilio> q+
<bkardell_> s/hreads/threads
<astearns> ack heycam
<fantasai> heycam: I was going to ask about the worklet stuff as well
<fantasai> heycam: Is it that we have to wait for worklet to synchronously finish its computation
<fantasai> heycam: e.g. on getComputedStyle?
<fantasai> heycam: Is that also what happens with layout?
<fantasai> iank_: If you call layout, we'll synchronously wait for it to finish
<fantasai> TabAtkins: have to run layout on getComputedStyle for width
<astearns> ack fantasai
<TabAtkins> fantasai: If the value can't change on properties of the element, etc, what are the intersting things we can compute here?
<emilio> fantasai: If the value can't change the properties of the elements and what not what's this useful for?
<emilio> ... it cannot even depend on lengths
<emilio> AmeliaBR: custom color mod functions, custom math functions
<emilio> TabAtkins: because we're waiting for computed value time, your ems are fine for example
<emilio> ... we can resolve lengths beforehand
<emilio> fantasai: oh you can resolve lengths
<emilio> AmeliaBR: yeah, not percents since they depend on layout
<fantasai> s/resolve lengths/resolve lengths; I had the impression from Amelia that you couldn't because length resolution varies per element and depends on e.g. font resolution
<emilio> TabAtkins: the restrictions mean that it's not guaranteed for your function to be called for every element that matches it if they have the same input
<emilio> emilio: then how does that work with multi-threading?
<emilio> TabAtkins: you're not required to not call the function multiple times
<heycam> q+
<emilio> AmeliaBR: you always get the same result
<emilio> emilio: but you can't guarantee that
<fantasai> ...
<fantasai> TabAtkins: Because you can invalidate apainter whenever, if you use random value it can flicker
<fantasai> TabAtkins: same result here
<fantasai> AmeliaBR: We can't force the Web author to write idempotent functions
<fantasai> AmeliaBR: But can say browser is allowed to assume the function is idempotent
<fantasai> emilio: If they write a non-idempotent, am I allowed to eat their laundry?
<fantasai> dbaron: invalidation thing is useful for debugging
<fantasai> [emilio's comment got lost in translation]
<fantasai> myles_: ...
<fantasai> AmeliaBR: if you look through issue discussion, brainstormed some ways that you could define just the right amound of randomness
<fantasai> AmeliaBR: do you want random per element -- same element gget same result
<TabAtkins> s/.../so how do you implement random() with this?/
<fantasai> AmeliaBR: or random per element + property -- different result per property?
<TabAtkins> TabAtkins: you can't implement random() at this level
<fantasai> AmeliaBR: That's part of the thing that's too complicated to deal with right now
<fantasai> TabAtkins: Have plans for that, but not worry about that right now
<fantasai> TabAtkins: want to handle things like more math functions without going through WG
<fantasai> myles_: So hashing?
<majidvp> q+
<fantasai> TabAtkins: yeah
<emilio> ack emilio
<fantasai> AmeliaBR: This is all about saying that browsers have a set of things they're looking at for when result will be invalidated, then it will be rerun
<fantasai> AmeliaBR: They can also rerun whenever convenient
<fantasai> AmeliaBR: that's the contract to authors
<Rossen_> ack heycam
<fantasai> heycam: This might already be partially answered by what happens in custom layout
<fantasai> heycam: but what happens when worker throws exception or times out?
<fantasai> TabAtkins: invalid at computed value time
<fantasai> heycam: also the case that with custom layout, you just have to wait until browser decides it wants to recalculate that it will try again?
<fantasai> TabAtkins: whenever your inputs are changed, we invalidate
<fantasai> TabAtkins: or whenever the browser feels like it
<fantasai> myles_: wouldn't want it to be observable
<fantasai> TabAtkins: If you had something that mapped out random < .5 throws exception
<fantasai> TabAtkins: "please run me again I'll do better!"
<fantasai> TabAtkins: browser will run whenever not because you failed
<fantasai> myles_: ...
<fantasai> TabAtkins: randomness is ill-defined
<Rossen_> q?
<emilio> q+
<myles_> s/.../presumably this would have the same thing as custom paint where it would throw away the world every so often?
<fantasai> majidvp: When I sw this proposal immediately what came to mind is our discusison yesterday about easing functions for animations
<fantasai> majidvp: The current use case is focused o nstyle resolution
<fantasai> majidvp: but idea of stateless function with results cachced
<fantasai> majidvp: is similar to what we want fo reasing
<fantasai> majidvp: I'm interested to see if we can make the design such that it can be used for ?
<fantasai> majidvp: example, one could have ?
<fantasai> majidvp: you don't bind all the arguments at styele resolution
<bkardell_> s/?/partial currying
<myles_> q+
<fantasai> majidvp: create function with some paratmeters bound at style resolution
<fantasai> majidvp: and one varible, % progress, that gets fed in
<fantasai> AmeliaBR: way to think about it is, easing fucntion in CSS is a function that returns as its value an f(x) type function
<fantasai> AmeliaBR: custom CSS function could return a function that was used as an easing function
<fantasai> AmeliaBR: to do that, we'd need a TypedOM representation of an easing function
<fantasai> AmeliaBR: Would be useful to create a custom easing function
<fantasai> AmeliaBR: right now they're defined in a string
<fantasai> AmeliaBR: but custom one, already talking about
<fantasai> AmeliaBR: knock out lost of use cases for custom animation worklet
<fantasai> AmeliaBR: wrap it up in a TypedOM object
<fantasai> AmeliaBR: then your function in this proposal would return such an object
<fantasai> AmeliaBR: that represents the easing function
<fantasai> myles_: Why can't you animate the input?
<fantasai> AmeliaBR: I think there's some circulatrity there
<fantasai> TabAtkins: This proposal is not meant to produce new value ypes
<fantasai> TabAtkins: takes input to provide existing value type
<fantasai> TabAtkins: if we need some new type of value, that would be a separate proposal
<fantasai> TabAtkins: once accepted could be something these functions can output
<fantasai> AmeliaBR: Myles was saying ...
<fantasai> AmeliaBR: converting your x progress to the change you want to have
<fantasai> AmeliaBR: indirect way to get what you want
<fantasai> majidvp: basically bypassing web animations machinery
<fantasai> ...
<Rossen_> ack majidvp
<fantasai> TabAtkins: You can use a transition delay to set exactly where in the animation you want to be at that moment
<emilio> ack emilio
<fantasai> ?: Thing deciding your animation position is being decided by another animation
<fantasai> myles_: We have that in animating custom properties
<fantasai> myles_: you animate custom property x from 0 to 1
<fantasai> myles_: you say left: custom-function(..x)
<fantasai> ?: will come up with the ?? proposal
<fantasai> flackr: ...
<AmeliaBR> `.el { width: custom-func(0px, 100px, var(--x)); --x: 0; transition: x 0.3s linear;} .el:hover { --x: 1}`
<fantasai> s/?:/flackr:/
<fantasai> s/?:/flackr:/
<fantasai> myles_: Do we need extra facilities?
<fantasai> myles_: are we going to try to make custom functions return more functions?
<fantasai> majidvp: no
<fantasai> majidvp: I don't think we should do that, was just thinking
<fantasai> majidvp: happy to limit right now
<fantasai> AmeliaBR: Right now custom functions return TypedOM objects
<fantasai> AmeliaBR: if we later have TypedOM objects for easing functions, it'll come along for the ride
<Rossen_> ack myles_
<fantasai> myles_: In the GH issue it shows that the return type is a single type
<fantasai> myles_: but a CSS variable can expand to a sequence of things
<fantasai> TabAtkins: No we don't need a return type if we build on variable machinery
<flackr> s/.../This will come up with our resolution on #869 where we can animate property --x from animation A and read that property in animation B to produce property --y.
<fantasai> TabAtkins: you can output arbitrary, if you want to do that with TypedOM, you represent it as CSSUnparsedValue
<fantasai> AmeliaBR: Not as usefut to return an unparsedValue
<fantasai> AmeliaBR: but I expect TypedOM to return lists ad other complex types
<fantasai> emilio: does this mean these nee to be handled like variable references
<fantasai> TabAtkins: yeah
<fantasai> TabAtkins: you look sad :(
<fantasai> emilio: No, it's OK
<fantasai> s/OK/fine
<Rossen_> q?
<fantasai> emilio: but that means the input can be anything as well
<fantasai> emilio: so same implications that ppl complain about
<fantasai> AmeliaBR: same resolutions as references in paint
<fantasai> TabAtkins: I still should change that to be not like variables, but at least for now will be more like variables. Can add more strongly typed later
<fantasai> TabAtkins: Questions like "what happens if throws an error" get answered that way
<AmeliaBR> s/change that/change env()/
<fantasai> ...
<fantasai> emilio: How do you resolve relative units?
<fantasai> emilio: If input is unparsedvalue
<fantasai> heycam: inputs are typed
<fantasai> emilio: so input syntax is in the definition of the function
<fantasai> TabAtkins: Yes. Unless you say *. But if you say <length> ems will resolve
<TabAtkins> q?
<fantasai> TabAtkins: I think I'm ready to write a spec for this if no one objections
<fantasai> AmeliaBR: I will happily let you the work, but also happy to continue involved
<fantasai> bkardell_: co-editor involved?
<fantasai> AmeliaBR: sure
<fantasai> [naming discussion]
<fantasai> myles_: This has a bunch of JS stuff too
<fantasai> Rossen_: So proposed resolution is Tab and Amelia to co-edit CSS Custom Functions
<fantasai> [more naming discussions]
<bkardell_> ?
<fantasai> bkardell_: I like keeping custom in there because it's not immediately clear to everyone that -- is our universal custom thing
<fantasai> bkardell_: so ties it in that way with custom properties
<fantasai> css-functions
<TabAtkins> css-functions-api
<fantasai> function-api
<dbaron> CSS Value Functions (as fullname)
<bkardell_> sepia-mode
<fremy> +1 to <css-funcs> CSS Value Functions
<bkardell_> +1 css functions api for the short name
<Rossen_> CSS Custom Functions
<fantasai> bkardell_, did you mean css-functions-api
<flackr> +1 css-functions-api
<fantasai> ?
<bkardell_> yes
<bkardell_> I am saving all my dashes fantasai for custom things
<AmeliaBR> short & sweet, css-functions
<fantasai> RESOLVED: Draft this proposal up as css-functions-api
<fantasai> RESOLVED: TabAtkins and AmeliaBR as editors
<bkardell_> also resolved to include Custom in the title?
<fantasai> Rossen_: That's it for Houdini
chriseppstein commented 5 years ago

flip into "invalid at computed-value time" if something is wrong - either a bad return type, or the JS throws, or whatever.

I'd expect the above situation described by @tabatkins to cause a warning to be emitted to console. but sometimes a function needs to be able to "nope out" elegantly and without triggering a warning.

A feature of Sass that is really useful in custom functions is the ability to return null to indicate that no return value is desired, causing the declaration it is part of to be invalid/dropped unless handled by another custom function that expects null as an input. , where-as null would have no warning if it is declared as an expected return value.

chriseppstein commented 5 years ago

This is excellent. I have some questions!

Booleans?

As far as I know, CSS has no boolean type. But custom functions really need them as an intermediate value to allow custom functions to interoperate. Otherwise developers will end up with some sort of cargo-culted convention of using true and false idents but only be able to declare an input as accepting <ident> which is a much bigger input space.

Optional Arguments?

Can trailing arguments be made optional? Can optional arguments be assigned a default value?

Evaluation Order?

The ability to lazily evaluate input arguments, is required to implement something like conditional execution. E.g. if(cond, if-true-val, if-false-val) is a function that only evaluates either the second or the third argument based on the value of the first.

I'm curious whether custom functions that receive arguments that other custom functions would receive the output of that function or would receive a production that indicates it is a custom function invocation and could lazily evaluate that invocation. If the former, could the latter be enabled by a different input argument declaration.

Type Generics?

So that authors can write generic code that is correctly typed, it seems like there should be some concept of a type generic.

Putting it all together

When we put all these ideas together we can write a generic if statement where the conditional checks whether the data being operated on is valid to be used in the if-true branch and handles that situation gracefully if it's not.

class ConditionalEvaluation {
    static get genericTypeArguments() { return ['<T>']; }
    static get inputArguments() { return ['<boolean>', CSS.CustomFunction.lazyArgument('<T>'), CSS.CustomFunction.lazyArgument('<T>?')]; }
    static get returnType() { return '<T> | <null>'; }

    // evaluate the ifTrue condition if the condition is true,
    // evaluate the ifFalse condition if it's provided and if the condition is false
    // return null, causing the declaration to be dropped otherwise.
    conditional([condition, ifTrue, ifFalse], styleMap) {
        if(!!condition) {
            return CSS.CustomFunction.evaluate(ifTrue);
        } else if (ifFalse) {
            return CSS.CustomFunction.evaluate(ifFalse);
        } else {
            return null;
        }
    }
}

registerCustomFunction("--if", ConditionalEvaluation, "conditional");
tabatkins commented 5 years ago

A feature of Sass that is really useful in custom functions is the ability to return null to indicate that no return value is desired

A custom function could return an empty CSSUnparsedValue to indicate that it finished correctly, but just didn't output anything.

But custom functions really need them as an intermediate value to allow custom functions to interoperate.

Can you give some example use-cases?

Can trailing arguments be made optional? Can optional arguments be assigned a default value?

Currently no, you have to predeclare your arity. (Like custom paint() functions, you provide an array of grammar strings, which are interpreted as the grammars for comma-separated arguments.) Or declare your grammar to be "*", which opts out of parsing entirely so you can do something more complicated on your own.

This is something we'll fix over time, definitely, and apply it to all the Houdini function things.

The ability to lazily evaluate input arguments, is required to implement something like conditional execution.

You need lazy eval in JS (and I assume Sass) because running code can have side effects. Side-effects aren't possible in this model tho. You can absolutely implement a conditional, but it would be akin to the JS trinary operator.

So that authors can write generic code that is correctly typed, it seems like there should be some concept of a type generic.

Note that, at least for now, custom functions don't have typed output - they're a JS-controlled var() function, and thus can output anything, and that output is then parsed alongside the rest of the stuff in the property value. The output does have to be a Typed OM value, but that includes CSSUnparsedValue, which is the output type of var().

Is there thus still a need for generics?

chriseppstein commented 5 years ago

A custom function could return an empty CSSUnparsedValue to indicate that it finished correctly, but just didn't output anything.

That would have different semantics than what I described. The function would output nothing and the resulting output might still be a legal value for the declaration, whereas a value that contains null would have the entire declaration quietly declared invalid. Both are important use cases.

Can you give some example use-cases?

You're asking why booleans are useful when writing code for separation of different concerns? Like I could write a function that determines if two colors meet some accessibility guidelines for contrast. it would return true or false. Another function (like the --if function above) can then take that boolean and perform different outputs. The logic for how to handle the color contrast issue might not be the same everywhere so delegating that to the stylesheet author is better than coupling it to the contrast determination function.

Can trailing arguments be made optional?

Currently no, you have to predeclare your arity.

I don't know fully understand what "opts out of parsing entirely so you can do something more complicated on your own." means but it sounds pretty annoying just to be able to omit what should be an optional parameter.

This is something we'll fix over time, definitely, and apply it to all the Houdini function things.

I don't understand why this spec wouldn't drive this change instead of being beholden to it... I suspect it has to do with stuff that seems really clear if you understand how the w3c standards process works but I don't 🤷‍♂

You need lazy eval in JS (and I assume Sass) because running code can have side effects.

No, this is useful purely from the perspective of functions having constraints on what is a legal input argument. The conditional can verify the value is legal to pass to the function and then only invoke the function if it is.

Overall what I'm trying to understand is what is the mechanism for developers who create custom functions to delegate things like error recovery to the stylesheet author. I've assumed there's no built-in try-catch mechanism so that means there needs to be some basic conditionals and branching logic. If CSS isn't going to provide those as a standard library, it will be up to the community to construct a library that can be re-used.

Is there thus still a need for generics?

It would be really great if using functions didn't cause a declaration to stop conforming to normal css declaration resolution thus allowing the cascade for that property to be relied upon instead of falling back to initial. My understanding was that a return type would help enable that. If so, I think generics are going to be very helpful. If sass is a guide, custom functions will get a lot of use, but I don't see an error recovery story here and I think the cascade should be part of that story without good error recovery, I suspect custom functions will be built with a footgun for authors.

AmeliaBR commented 5 years ago

whereas a value that contains null would have the entire declaration quietly declared invalid.

We have something like this in CSS, as the initial value of a custom property, which triggers fallback in the VAR function. I agree that it might be useful to be able to represent that in a function return value.

dead-claudia commented 5 years ago

@chriseppstein

As far as I know, CSS has no boolean type. But custom functions really need them as an intermediate value to allow custom functions to interoperate. Otherwise developers will end up with some sort of cargo-culted convention of using true and false idents but only be able to declare an input as accepting <ident> which is a much bigger input space.

Just to add to that point, it's not like there isn't precedent. Less does this very thing internally IIUC, just making true and false just special identifiers after people started using it that way.

tabatkins commented 5 years ago

That would have different semantics than what I described.

Ah, you didn't describe in enough detail for me to tell which you meant. ^_^ If you just want it to invalidate the property it's found in, throw in the worklet (or I guess return a non-Typed OM value, but throwing will definitely work).

You're asking why booleans are useful when writing code for separation of different concerns?

Yes, CSS doesn't have tests like that, so I was curious. Remember, I have very little experience with Sass actually; also, I responded to this part before I read down to your if() example. ^_^ Makes sense tho.

I don't know fully understand what "opts out of parsing entirely so you can do something more complicated on your own." means but it sounds pretty annoying just to be able to omit what should be an optional parameter.

It means you just get handed the arguments as a string, and have to parse them on your own. Yes, it is annoying. But see the following point...

I don't understand why this spec wouldn't drive this change instead of being beholden to it...

I'm editor on both specs, so neither's beholden to the other. But I'm also trying to make sure the feature is reasonably implementable quickly, so I'm keeping it hovering around an MVP. There are several additional features I want to add to custom functions that'll also wait for v2 most likely!

But further, optional arguments in CSS functions don't currently have a widespread pattern to follow. The contents of a function are, philosophically, no different from the value of a property, and so generally follow property conventions for optionality on a per-component basis. Coming up with a new, and intended-to-be-consistent, way to handle variable-arity functions isn't something I want to commit to hanging on this proposal's v1. There's a lot of variation between languages on how optional args are handled, after all, and I'm not 100% sure what way would be best for CSS immediately. (Like: named args are probably good, for both readability and writeability?)

No, this is useful purely from the perspective of functions having constraints on what is a legal input argument. The conditional can verify the value is legal to pass to the function and then only invoke the function if it is.

Hm, this is why I was asking for specific use-cases. Can you give me a concrete example of this? I'm pretty sure this case you're describing happens as a side-effect of Sass doing its programming within CSS, and it either doesn't occur in custom functions, or can happen within the JS which already has all the semantics you're talking about.

(I think you're talking about something like --if(--isAngle(var(--val)), sin(var(--val)), var(--defaultSine)), so you only try to invoke sin() when the value is a legit type for it, as otherwise evaluating sin(var(--val)) will error when --var is subbed in and automatically make the declaration invalid-at-computed-value-time. If so, then yeah, this doesn't apply, you'd just do that error-checking in JS if you needed to.)

It would be really great if using functions didn't cause a declaration to stop conforming to normal css declaration resolution

Yeah, that would be great, but it's a much longer-term goal, because it's enormously more complicated than just relying on the var()-resolution timing and logic. Happy to chat more on the details of why full "make invalidly-used custom functions invalidate the property they're used in like builtins do" is much harder, but for now suffice to say: omg it's so much harder.


Oh, and responding to an older comment that @isiahmeadows's comment reminded me I'd accidentally skipped over:

using true and false idents but only be able to declare an input as accepting <ident> which is a much bigger input space.

You can declare your grammar to be particular idents, so true | false | FILE_NOT_FOUND works just fine and properly constrains you without you having to do error-checking in the impl of your function.

AmeliaBR commented 5 years ago

The contents of a function are, philosophically, no different from the value of a property,

I agree that this is the best way to think about the parameters. But I don't think that means we should ignore the use case of optional parameters or arbitrary-length parameter lists. Instead, we should be considering the syntax and structure in parallel with extending Typed OM to handle the full complexity of CSS values. And yes, that may mean that Level 1 restricts to the simple case.

tabatkins commented 5 years ago

Yeah, my comment def isn't saying "we're not going to do optional/variadic/named params", just that we haven't done that yet in CSS, so I don't want to block on that while we figure it out. We're definitely going to want something like that, tho.

guest271314 commented 5 years ago

@tabatkins What is the appropriate venue to propose a specific type of Worklet be specified?

tabatkins commented 5 years ago

None, because worklets are tools used by other features, not features in themselves. ^_^

But if you want to talk about potential new Houdini features, open an issue in this repo.

rumkin commented 4 years ago

This functions should be imported directly and explicitly from CSS. In other way we receive another global namespace war with conflicts and collisions.

jimmyfrasche commented 4 years ago

Especially with the recent influx of math functions in css, it would be nice to be able to define simple, recursion-free functions directly in css just to name and parameterize basic calc expressions.