w3c / css-houdini-drafts

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

[css-typed-om] Inputs for the CSSColorValue constructors #1014

Open tabatkins opened 3 years ago

tabatkins commented 3 years ago

I'm writing the spec for color values right now, and the overall structure is straightforward: a CSSColorValue superclass, and subclasses for each type of color function.

I'm struggling a bit with the best design for the constructor arguments and/or the shorthand functions.

Some arguments, like hue angles, are easy - they're just a CSSNumericValue that needs to match <angle>. But most of the arguments to color functions are percentages. Obviously I'll accept a CSSNumericValue that matches <percentage>, but I'd like easier ways to invoke these - writing new CSSRGB(CSS.percent(10), CSS.percent(0), CSS.percent(100)) isn't great.

In particular, I'd like to allow them to accept JS numbers, interpreting the range 0-1 as being equivalent to a percent between 0% and 100%, so you could instead write the preceding function as new CSSRGB(.1, 0, 1).

This would generally be unproblematic and straightforward, except that rgb() accepts <number> as well, in the range 0-255. And elsewhere in TypedOM (such as new CSSScale()), passing a raw JS number is equivalent to passing a CSSUnitValue(x, "number") (see the "rectify a numberish value" algo). So this leaves me with several possibilities, none of which I find great:

  1. Always allow JS numbers in the color functions in the 0-1 range. There's no constructor form that's a direct equivalent to the rgb(0, 128, 255) syntax.

  2. Allow JS numbers in the color functions in the 0-1 range, but also allow CSSUnitValue(x, "number") to be passed to CSSRGB, with the 0-255 range.

  3. Don't allow JS numbers at all, only CSSNumericValues. CSSRGB accepts CSSUnitValue(x, "number") in the 0-255 range.

  4. (Definitely bad, not doing this) Allow JS numbers in the 0-1 range for all the color functions except CSSRGB, allow them in the 0-255 range for CSSRGB (in addition to CSSUnitValue(x, "number")).

  5. Same as (3), but also have shorthand functions like CSS.rgb() (akin to the CSS.px() family for numeric values) that act like (1).

I'm considering going with (1), but (5) would be fine as well. I don't like the others very much. Anyone else have opinions?

tabatkins commented 3 years ago

Oh, forgot to list the big downside of (1) - if you wanted to make a lookalike custom function for rgb() that took 0-255 arguments (in the CSS) in the same way, you'd have to remember to translate any "number" arguments into JS numbers, rather than passing them thru directly.

Well, calling it "big" is perhaps a bit much, but it's the largest downside for (1), at least.

plinss commented 3 years ago

Another option (and a bit more modern JS) could be to use a dictionary initializer as a single argument to the constructor. It also buys you some extensibility and flexibility.

Something like: CSSRGB({r: 0, g: 128, b: 255}) or CSSRGB({rPct: 0, gPct: 50, bPct: 100}) (not personally a fan of the abbreviation but trying to keep it succinct)

svgeesus commented 3 years ago

@LeaVerou the design decisions for color.js seem directly relevant here, so given your expertise in API design I look forward to your comments.

My expertise is more on the functionality, rather than the API surface.

svgeesus commented 3 years ago

In particular, I'd like to allow them to accept JS numbers, interpreting the range 0-1 as being equivalent to a percent between 0% and 100%, so you could instead write the preceding function as new CSSRGB(.1, 0, 1).

That is mostly going to work, except of course for CIE Lightness where 100% maps to 100, not 1.

LeaVerou commented 3 years ago

This is a very common issue with any library working with RGB colors. Typically the decision is to go with one of the two and if one wants to use a different range, they need to convert. In Color.js we went with 0-1 for reasons I will detail below, chroma.js and d3 Color went with 0-255.

I would suggest going with numbers in the 0-1 range, as the 0-255 range is a relic. It originates in using 8 bit for each color component, and seems completely arbitrary otherwise. Note that rgb() is not restricted to 8 bit anymore as it can accept non-integers. Also, all of the new RGB-based colors in Color 4 use 0-1 as well, including color(srgb). The more using > 8 bit per component becomes commonplace, the more arbitrary 0-255 will seem. 0-1 future-proofs this API.

Something like: CSSRGB({r: 0, g: 128, b: 255}) or CSSRGB({rPct: 0, gPct: 50, bPct: 100}) (not personally a fan of the abbreviation but trying to keep it succinct)

This creates unnecessary error conditions: what happens when both r and rPct are specified?

Stepping back for a bit, speccing Typed OM for colors is a fairly substantial undertaking and it would be good if we could get some consensus on the overall architecture and design decisions before discussing the minutiae of constructor arguments. I know there are many people in the group who would have input on said design decisions and would love to partake, myself and @svgeesus included.

plinss commented 3 years ago

Something like: CSSRGB({r: 0, g: 128, b: 255}) or CSSRGB({rPct: 0, gPct: 50, bPct: 100}) (not personally a fan of the abbreviation but trying to keep it succinct)

This creates unnecessary error conditions: what happens when both r and rPct are specified?

That could be trivially resolved. You either raise an exception or decide which one wins.

Using an initialization dictionary also allows you to mix and match, e.g. CSSRGB({r: 0, gPct: 50, b: 42})

That said, I agree that 0-255 is a relic, but it is in common use. I'm in favor of making the API more forward-looking, but want to maintain backward compatibility with legacy code as well to ease the transition. I'm also concerned that if we go with pure 0-1 floating point that we make sure all the (0-255)/255 values can be accurately represented by JS numbers so they at least can round-trip without rounding errors (I think that's the case, but someone should check). e.g. if someone does new CSSRGB(42/255, 42/255, 42/255) and we have an accessor that returns a color channel in the 0-255 range, it needs to return 42, not 42.0000001 or 41.99999999. There's also the question of how these serialize, would the preceding example serialize as rgb(42, 42, 42) or rgb(16.47%, 16.47%, 16.47%)? css-color-4 specifies the former.

Even if we decide that the only values are r, g, and b in the 0-1 range, using an initialization dictionary is more readable, and is much more future-proof. It's simply a more forgiving pattern. Not to mention, it's endorsed by the TAG: https://www.w3.org/TR/2020/NOTE-design-principles-20201110/#prefer-dict-to-bool and https://www.w3.org/TR/2020/NOTE-design-principles-20201110/#naming-optional-parameters

Actually, the more I think about the serialization issue, the more I think the CSSRGB constructor should only take values comparable with the CSS rgb() function. This is an object model meant to represent the rgb() function as it stands after all, not an opportunity to invent something new. So I'm voting in favor of option 4 above (with a dictionary constructor, and the ability to take percentages as well as numbers in the 0-255 range). New ways of defining colors should be used in classes that represent the CSS constructs that allow them, e.g. CSSColor() that takes arguments like color() does.

svgeesus commented 3 years ago

In Color.js we went with 0-1 for reasons I will detail below, chroma.js and d3 Color went with 0-255.

chroma.js and d3 Color are both sRGB-only, so that was a reasonable decision at the time but will bite them once they extend to other RGB spaces.

Some spaces, like rec2020 and rec2100, don't even allow 8 bits per component; the choices are 10, or 12 (recommended). Also, when quantized, there are two encoding ranges (narrow and wide) but the float 0..1 representation is always wide, which removes one source of error. For example, in 10 bit narrow-range encoding, color component values range from 64 to 940; in 12 bit narrow-range encoding, they range from 256 to 3760.

maltenuhn commented 3 years ago

(first contribution, hi! and apologies for any conventions I'm breaking). Design / dev tool author here. I favour (1) for the same reasons as @tabatkins and @LeaVerou Lea (0-255 is legacy, 0-1 is computationally simpler).

Some additional data points / perspectives:

LeaVerou commented 3 years ago

Something like: CSSRGB({r: 0, g: 128, b: 255}) or CSSRGB({rPct: 0, gPct: 50, bPct: 100}) (not personally a fan of the abbreviation but trying to keep it succinct)

This creates unnecessary error conditions: what happens when both r and rPct are specified?

That could be trivially resolved. You either raise an exception or decide which one wins.

Indeed, you can solve all unnecessary error conditions by raising an error, but that doesn't make them good practice. It's far better to avoid creating the error condition in the first place. Deciding which one wins is the kind of arbitrary decision that makes APIs feel unpredictable and hard to learn.

Using an initialization dictionary also allows you to mix and match, e.g. CSSRGB({r: 0, gPct: 50, b: 42})

That said, I agree that 0-255 is a relic, but it is in common use. I'm in favor of making the API more forward-looking, but want to maintain backward compatibility with legacy code as well to ease the transition. I'm also concerned that if we go with pure 0-1 floating point that we make sure all the (0-255)/255 values can be accurately represented by JS numbers so they at least can round-trip without rounding errors (I think that's the case, but someone should check). e.g. if someone does new CSSRGB(42/255, 42/255, 42/255) and we have an accessor that returns a color channel in the 0-255 range, it needs to return 42, not 42.0000001 or 41.99999999.

I just ran for (let i=0; i<256; i++) { let a = i/255; console.log(a * 255); } and it seems to only log integers. Not sure if there's a compiler optimization at play though.

Even if we decide that the only values are r, g, and b in the 0-1 range, using an initialization dictionary is more readable, and is much more future-proof. It's simply a more forgiving pattern. Not to mention, it's endorsed by the TAG: w3.org/TR/2020/NOTE-design-principles-20201110/#prefer-dict-to-bool and w3.org/TR/2020/NOTE-design-principles-20201110/#naming-optional-parameters

Please note that the TAG guidelines you link to (both of which I'm aware of and have even taught) do not apply here. They are both about optional arguments, which these are not. Also, the first one is mainly about booleans, edited recently to generalize to all optional primitives. Indeed, avoiding boolean positional arguments is one of the most basic API design principles (they are called "boolean traps" for good reason). However, red, green, and blue, are numbers with an established meaning, an established order, and all of which are mandatory.

That said, there is less of an established order in other color spaces (e.g. LCH vs HCL), so for consistency, it would make sense to use named parameters here too.

LeaVerou commented 3 years ago

(first contribution, hi! and apologies for any conventions I'm breaking). Design / dev tool author here. I favour (1) for the same reasons as @tabatkins and @LeaVerou Lea (0-255 is legacy, 0-1 is computationally simpler).

Hi @maltenuhn! Welcome! 👋🏼

Some additional data points / perspectives:

  • from user interviews we know that for many developers / designers, rgb 0-255 often results in mental estimations such as "how far along the scale from from eg "no red" to "red" do I want to be. (That's not to say there isn't a lot of muscle memory here)

That's fascinating. Do you have a source for these user interviews? I would love to read more about this study!

  • Additionally: Safari (proprietarily) already exposes the P3 colour space with 0-1 for rgb. I wrote something up here a year ago that has some details. (update: the official webkit docs )

Not proprietary, but perfectly valid CSS Color 4 syntax! Unfortunately, they only implemented part of it, no other spaces besides P3, and no actual <percentage> values.

plinss commented 3 years ago

Indeed, you can solve all unnecessary error conditions by raising an error, but that doesn't make them good practice. It's far better to avoid creating the error condition in the first place.

We can make many more APIs avoid errors by severely restricting what they can do, that doesn't help authors either. There's a balance needed. Allowing authors more flexibility at the risk of allowing them to make mistakes is a common trade off.

Please note that the TAG guidelines you link to (both of which I'm aware of and have even taught) do not apply here.

The principles they represent do apply.

They are both about optional arguments, which these are not.

First, there is an optional argument to the CSS rgb() function that we seem to be forgetting about here, a. Second, there's no reason to make all the arguments mandatory, new CSSRGB({g:128}) could (and should) be equivalent to new CSSRGB({r: 0, g: 128, b: 0, a: 1}).

Also, the first one is mainly about booleans, edited recently to generalize to all optional primitives.

It used to be about booleans, and despite the anchor (which was left intact to not break links) now talks about the general case, which is why I included the link (and the links were for the benefit of other readers of the thread, I'm aware that you know them).


Back to Tab's original question, I'm now fairly well convinced that bare numbers passed into the constructor of a CSSRGB object (regardless of how they're passed) should be interpreted the same as bare numbers passed into CSS rgb(). This is an object model of the CSS rgb() function, it needs to match the semantics of the rgb() function. Full stop. I have no issue with convenience functions or conversions from other types that take 0-1 numbers, but will strongly object to redefining the semantics of the rgb() function in the object model. It should be all about modeling the rgb() function as it exists, not about creating a new general purpose color object (which I'm in favor of, and agree should use 0-1 numbers). Similarly, object models of other CSS color functions should accept the same arguments as their CSS equivalents.

tabatkins commented 3 years ago

Using an initialization dictionary also allows you to mix and match

We have numeric types already for precisely this reason - if you want percentages, you can just pass a CSS.percent(10) as the value.

I don't think an initialization dict, in general, is the best pattern here - most of the arguments are required, and there's already a well-known and established order for them, coming from CSS (and re-expressed right in the name). So I'm pretty confident positional is still the right way to go here, at least for the simple functions. (color() might get a different treatment.)

So I'm voting in favor of option 4 above

Given that taking raw numbers is a pure convenience (rather than only taking the CSSStyleValue objects that it'll expose post-construction), I'm really loathe to do tricky stuff like that and have the numbers be interpreted in different ways depending on the function. It's just begging for people to hold wrong. My current spec text is doing option 2 (raw JS numbers are always 0-1 percentages, but CSSRGB accepts CSS.number(127) as well).

plinss commented 3 years ago

Using an initialization dictionary also allows you to mix and match

We have numeric types already for precisely this reason - if you want percentages, you can just pass a CSS.percent(10) as the value.

Fair enough.

I don't think an initialization dict, in general, is the best pattern here - most of the arguments are required, and there's already a well-known and established order for them, coming from CSS (and re-expressed right in the name). So I'm pretty confident positional is still the right way to go here, at least for the simple functions. (color() might get a different treatment.)

I disagree. I have no issue with also accepting positional arguments. But I want to push back on them all being required. e.g. does let x = new CSSRGB() throw an exception or is it simply new CSSRGB(0, 0, 0). Similarly, what if an author calls new CSSRGB(42) should that throw or just be new CSSRGB(42, 0, 0)?

So I'm voting in favor of option 4 above

Given that taking raw numbers is a pure convenience (rather than only taking the CSSStyleValue objects that it'll expose post-construction), I'm really loathe to do tricky stuff like that and have the numbers be interpreted in different ways depending on the function. It's just begging for people to hold wrong. My current spec text is doing option 2 (raw JS numbers are always 0-1 percentages, but CSSRGB accepts CSS.number(127) as well).

But that's actually inconsistent with your "passing a raw JS number is equivalent to passing a CSSUnitValue(x, "number")" so will cause even more author confusion IMO. I see no reason why new CSSRGB(100, 150, 200) should yield any different result than rgb(100, 150, 200) in CSS. Anything else is asking for trouble. As I said, we're modeling the existing rgb() function here, not inventing something new.

Frankly if you interpret bare numbers as 0-1 you are doing something tricky and interpreting them in a different way for this function, interpreting 0.5 as equivalent to CSS.number(127) rather than CSS.number(0.5) like you do everywhere else.

So let me make a concrete proposal:

dictionary CSSRGBInit {
    CSSNumberish r = 0; 
    CSSNumberish g = 0; 
    CSSNumberish b = 0;
    CSSNumberish a = 1; 
};
interface CSSRGB {
    constructor(optional CSSNumberish r = 0, optional CSSNumberish g = 0, optional CSSNumerish b = 0, optional CSSNumberish a = 1);
    constructor(optional CSSRGBInit init = {});
    ...
};

Both bare numbers and CSS.number() are interpreted the same as if they were specified in rgb().

This allows for author convenience, is succinct, is consistent with CSS and should be consistent with other CSS color object constructors.

(I don't have an issue if you want to make a new type restricting the values to double (or even octet), number, and percent types, rather than CSSNumberish, or just define other values to throw exceptions.)

LeaVerou commented 3 years ago

We can make many more APIs avoid errors by severely restricting what they can do, that doesn't help authors either. There's a balance needed. Allowing authors more flexibility at the risk of allowing them to make mistakes is a common trade off.

True, though I'm not convinced saving the user one division with 255 is a worthy trade off.

Please note that the TAG guidelines you link to (both of which I'm aware of and have even taught) do not apply here.

The principles they represent do apply.

Both Tab and I have explained why they do not:

  1. all arguments (besides a) are required (assigning an arbitrary default can theoretically make any argument optional, but in this case that's externally inconsistent)
  2. There is a very well established order, and arguments never/rarely need to be provided in a different order.

Back to Tab's original question, I'm now fairly well convinced that bare numbers passed into the constructor of a CSSRGB object (regardless of how they're passed) should be interpreted the same as bare numbers passed into CSS rgb(). This is an object model of the CSS rgb() function, it needs to match the semantics of the rgb() function. Full stop. I have no issue with convenience functions or conversions from other types that take 0-1 numbers, but will strongly object to redefining the semantics of the rgb() function in the object model. It should be all about modeling the rgb() function as it exists, not about creating a new general purpose color object (which I'm in favor of, and agree should use 0-1 numbers). Similarly, object models of other CSS color functions should accept the same arguments as their CSS equivalents.

You feel so strongly that the function should mirror the semantics of rgb(), yet you are opposed to it using positional arguments, just like rgb(). 🤔

Please note that color(srgb) uses the 0-1 range when provided with bare numbers. Are we really going to have two different sRGB objects with components in different ranges?

LeaVerou commented 3 years ago

As @plinss points out, another solution would be polymorphism. We could accept an array OR an object literal:

new CSSRGB([1, 0, .6])
new CSSRGB({r: 1, g: 0, b: .6});

Aside: can we please name the constructor something other than CSSRGB, i.e. two different acronyms smushed together? A namespace, and/or the word "Color" somewhere would help.

@tabatkins Re-iterating as I think it was lost in the argument discussion:

Stepping back for a bit, speccing Typed OM for colors is a fairly substantial undertaking and it would be good if we could get some consensus on the overall architecture and design decisions before discussing the minutiae of constructor arguments. I know there are many people in the group who would have input on said design decisions and would love to partake, myself and @svgeesus included.

Where can we find this draft? It's not anywhere in this repo. Searching for CSSRGB in the repo yielded no results. There are a lot of design decisions involved that are more substantial than constructor arguments, and it would be good to discuss and iterate earlier rather than later.

tabatkins commented 3 years ago

e.g. does let x = new CSSRGB() throw an exception or is it simply new CSSRGB(0, 0, 0). Similarly, what if an author calls new CSSRGB(42) should that throw or just be new CSSRGB(42, 0, 0)?

Both throw. Those are required arguments in CSS, and there's no particularly good reason to change that in the JS representation, I believe. There's not really a meaningful sense in which 0 is a "default" value for any of the color channels.

But that's actually inconsistent with your "passing a raw JS number is equivalent to passing a CSSUnitValue(x, "number")" so will cause even more author confusion IMO.

Yes, it is inconsistent, but in a different realm. My current spec text has JS numbers be <percentage> for all colors; JS numbers are <number> elsewhere where numbers are the only sensible thing. I think this is a simple enough boundary that the confusion will be low, enough to be outweighed by the convenience of being able to write, say, CSS.hsl(CSS.degrees(60), 1, .5) over CSS.hsl(CSS.degrees(60), CSS.percent(100), CSS.percent(50)).

Where can we find this draft? It's not anywhere in this repo. Searching for CSSRGB in the repo yielded no results. There are a lot of design decisions involved that are more substantial than constructor arguments, and it would be good to discuss and iterate earlier rather than later.

It's still an in-progress edit right now, I'll have first draft up soon.

plinss commented 3 years ago

all arguments (besides a) are required (assigning an arbitrary default can theoretically make any argument optional, but in this case that's externally inconsistent)

In JS all arguments are effectively optional. One way or another you have to define what happens when an author uses new CSSRGB(42), because nothing's stopping them from doing it. If you don't specify the behavior, we'll have interop bugs. In your argument above, throwing an exception is not necessarily best practice and it's better to avoid the error condition in the first place, I agree. Hence reasonable defaults, e.g. 0 for color channels, 1 for alpha.

You feel so strongly that the function should mirror the semantics of rgb(), yet you are opposed to it using positional arguments, just like rgb(). 🤔

I'm explicitly not opposed to positional arguments, see my proposal above. I just want the option to use an initialization dictionary. What I do feel strongly about is not redefining the behavior of a bare number for this one constructor.

Keeping it consistent with a theoretical color object model isn't as valuable as keeping it consistent with CSS syntax that's been in use for over 20 years.

Another advantage of an initialization dictionary is that it does let the author opt-in to a different behavior for bare numbers, by specifying them in a different dictionary slot (If that's a case we really need to support, after all, is it worth adding it to save them writing *255?).

Please note that color(srgb) uses the 0-1 range when provided with bare numbers. Are we really going to have two different sRGB objects with components in different ranges?

Yes. CSS already has them. This is the CSSOM, not a color OM, the need here is to provide an object model for CSS constructs, in this case rgb(). It's a CSS value, not a general-purpose color object. The object's constructor and behavior should match the usage in CSS. The argument to the constructor for CSSColor should match CSS color(), etc.

I'd love to see a generic color object model. And if we create one I'm happy for that to use floats and do away with the 0-255 legacy there. But that's not the scope of this issue or module and we shouldn't let this scope-creep into one.

We could accept an array OR an object literal

I don't see any value in accepting an array. I'm not seriously opposed, but unless there's precedent or alignment with other CSS color constructors I'd rather not invent another new thing here. In fact, we may want to reserve supporting an array in case the rgb() function ever gets list-like capabilities.

plinss commented 3 years ago

Both throw. Those are required arguments in CSS, and there's no particularly good reason to change that in the JS representation, I believe. There's not really a meaningful sense in which 0 is a "default" value for any of the color channels.

I don't see any value in making those throw unless the objects are immutable. Why can't I construct an object and then populate the attributes later? Forcing authors to have all the values before constructing an object is an anti-pattern. What if I want to pass an object into a function to get the values for the channels?

svgeesus commented 3 years ago

I'm now fairly well convinced that bare numbers passed into the constructor of a CSSRGB object (regardless of how they're passed) should be interpreted the same as bare numbers passed into CSS rgb(). This is an object model of the CSS rgb() function, it needs to match the semantics of the rgb() function.

I'd love to see a generic color object model. And if we create one I'm happy for that to use floats and do away with the 0-255 legacy there. But that's not the scope of this issue or module and we shouldn't let this scope-creep into one.

Wait, is that all it is doing? An object model of the sRGB-only rgb() function? I don't understand how that is useful.

Does the #rrggbb form have a different object? Does lab() have a different object? From the June 2020 Houdini minutes it seemed that there was consensus to have a color object, regardless of how it had originally been specified - so that there can be color conversion, color manipulation, and extras like WCAG contrast ratio, gamut mapping, and so forth.

plinss commented 3 years ago

Wait, is that all it is doing? An object model of the sRGB-only rgb() function? I don't understand how that is useful.

Having not seen the actual spec prose Tab is working on, I can't say. But I can say what I believe it should be doing.

Yes, there should be a CSSRGB (modulo bikeshedding) class whose primary role is to model a CSS rgb() function. And that's all. The usefulness is in constructing, parsing, manipulating, and serializing CSS without using the ancient string-based APIs. e.g. I should be able to load a style sheet containing color: rgb(1,2,3), access the value of the color property as an object, and set g to 42 via code like value.g = 42, and not have to parse that string and compose a new one. That's the purpose of the CSS Typed OM module, not to be a generic color handling module.

Does the #rrggbb form have a different object? Does lab() have a different object?

Yes, that should be a CSSHexColor (or whatever we call it). We should also have a CSSLAB (for lab()), CSSColor (for color()) etc., that each model the respective CSS constructs, and will serialize as the same construct they were constructed as

From the June 2020 Houdini minutes it seemed that there was consensus to have a color object, regardless of how it had originally been specified - so that there can be color conversion, color manipulation, and extras like WCAG contrast ratio, gamut mapping, and so forth.

Right, and that should be a Color class (and whatever appropriate subclasses, I trust you and Lea to have the best input on the shape of that API). But it should not be a CSS color class as it's representing a color, not a CSS construct. And it should not be in this module (and arguably should not be a CSS module either as it should also be used in Canvas APIs, etc).

We should be able to convert from the CSS color constructs to a Color object and vice-versa. The Color class should have all the conversion, manipulation, and extra functionality, not the CSS color objects (though I'm happy to add convenience methods to the CSS color classes once we've defined the Color class(es)).

I fear the two concepts have become conflated here.

mattgperry commented 3 years ago

Hi all. Author of Popmotion and Framer Motion here, I've spent a fair amount of time working with value ranges and I'd love to ditch a bunch of code in favour of a native color API. Here's some thoughts after reading this discussion.

First of all I recognise there's a huge value in maintaining familiar APIs like the RGB 0-255 range. But for me, that doesn't override the potential wins from option 1.

All finite ranges are IMO best described as ranges 0-1. It's a range that has intrinsically semantic meaning and is easy to visualise mentally. Standardising it between percentage/bounded time/8 bit color makes interpolating between these ranges straightforward.

On the keyed object idea, it'd of course be possible to make new RGB({ r: 0 ... }) to differentiate between 0-1 and 0-255. But compared to new RGB(0, 0.5, 1) this would be unwieldy and only promote wrapper libraries that simplify the syntax. I'd prefer to reduce the keystrokes and the payload. new RGB(0.5) should forward-fill with 0, 0, 1, the common-sense defaults. The short-hand for blue: 1 is new RGB(0,0,1), shorter than new RGB({ b: 1 }).

I don't think an initialization dict, in general, is the best pattern here - most of the arguments are required, and there's already a well-known and established order for them

I totally agree with this, it's even in the name!

Right, and that should be a Color class

I agree with this too. But Houdini has been developing for a while and I can easily imagine another 5(ish?) years waiting for a proper Color API. This should and can be it. It can be renamed, or not, I don't overly care. I just want a sane, native color API. The semantics in this sense really don't have me concerned.

Ultimately I think if the existing standard was rgb(0, 0.5, 1) and we were debating 0-255 this issue wouldn't get past the opening post. As @LeaVerou mentions, it's rooted in legacy and even accepts decimals now. If I want a half-value it's far more intuitive to think "0.5" than "127.5". This is a choice that can be chose for everyone consuming this new API in the coming decades.

plinss commented 3 years ago

Also, to be clear, I'm perfectly happy for the generic Color classes to take simple bare numbers in their constructors. e.g. should there be a RGB Color subclass, new RGB(0, .5, 1) would yield the same color as a new CSSRGB(0, 128, 255) (modulo the rounding issue on 128/255) when converted to a Color object.

The Color class(es) should have a clean, uniform, API that makes the most sense for handling colors. The CSSOM classes however, should take the same inputs and match the semantics of their CSS counterparts.

plinss commented 3 years ago

I'd prefer to reduce the keystrokes and the payload. new RGB(0.5) should forward-fill with 0, 0, 1, the common-sense defaults. The short-hand for blue: 1 is new RGB(0,0,1), shorter than new RGB({ b: 1 })

That's not now JS works, if the constructor is expecting RGB(r, g, b) and you call it with RGB(1), you're passing (1, undefined, undefined), shifting arguments around is a completely unexpected behavior.

But Houdini has been developing for a while and I can easily imagine another 5(ish?) years waiting for a proper Color API

And Houdini will continue developing for many years to come. It's all modules that can be implemented independently. There's no reason to expect defining a proper Color API, independent of the CSSOM would take another 5 years, or that it would, in fact take any longer than trying to define a CSSOM that conflates CSS constructs with generic colors.

In fact, as this thread demonstrates, mixing the two concepts is (and will continue to be) a source of contention that will dramatically slow the process. Having a CSSOM that's simply CSS, and a Color API that's just color, splits the concerns and lets each develop at their own pace, focusing on their own needs. Believe me, we'll get a Color API much faster if it's not carrying around 25 years of CSS baggage. We'll also get a reasonable CSSOM faster if we don't try to solve all of color in it.

mattgperry commented 3 years ago

That's not now JS works, if the constructor is expecting RGB(r, g, b) and you call it with RGB(1), you're passing (1, undefined, undefined), shifting arguments around is a completely unexpected behavior.

Fairly familiar with how JS works. If you pass it RGB(1) you can reasonably forward fill GBA with 0,0,1. There’s no shifting. I then went on to say RGB({ b: 1 }) isn’t shorter than just writing RGB(0,0,1) so there’s no real benefit to this either in terms of shorthand.

plinss commented 3 years ago

If you pass it RGB(1) you can reasonably forward fill GBA with 0,0,1

(sigh) I'm not saying it can't be done, I'm saying it shouldn't. It violates the principle of least surprise, e.g. any JS programmer unfamiliar with the API will presume you're passing (1, undefined, undefined), not (0, 0, 1). It also violates the TAG design principles regarding optional arguments. I can guarantee you an API like that would never pass TAG review.

Furthermore, the original intent of offering an initialization dict was to pass different types of values, e.g. numbers vs percentages. The advantages in using one for optional arguments is for clarity more than brevity. It also allows unspecified argument defaults to be controlled by the receiving function rather than the caller.

mattgperry commented 3 years ago

(sigh) I'm not saying it can't be done, I'm saying it shouldn't. It violates the principle of least surprise, e.g. any JS programmer unfamiliar with the API will presume you're passing (1, undefined, undefined), not (0, 0, 1

We’ve crossed wires here. I may have incorrectly used “forward” fill? But said “GBA” get filled. Red is assigned 1, as provided. The rest get assigned their default values 0,0 and 1, as is standard JS. And that this syntax is so short we don’t need a shorthand for setting, for instance, blue, with named objects, as was earlier proposed.

plinss commented 3 years ago

We’ve crossed wires here.

Agreed, I read "forward fill" as filling in missing arguments from the front, I also misread your example as RGB(1) creating blue, sorry for the confusion. FWIW, I'd describe that as "back filling" or more commonly, simply using default values.

The behavior you describe is precisely the same as the proposal I offered in https://github.com/w3c/css-houdini-drafts/issues/1014#issuecomment-744744849

LeaVerou commented 3 years ago

From the June 2020 Houdini minutes it seemed that there was consensus to have a color object, regardless of how it had originally been specified - so that there can be color conversion, color manipulation, and extras like WCAG contrast ratio, gamut mapping, and so forth.

Right, and that should be a Color class (and whatever appropriate subclasses, I trust you and Lea to have the best input on the shape of that API). But it should not be a CSS color class as it's representing a color, not a CSS construct. And it should not be in this module (and arguably should not be a CSS module either as it should also be used in Canvas APIs, etc).

We should be able to convert from the CSS color constructs to a Color object and vice-versa. The Color class should have all the conversion, manipulation, and extra functionality, not the CSS color objects (though I'm happy to add convenience methods to the CSS color classes once we've defined the Color class(es)).

I fear the two concepts have become conflated here.

Note that what @svgeesus and I were concerned about, that people will start using CSSColorValue in lieu of a Color class is already happening: https://github.com/fserb/canvas2D/blob/master/spec/color-input.md

tabatkins commented 3 years ago

Which I'm still fine with, fwiw. I never understood why we'd want to produce a secondary version of the color classes; either we exclusively put a bunch of useful color manipulation methods over there (leaving the TypedOM variants extremely low-power and requiring conversion back and forth by authors to do useful things) or we put them on both (making it unclear why we have two class hierarchies in the first place).

plinss commented 3 years ago

Let me try to be clear about my feelings on the matter, I want two things: 1) a color class (or class hierarchy) that is used to represent color values across the entire platform, and has color space conversion, color manipulation functions, etc; 2) A set of CSS OM classes that represent the various CSS color constructs well allowing creation and manipulation of CSS style sheets without resorting to string manipulation and parsing.

If it turns out that we can serve both of those needs with a single class (or class hierarchy), then fine. But they have different goals and different needs as to API surface. Yes there's some overlap, but there's also a lot of orthogonal functionality. My preference is that we design the two independently, and then see how they overlap and if it makes sense to combine them. What I don't want is to see a bunch of compromises in the API surface in order to serve two orthogonal purposes, making one (or both) less suited to task. Especially in the early design stages.

This thread started because of just such an impedance mismatch between the two use cases.

A similar example is DOMMatrix, we have CSSOM classes to represent the various CSS constructs, and a Matrix class to handle the actual math. It didn't make sense to conflate them, it likely doesn't make sense to conflate CSS color constructs and a generic color class either.

tabatkins commented 3 years ago

The OP's "impedance mismatch" wasn't a big deal, it was just a design question. (And I solved it, by just saying that the Typed OM always represents values as 0-1 or %s; CSS's 0-255 range on RGB is just a weird CSS text-syntax thing.)

Notably, this ends up being the same solution that a native JS Color class would do, like in Lea/Chris's library.

My experiences so far don't suggest that we'll have much, if any, use-case mismatch. When we do, we can design around it, perhaps with an independent class doing something more directly when the CSS version is too weird/restricted. But for colors? The CSS version appears to be just fine.

(The DOMMatrix stuff is an example where the CSS version is indeed too weird/restricted; CSS only uses 3x2 or 4x4 matrixes, but the general use-case wants arbitrary sizes and dimensions. It makes sense that handling the arbitrary case in a CSS-specialized class is probably overkill. I'll note, tho, that we haven't produced an arbitrary Matrix class for the web, even tho DOMMatrix has been around for years. Hooking things together even when there is a slight mismatch at least ensures there's progress on both cases, rather than us only solving the one and leaving the other to languish.)

plinss commented 3 years ago

(And I solved it, by just saying that the Typed OM always represents values as 0-1 or %s; CSS's 0-255 range on RGB is just a weird CSS text-syntax thing.)

And that's not a good solution. As I said before, the goal of the CSSTypedOM is to represent CSS constructs accurately and faithfully. A class to represent a CSS rgb() function should take the same arguments in its constructor as a rgb() function does. Doing anything else is not solving the goals of the CSSTypedOM. These classes should be focused on modeling CSS syntax first and foremost.

The design question here isn't "which range of numbers do we take in this one constructor", it's "what's the purpose of this API". It's the second question that should be informing the answer to the first.

Trying to combine CSS OM with a generic Color class at this point (i.e. without having a well-designed API surface of a generic color class, or even a fully-formed CSSTypedOM API), is a premature optimization that leads to bad design decisions. Those are two very different beasts. Combining them without the big-picture of the best API surface for each is a mistake.

Adding color manipulation functions to the CSSTypedOM color classes may very well be a nice convenience for authors, it also might make an API surface that's not performing either job well because it's trying to serve two different functions. As I said, I'm not opposed to combining the functionality in principle, but I am very much opposed to starting with the premise that we must do so. And any compromises we make now trying to serve that end are a mistake.

mattgperry commented 3 years ago

@plinss this sounds like purposefully downgrading a new API because of the poor or outdated decisions of the past.

plinss commented 3 years ago

@mattgperry No, it's purposefully designing a new API to be properly suited to its task.

LeaVerou commented 3 years ago

I'm strongly with @plinss here. When @svgeesus and I expressed concerns about this CSSColorValue class, we were repeatedly told not to worry about it, because it's just to represent CSS constructs, and not as a general Color class for the Web platform. We were told there is a lot of weirdness in this class to account for legacy CSS things, and that should not be part of a general Color class for the Web platform. If CSSColorValue is intended as a general Color class for the Web Platform, then the use cases of the Web Platform need to be considered while designing it, we can't just design it for the CSSOM and then use it in the Web Platform as an oh-and-by-the-way. Also, if this is intended to be used more broadly, it needs more broad input as well.

svgeesus commented 3 years ago

Agreeing with @plinss and @LeaVerou here.

@litherum wrote

Presumably, CSS Typed OM will want a color type. We should make sure that we don't have two competing color types. Instead, the same type should work for both.

@fserb wrote

We had a conversation with the CSS WG and CSS Typed OM is very close to have a Color type. Given that, we are working on modifying this to simply support the Typed OM color type described here: https://drafts.css-houdini.org/css-typed-om-1/#colorvalue-objects

So people are assuming that the color type in CSS Type OM is the color type for the Web platform. And honestly, Canvas people should not be penalized for trying hard to align with CSS Color 4 here and re-using CSS stuff in Canvas.

Yes, Typed OM needs to reflect whatever was used to construct a given color. But at the end of the day, it is being used to set and query colors; and colors have meaning, and the same actual color can be set in multiple syntactic ways and still be the same color. So there is a need for a color type.

Colors can also be compared with other colors, and whatever syntax was used to create that pair of colors is irrelevant to that comparison operation.

LeaVerou commented 3 years ago

Yes, Typed OM needs to reflect whatever was used to construct a given color. But at the end of the day, it is being used to set and query colors; and colors have meaning, and the same actual color can be set in multiple syntactic ways and still be the same color. So there is a need for a color type.

Colors can also be compared with other colors, and whatever syntax was used to create that pair of colors is irrelevant to that comparison operation.

IMO that is one of the reasons why we shouldn't use CSSColorValue as a general Color object. CSSColorValue needs to reflect syntax, whereas a Color object only needs to reflect color space & representation thereof. E.g. the distinction between color(srgb 0 1 0), #0f0, and rgb(0% 100% 0%) is meaningless for a Color object, but needs to be preserved for the CSS OM.

Nobody is complaining that we have CSSNumericValue in Typed OM and numbers in JS, or CSSImageValue in Typed OM and Image in JS, or CSSResourceValue in Typed OM and URL in JS. I don't see how this is any different.

annevk commented 3 years ago

Someone here might want to butt in on https://github.com/whatwg/html/pull/6609 to say these types are not stable (if I understand this discussion correctly).

LeaVerou commented 3 years ago

Someone here might want to butt in on whatwg/html#6609 to say these types are not stable (if I understand this discussion correctly).

We just did in the response to their TAG review request (which is how I came across this in the first place), here: https://github.com/w3ctag/design-reviews/issues/627#issuecomment-838726027

annevk commented 3 years ago

That's how I arrived here, but TAG review is somewhat orthogonal to things landing in a WHATWG standard.

tabatkins commented 3 years ago

And that's not a good solution. As I said before, the goal of the CSSTypedOM is to represent CSS constructs accurately and faithfully. A class to represent a CSS rgb() function should take the same arguments in its constructor as a rgb() function does. Doing anything else is not solving the goals of the CSSTypedOM. These classes should be focused on modeling CSS syntax first and foremost.

The design question here isn't "which range of numbers do we take in this one constructor", it's "what's the purpose of this API". It's the second question that should be informing the answer to the first.

Trying to combine CSS OM with a generic Color class at this point (i.e. without having a well-designed API surface of a generic color class, or even a fully-formed CSSTypedOM API), is a premature optimization that leads to bad design decisions. Those are two very different beasts. Combining them without the big-picture of the best API surface for each is a mistake.

I strongly disagree on several of these points.

The Typed OM needs to reflect CSS, yes. That does not mean it needs to be perfect 1:1 direct translation of CSS syntax into JS constructs; I'm explicitly not doing that in several spots, and expect to continue not doing that for the future. When possible, I'm simplifying and following canonical JS practices more closely, because sticking closely to CSS would be a net loss for the API; direct translation helps in a theoretical purity sense only, but harms authors in practice.

Directly: my goal with Typed OM is not and never has been to be a 100% reflection of what authors typed into their CSS. It needs to be able to represent equivalent values, but where JS and CSS idioms clash, or where I can make some minor useful changes that faithfully reflect the underlying value but fold away some aspects of the surface syntax, I will do so. This should be, as much as we can make it, a convenient API to use for authors; "faithful" is not a goal beyond the minimum that needs to be maintained to make it worthwhile at all.

This precise case is a great example. Currently, color() accepts %s and 0-1 numbers, mapping them together; a number of functions accept %s only; and rgb() accepts %s and 0-255 numbers. I've got a few possible ways to represent this:

  1. All %s can be given as a CSS.percent(), or as a JS number. This directly matches color(); is a slight expansion (in a reasonable way) for the other color functions; and is a diversion from rgb(), where CSS numbers have a different range and interpretation.

  2. All %s can be given as a CSS.percent(), or as a JS number in everything but rgb(), where JS numbers are instead accepts in the 0-255 range. This maps more closely to rgb()'s CSS syntax, but means that you can't use JS numbers as a generic way to represent % values across the color functions; it works for everything but rgb(), which is a different source of confusion.

  3. JS numbers are only ever interpreted as CSS.number(). color() and rgb() accept them, with their correspondingly different ranges (0-1 and 0-255), but the other color functions can only be interacted with using CSS.percent().

  4. JS numbers aren't allowed at all; you must pass in a CSSNumericValue to interact with any of the color APIs.

I think #4 is immediately a no-go. The TypedOM already allows raw JS numbers and strings in several places, and translates them into the appropriate TypedOM objects (CSSKeywordValue and CSSUnitValue). Disallowing it here would be inconsistent and author-hostile.

I think #3 has theoretical consistency going for it, and nothing else. Forcing people to write CSS.percent() to set a Lab function's saturation, vs just saying color.s = .5; like you'd do in any purpose-built color library, seems excessive. Again, this hurts authors and benefits only spec authors, which is a no-go.

2 kinda works, but it means authors have to remember that they can use JS numbers for %s in all the color functions except rgb(), where JS numbers mean something completely different. On the other hand, it means that it's easier to mentally translate back and forth between CSS and Typed OM - you see an rgb(0 127 255), you'll get CSSUnitValues that have 0, 127, and 255 as their value. This is at best a toss-up, imo, and I think the "useful, not faithful" principle leans strongly against it.

1 is what I went with, because tho it means CSSRGB is a less direct translation of the CSS syntax, it gives a more consistent JS API to authors overall - in all the color functions, if you use a plain JS number, it's interpreted as a %. That's easy to learn, and easy to use.

Adding color manipulation functions to the CSSTypedOM color classes may very well be a nice convenience for authors, it also might make an API surface that's not performing either job well because it's trying to serve two different functions. As I said, I'm not opposed to combining the functionality in principle, but I am very much opposed to starting with the premise that we must do so. And any compromises we make now trying to serve that end are a mistake.

I'm not starting from the premise that we must do so. Many parts of Typed OM will be specialized for CSS and not suitable for generalization; earlier in this thread matrix APIs were mentioned, which is definitely one such case.

I am, instead, affirmatively stating that I think the Typed OM color APIs are reasonably suitable as a general-purpose color library for the web, and while we'd make slightly different decisions if we were designing a JS color API from scratch, the differences between our real and ideal library are small enough that it won't be worthwhile trying to make a second color library.

If we tried to make second dedicated color library for the web, either it'll duplicate the large majority of functionality that TypedOM's color types will have, or to avoid that duplication we'll have to hobble the TypedOM classes to be very strictly about solely producing CSS values, such that authors will have to do a two-step converting between TypedOM and the JSColor objects.

If someone wants to prove me wrong, and make a Color library proposal that has tons of bells and whistles that we won't want to put into TypedOM, and can get implementor interest behind such a thing, feel free! But I won't be spending my time on it, and in the meantime various APIs on the web want to be able to use something other than strings of CSS text to work with colors, and TypedOM will be here for that.

the distinction between color(srgb 0 1 0), #0f0, and rgb(0% 100% 0%) is meaningless for a Color object, but needs to be preserved for the CSS OM.

Not quite. I think it's useful to preserve the distinction between the first and the other two (for some usability and forward-compat reasons), but I don't think the latter two need to be distinguished by the Typed OM. They're definitely not currently preserved; both get represented as a CSSRGB in the current spec.

Nobody is complaining that we have CSSNumericValue in Typed OM and numbers in JS, or CSSImageValue in Typed OM and Image in JS, or CSSResourceValue in Typed OM and URL in JS. I don't see how this is any different.

That first pair is actually conflated in the TypedOM; in several places a plain JS number is allowed and auto-converted into a TypedOM value. The rest are significantly different from each other, in use-case and desirable API. We definitely wouldn't want to try and combine them.

I don't think colors are the same, as I argue earlier in this message. This is something that varies case-by-case, and I think trying to draw a general principle out will result in a worse API for authors for minor theoretical purity benefits.

LeaVerou commented 3 years ago

while we'd make slightly different decisions if we were designing a JS color API from scratch, the differences between our real and ideal library are small enough that it won't be worthwhile trying to make a second color library.

It sounds like you have specific differences in mind, since you are asserting that they are small. Could you please share them with us?

If someone wants to prove me wrong, and make a Color library proposal that has tons of bells and whistles

Nobody is asking for unimplementable, complicated "tons of bells and whistles", but for the existing author use cases to be considered. There is a lot of prior art on this.

tabatkins commented 3 years ago

It sounds like you have specific differences in mind, since you are asserting that they are small. Could you please share them with us?

I think the biggest obvious fix would be to use raw JS numbers for channel values, rather than CSSNumericValue objects. TypedOM numbers aren't too annoying to use, so long as they're CSSUnitValues (just an extra indirection thru the .value property, and I made sure you can even directly set a raw JS number so you only need to do the indirection when reading), but they could still be better.

(Tho, having a real angle type, instead of having to choose between setting a raw JS number representing either radians or degrees, is nice.)

Possibly a change would be to have all the color spaces live in one class, rather than separate classes per function. That would probably require doing some magic tear-offs, tho, like your Color.js does, which we try to avoid when possible in JS APIs, as it makes lifetime management more complicated. Unsure if this would be a net positive or not.

Turning it around tho, it seems like you're asserting the differences would be large? If so, what would they be? It's very possible that we could add most or even all of what you want to see!

Nobody is asking for unimplementable, complicated "tons of bells and whistles", but for the existing author use cases to be considered. There is a lot of prior art on this.

I didn't say "unimplementable" or "complicated". ^_^ I'm referring to the implicit assertion y'all are making that there are a lot of things we'd want in a Color library that, for whatever reason, aren't appropriate for the Typed OM classes. If you or anyone else would like to demonstrate those things, and we can see that we, indeed, would not want to include them in the Typed OM classes, feel free! Currently I don't believe there are, but I'm not the most knowledgeable about this space; I'm not, however, willing to accept a priori that the Typed OM classes must be more limited by their very nature, making them unsuitable for this sort of thing.

I'm not being sarcastic in any way - please prove me wrong. I'm just operating off of the information I currently have and my own intuitions, same as any of us are.

annevk commented 3 years ago

FWIW, I think that CSS types should not be about input syntax. Some input syntax aspects might have to be preserved in certain cases because of legacy roundtripping behavior, but ideally that is minimized as it there is not a lot of benefit to it. I would actually have expected color(srgb 0 1 0), #0f0, and rgb(0% 100% 0%) to all end up as the same type with a bit indicating how it would need to be serialized back (if any API has to preserve that today, not sure).

I also agree that we should not have separate (non-CSS) color and CSS color types. We don't have that today with string-based colors either and thus far that has been great and allowed for reuse of a great deal of logic between canvas, CSS, and SVG.

svgeesus commented 3 years ago

@annevk wrote

Some input syntax aspects might have to be preserved in certain cases because of legacy roundtripping behavior, but ideally that is minimized as it there is not a lot of benefit to it. I would actually have expected color(srgb 0 1 0), #0f0, and rgb(0% 100% 0%) to all end up as the same type with a bit indicating how it would need to be serialized back (if any API has to preserve that today, not sure).

I agree that, apart from serialization (which use of Typed OM should minimize) preserving the minutiae of input syntax, particularly the mess of sRGB legacy syntaxes, has not much value and should not be the primary goal of a CSS Color model, far less a Web-wide color model if that is what we are designing (still unclear on that).

Color serialization was moved from CSSOM, where it was sRGB only and 8bit only, to CSS Color 4 by CSS WG resolution.

Currently all the legacy sRGB syntaxes - rgb(), rgba(), hsl() hwb() and named (X11 and system) colors serialize the same way - and not in a very nice way either, but one chosen for maximum legacy compat. So the bit depth is no longer fixed at 8 per component, but uses a real number which means 8bit values still serialize as integers: rgb(146.06, 107.46, 131.2 / 0.8). The minimum conformant bit depth is still 8 though.

However, the color() function serializes separately, partly for consistency within the color function (values are in the range [0-1] and also as a non-legacy sRGB opt-in to better behavior (interpolate in Lab instead of gamma-encoded sRGB, minimum bit depth 10). Plus of course it can use other color spaces, like display-p3.

I also agree that we should not have separate (non-CSS) color and CSS color types. We don't have that today with string-based colors either and thus far that has been great and allowed for reuse of a great deal of logic between canvas, CSS, and SVG.

Re-use of concepts has certainly been great, for example the Canvas HDR proposal re-uses a lot from CSS Color 4. As you say, some syntax details are already not preserved and are not the most important aspect anyway.

But one reason the string-based syntax stuff worked okay was that it didn't have much to do. The only colorspace was sRGB so there was no need for colorspace conversion when mixing, interpolating, compositing or animating. As we move into a Wide Clor Gamut and High Dynamic Range world, those conversions are necessary and commonplace.

LeaVerou commented 3 years ago

@tabatkins

So all the changes you can think of are about syntax, not functionality?! No wonder you think this would be sufficient (though having to deal with objects instead of numbers would be really painful, think of how bad the SVG DOM is — and that wasn't even on my radar!)

Off the top of my head (and based on my work and research on this with @svgeesus for the last year or so), I think that to cater to enough author use cases, a color object for the Web would need to at least:

I honestly don't think it would be a good idea to hang all of that off of a Typed OM object, nor are they in scope for what the Typed OM does. And especially now that you pointed out this issue with dealing with numbers, I'm even more convinced this is a bad idea that will lead to a lot more developer pain than just having to do new Color(csscolorvalue) or csscolorvalue.toColor() or whatever.

What do you mean by "magic tear-offs"?

@annevk

The difference needs to be preserved for Typed OM, since it's preserved for serialization. Not to mention that color(srgb) and rgb() are treated differently in CSS. The former is 10 bit whereas the latter is 8 bit, and the former uses the new improved interpolation, whereas the latter is legacy and uses legacy interpolation. The reason you think they should both be represented by the same object (which one though? CSSRGB or CSSColor?) is exactly because you are trying to make this into something it is not, just to avoid having two color objects (even though their purposes and use cases are entirely different). But by trying to combine them, all that will be achieved is one object that does neither job well…

tabatkins commented 3 years ago

So all the changes you can think of are about syntax, not functionality?!

No, I was just focusing on things that I might change from our current model. Things that would be useful to add could definitely be added! In particular, most of the bullet points you listed are absolutely reasonable to go in Typed OM; a few are slightly more questionable for this API but definitely within the realm of possibility.

Just because the list is pretty long and getting off-topic for this issue, I've filed #1040 to track them.

What do you mean by "magic tear-offs"?

Whenever an object has a sub-object that is intimately tied to it, so that manipulating the sub-object fiddles with the state of the super-object, it's a "tear-off". Compare to a sub-object that is just its own encapsulated state, which the super-object can reference at some well-defined time to do some operation.

A tear-off example is y'all's Color.js object, with its .rgb and .lab sub-objects, where the sub-objects are just "windows" into the Color object's state; any changes to the Color object are immediately reflected in the sub-objects, and mutating the sub-object immediately changes the Color object. You can't (I assume?) take the .rgb object from one Color instance and set it to another Color instance and get something useful out of that.

A non-tear-off example is the current TypedOM classes. Some of them have sub-objects, like the list of transform components in CSSTransformValue. But they're not linked to each other in any intimate way; the super-object doesn't even look at the sub-objects until they're passed to .set(). Between those times, you can fiddle with either and the other is unaffected; if you reuse an transform component across multiple CSSTransformValue objects, that's fine. This is an important part of Typed OM objects being "not live" that we decided on very early.

Tear-offs have lifetime implications: the super- and sub-objects keep each other alive, and they have to maintain a 1:1 correspondence; this complicates implementation. Their behavior is often mildly confusing to authors and can cause subtle bugs, when authors don't anticipate the long-term connection they have with each other, and mutating one of them unexpectedly mutates the other. The sub-objects usually can't be constructed on their own; they only come as a package with the super-object.

(There are some cases which look kinda halfway between, but are for the most part in the "non" case, like URL and URLSearchParams. A URL object comes with its own URLSearchParams, which you can't replace. But they don't communicate with each other except when you actually use the URL, at which point it reads all of its attributes; they don't keep each other alive (if you just grab the URLSearchParams object and drop the URL, the URL can get collected safely); and you can construct independent URLSearchParams objects without referencing a URL at all.)

So, they're not verboten, they're just good to avoid when we can shape the API in another way. Sometimes they're unavoidable, but usually they're not.