w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.5k stars 664 forks source link

[css-values-4] The rest of the JS math functions #4688

Closed tabatkins closed 4 years ago

tabatkins commented 4 years ago

Here's the list of all the properties on the JS Math object:

abs: ƒ abs()
acos: ƒ acos()
acosh: ƒ acosh()
asin: ƒ asin()
asinh: ƒ asinh()
atan: ƒ atan()
atanh: ƒ atanh()
atan2: ƒ atan2()
ceil: ƒ ceil()
cbrt: ƒ cbrt()
expm1: ƒ expm1()
clz32: ƒ clz32()
cos: ƒ cos()
cosh: ƒ cosh()
exp: ƒ exp()
floor: ƒ floor()
fround: ƒ fround()
hypot: ƒ hypot()
imul: ƒ imul()
log: ƒ log()
log1p: ƒ log1p()
log2: ƒ log2()
log10: ƒ log10()
max: ƒ max()
min: ƒ min()
pow: ƒ pow()
random: ƒ random()
round: ƒ round()
sign: ƒ sign()
sin: ƒ sin()
sinh: ƒ sinh()
sqrt: ƒ sqrt()
tan: ƒ tan()
tanh: ƒ tanh()
trunc: ƒ trunc()
E: 2.718281828459045
LN10: 2.302585092994046
LN2: 0.6931471805599453
LOG10E: 0.4342944819032518
LOG2E: 1.4426950408889634
PI: 3.141592653589793
SQRT1_2: 0.7071067811865476
SQRT2: 1.4142135623730951

I've crossed out the things we already have in the spec. Of the leftovers, I think we can break them into a few categories:

General Algebra

I'm happy to add sign() (#4673). I'd be fine with adding cbrt() too; it's lower-value than sqrt(), but sure, might as well. abs() can be done with single-argument hypot(), but might as well have the better-named version as well. So these are all plausible additions.

Hyperbolic Trig

I've never had to use the hyperbolic functions in my life. If there's a reasonable argument for them being useful, I'm fine with adding them, but until then I consider these something we can safely skip.

Logs

In general I think we could add these reasonably. exp() is currently handleable with pow(), if you write in e explicitly, but that's clumsy.

Logs seem useful, but we should just have a single function with an optional second arg for the base, rather than introduce three differently-named variants. Other than precision issues, log2(x) is exactly equivalent to log(x) / log(2), after all.

I don't think we should add log1p() and expm1(); I suspect they're just things we pulled in from Java? You can just use math, dang: log(1 + x) and calc(exp(x) - 1).

So in conclusion I think log(val, base?) (with base defaulting to e) and exp() are reasonable to add.

Rounding

Covered in #2513. Current proposal is to add a round() function, with keywords distinguishing between ceil/floor/round/trunc behavior. fround() is ignored due to lack of use-cases.

Constants

Pi and E might have reasonable cases. E can be written as exp(1), which might be reasonable to just stick with (with a note calling it out). Pi is calc(2 * acos(0)), or a few other possibilities, which is a lot less reasonable, but a lot of places that need Pi take angles already. Unclear if it's needed or not.

If we did add them, I guess they'd be added as zero-arg functions e() and pi()? Weird, but at least functions are already fine grammatically in calc expressions.

The rest of the constants are just existing easy expressions, with their values baked into constants for speed in hot loops. I don't think we need them.

Stuff We Probably Don't Want

random() can't be done; it's a totally different topic.

clz32() (round to a 32-bit integer, count number of leading zeros in the binary representation) and imul() (do a C-like 32-bit integer multiply) are both odd things that I don't think we have any usecases for.


So in conclusion, beyond the things already discussed in #4673 (sign()) and #2513 (round()), we might want to add:

...and then we're done, I think.

css-meeting-bot commented 4 years ago

The CSS Working Group just discussed the rest of the JS Math functions, and agreed to the following:

The full IRC log of that discussion <bkardell_> topic: the rest of the JS Math functions
<bkardell_> TabAtkins: I went through all of the Math functions and reviewed for consistency...
<astearns> github: https://github.com/w3c/csswg-drafts/issues/4688
<bkardell_> TabAtkins: in the issue I listed out the functions and constants - I just dumped the object in chrome, I assume everyone has the same thing...
<bkardell_> TabAtkins: I seprated them into categories
<bkardell_> TabAtkins: a handfull of algebra stuff - abs, cube root
<bkardell_> TabAtkins: you can do these yourself if you know how, but it's fine with me to just go ahead and match for parity/consistency
<bkardell_> TabAtkins: low value, but reasonable
<bkardell_> heycam: if we have this goal of 'if it is on the math object and there is no reason to not include it here'... it seems even more odd to me that round would devitate from this
<bkardell_> TabAtkins: exact correspondance is not my goal, we already can't match exactly because we have to include precision
<bkardell_> fantasai: If we aren't going for exact correspondance, we should maybe not add cube root
<dbaron> +1 for adding 'abs' and not adding 'cbrt'
<florian> +!
<bkardell_> fantasai: I do think we should add abs() because people are unlikely to understand there is anothe way to do that, it's not as intuitive
<fantasai> s/cube root/cube root right now; if there's demand for it later, we can add it later/
<RossenTheReal> q?
<bkardell_> dbaron: I think there is a small chance that it applies here, one reason some of the weird functions exist in JS might be because they can be made faster than the non-weird version
<bkardell_> TabAtkins: is that an argument for doing it or not ?
<bkardell_> dbaron: it might be an argument for someone to look into it
<bkardell_> fantasai: you could optimize it anyway
<bkardell_> dbaron: it would be harder
<bkardell_> (oriol demonstrates math the scribe couldn't understand)
<dbaron> Oriol was pointing out that cbrt(-8) is -2 whereas pow(-8, 1/3) is NaN
<dbaron> And, particularly, fractions 1/N where N is odd are not representable exactly unless N is 1 or -1.
<bkardell_> TabAtkins: next up - hyperbolic trig functions... I don't know use cases here in CSS?
<bkardell_> TabAtkins: can skip this entire category unless someone needs to object?
<bkardell_> fremy: if you have the exponent function you should be good
<bkardell_> TabAtkins: next - the family of e related functions -- log(), log 2, log 10
<bkardell_> in JavaScript it was like this - I suggest we just pass in instead of 2, 10
<dbaron> TabAtkins: log() is 2 argument with base defaulting to e
<bkardell_> TabAtkins: suggesting we just add log() and possibly exp()?
<bkardell_> fantasai: I think I agree with your basic take, I think we just do log() now and exp later
<bkardell_> TabAtkins: next is rounding, we already discussed
<dbaron> Ah, Mercator projection drawing math uses asinh().
<bkardell_> TabAtkins: the last thing is constants...
<bkardell_> TabAtkins: I can see you wanting to use some of these - Pi, E ..
<bkardell_> TabAtkins: should we add these as single argument functions? it seems like there's a challenge with globally reserved keywords
<dbaron> TabAtkins: Would break animation-names of e and pi
<bkardell_> heycam: can you not make it global, just in calc?
<chris> maybe make them ε and π (the greek letters) to avoid conflicts
<bkardell_> TabAtkins: ... maybe, probably
<fantasai> +1 to heycam's suggestion
<bkardell_> RossenTheReal: do we need them for now?
<bkardell_> TabAtkins: no non-ascii things please
<bkardell_> florian: keeping the parser non-breaking seems useful
<bkardell_> fantasai: we have an intent to use them anywhere -- as long as it is not a great complication to the parser it seems like a win
<dbaron> could we add them in a function like const(pi)?
<florian> env(pi)
<fantasai> s/use them anywhere/add keywords to calc() someday anyway/
<bkardell_> TabAtkins: actually, the units thing is not a terrible idea
<fantasai> s/a win/a usability win/
<bkardell_> dbaron: one thing that is painful about e is its interaction with exponential notation
<heycam> Tab there is responding to my /me joke to make pi and e be units
<bkardell_> dbaron: we already support things like 1e
<cbiesinger> s/1e/1e1/
<bkardell_> dbaron: I saw a suggestion - what if we stuck these inside of some other function?
<bkardell_> TabAtkins: we want this to be short is the counter argument I guess, but const is not bad
<dbaron> 1e1e is pretty ugly (in terms of having pi and e be units)
<bkardell_> myles: it could be math.e to match javascript
<bkardell_> fantasai: that would change the parser
<bkardell_> TabAtkins: which of these 3 options do we like?
<TabAtkins> 1. "e" and "pi" allowed in calc() as keywords
<TabAtkins> 2. e() and pi() available everywhere
<leaverou> const(e) might be more clear than e for people reading others' code
<TabAtkins> 3. const(e) and const(pi) available everywhere
<fremy> 2
<fantasai> 1
<florian> 1
<leaverou> 3
<fantasai> Note that 1 allows calc(e) everywhere
<dbaron> could also be math(pi) rather than const(pi)
<faceless2_> 2
<astearns> 1 or 3
<iank_> not 3
<chris> 3
<fantasai> dbaron, let's take that as a variant of 3
<jensimmons> `grid-template-layout: 100px 1fr calc(2pi);`
<jensimmons> `grid-template-layout: 100px 1fr const(pi);`
<jensimmons> `grid-template-layout: 100px 1fr pi();`
<chris> 3 > 2 > 1
<emilio> 1
<RossenTheReal> option 1: 13 votes
<RossenTheReal> option 2: 3
<jensimmons> oh I wanted to vote for 2
<jensimmons> option 2: 2
<RossenTheReal> option 3: 5 vites
<bkardell_> TabAtkins: we will take the strawpoll as evidence for now and use option 1
<jensimmons> `grid-template-layout: 100px 1fr calc(2*pi);`
<TabAtkins> Chris: This'll let us close the long-running issue that 'rad' is useless without pi?
<TabAtkins> TabAtkins: Yes
<bkardell_> TabAtkins: final bits "stuff we probably don't want"... random ... seems impossible
<bkardell_> TabAtkins: clz32 -- it's... complicated
<bkardell_> myles: it's used in video codecs
<bkardell_> TabAtkins: which you probably shouldn't be doing in css
<bkardell_> tess: I love the idea of someone trying tho
<bkardell_> TabAtkins: last one is imul()
<bkardell_> chris: what is that for even
<bkardell_> TabAtkins: we don't want it
<bkardell_> TabAtkins: pow with an e argument - I don't know if there is something here with speed
<bkardell_> fantasai: the pow function takes two args -- what if you give on?
<bkardell_> fantasai: the pow function takes two args -- what if you give one?
<bkardell_> TabAtkins: it would be invalid
<bkardell_> fantasai: maybe we should make it consistent with others?
<chris> make second argument to pow optional, defaulting to 1
<iank_> The other way to make it consistent is to not have the default arg for, log(), and add ln()
<fantasai> s/with others/with log by making it default to e/
<TabAtkins> Notes that exp(x) == pow(e, x)
<iank_> log(val, base), ln(val), pow(val, pow), exp(val)
<myles> TabAtkins: that isn't quite true to to precision (which CSS doesn't care about)
<chris> pow(foo) == pow(foo, 1) == foo
<myles> TabAtkins: but it's almost true!
<bkardell_> dbaron: so is anyone going to be annyoed by log in css being math log and not console log
<jfkthame> calculators I have known typically have a dedicated e^x key (=exp), although they also have x^y (=pow)
<bkardell_> fantasai: do you have a different suggestion dbaron ?
<bkardell_> dbaron: ln?
<fantasai> fantasai: for log of base 10?
<bkardell_> RossenTheReal: any more comments or objections on adding these 5: abs, log, exp, and the two constants e and pi
<fantasai> s/constants/keyword constants/
<bkardell_> RESOLVED: add these 5 - abs, log, exp, and the two constants e and pi
css-meeting-bot commented 4 years ago

The CSS Working Group just discussed Adding an infinity keyword, and agreed to the following:

The full IRC log of that discussion <fremy> Topic: Adding an infinity keyword
<RossenTheReal> github, https://github.com/w3c/csswg-drafts/issues/4688
<RossenTheReal> github-issue, https://github.com/w3c/csswg-drafts/issues/4688
<fantasai> github: https://github.com/w3c/csswg-drafts/issues/4688
<fremy> TabAtkins: now that we have a precedent for "e" and "pi"
<fremy> ... heycam mentioned it could be useful to add "infinity"
<fremy> ... it's not useful per se, but it would allow us to serialize infinite values coming from TypedOM
<fremy> ... instead of 1/0
<fremy> TabAtkins: Anyone has opinions on this?
<dbaron> Tab also mentions 1px/0 and the newer option infinity*1px
<fremy> oriol: could we add "NaN" too?
<fremy> TabAtkins: do we need NaN to serialize to infinity
<fremy> oriol: we currently we would need to serialize as 0/0 which is strange
<fremy> TabAtkins: I don't think we have to serialize these values at will, only when we have that as the result of a computation
<jensimmons> `grid-template-column: 1fr 1fr calc(infinity);` ??????
<fremy> TabAtkins: and we could say that NaN as a result of a computation is an infinity
<TabAtkins> s/could/already/
<fremy> heycam: Nothing to add, but yeah I find the current serialization annoying, and since we have this concept of values now, I'd like to use it
<fremy> TabAtkins: Just realized TypedOM allows injecting NaN anywhere, so we might actually need to serialize NaN anywhere
<fremy> TabAtkins: I'm then in favor of adding NaN as a keyword
<jensimmons> `margin-left: calc(-1*infinity);` ???????
<leaverou> transitions to infinity would be ...interesting
<fremy> TabAtkins: (if we add any of them)
<fremy> TabAtkins: to answer to leaverou, we can already do that, we just would need to write as a calc() right now
<fremy> dbaron: Serialization would be lower case?
<fremy> TabAtkins: yeah, I'd serialize lowercase
<fantasai> s/I'd/CSS would/
<fremy> TabAtkins: but we could do either way, no strong opinion
<fantasai> s/lowercase/lowercase by default/
<fremy> jensimmons: what capabilities are we adding by allowing these keywords?
<chris> z-index: -infinity
<fremy> TabAtkins: none, you can already get those results today by doing computations
<fremy> TabAtkins: (we clamp to the maximum value if we need to)
<jensimmons> `margin-left: calc(-1*(1/0));` works today??
<fremy> TabAtkins: so, what's the room's feelings about this?
<TabAtkins> Yes it does
<astearns> it's defined, in any case
<fremy> chris: there are also people who want rational numbers, so outputting 1/0 as a returned value would give a bad precedent
<tantek_> -∞
<heycam> -oo
<fremy> chris: negative infinity would have to require a space, correct?
<chris> RESOLVED: no non-ASCII in Core CSS
<fremy> TabAtkins: yes, we would need to be careful, like usual for math in css
<fantasai> heycam++
<fremy> (the room seems to have too much fun)
<fremy> TabAtkins: since nobody objected, let's resolve to add them
<jfkthame> heycam, you need some negative letter-spacing there
<tantek_> could you list then explicitly in IRC?
<fremy> RESOLVED: add infinity and nan as new css math constants
<tantek_> I thought I heard NaN or nand?
<TabAtkins> calc(infinity), calc(-infinity), and calc(nan)
<chris> -moz-infinity
<fremy> myles: -infinity sounds odd
<myles> calc(negative-infinity)
<tantek_> -infinity-infinity
<jensimmons> calc(-1*infinity)
<fremy> TabAtkins: idents can start with dash
<tantek_> == 0?
<fremy> dbaron: but usually it's for vendor prefixes
<fremy> TabAtkins: (missed)
<fremy> RossenTheReal: let's move on
Crissov commented 4 years ago

log1p(), expm1() etc. are recommended in IEEE 754, which also suggests stuff like sinPi(x) = sin(x*PI).

Crissov commented 4 years ago

In #309, I argued for a new angular unit pi or pirad. Exponential notation inherited from SVG makes the letter e unfit to be used as a unit, but it is required way less often than π or τ in planar geometry, which is the scope of CSS.

I believe Eulerʼs number would be fine as exp(1), which would be available anyway and is shorter than const(e), calc(e) or math(e). It would be strange to have e() as well, whereas a single-letter keyword would be fine.

Crissov commented 4 years ago

<dbaron> Oriol was pointing out that cbrt(-8) is -2 whereas pow(-8, 1/3) is NaN <dbaron> And, particularly, fractions 1/N where N is odd are not representable exactly unless N is 1 or -1.

How was this not convincing enough to also add cbrt() or, alternatively, inv() for 1/x to get pow(-8, inv(3)) = -2?

tabatkins commented 4 years ago

I've added everything in 90ff717f and the several preceding commits.

tabatkins commented 4 years ago

log1p(), expm1() etc. are recommended in IEEE 754, which also suggests stuff like sinPi(x) = sin(x*PI).

Yeah, special-cases for those can be useful for increased precision and faster calculation in potentially perf-sensitive locations. Neither of those are particularly relevant in CSS.

In #309, I argued for a new angular unit pi or pirad.

We've added the keyword pi now. You do need to do standard calc math at it: calc(2 * pi), not calc(2pi). (Using a unit was discussed, but it was rejected for a few reasons: it would be breaking new ground to have a unit which resolves to a <number>; it would be inconsistent with e due to the exp-notation clash you mentioned; in general it's a bit too clever of a hack.)

I believe Eulerʼs number would be fine as exp(1), which would be available anyway and is shorter than const(e), calc(e) or math(e). It would be strange to have e() as well, whereas a single-letter keyword would be fine.

exp(1) is shorter than calc(e), yes, but calc(exp(1) - 1) is longer than calc(e - 1). Given that using e directly as a value is something I expect to be quite odd and rare, I don't think we need to optimize for that.

How was this not convincing enough to also add cbrt() or, alternatively, inv() for 1/x to get pow(-8, inv(3)) = -2?

We don't have any particular use-cases for cube-rooting negative numbers right now. Note that its addition is still open if someone comes up with a sufficiently convincing use-case, either showing that cube-rooting in general is common enough to be worth a special-case, or showing that cube roots of negative numbers in particular are useful in reasonable scenarios (probably a lower threshold needed than the previous).

Something like inv() wouldn't really work; the model we're using right now still assumes that subtrees can be collapsed to numeric values representable in a float, double, or similar representation. Explicit rationals are out of scope currently. If we did decide this was worthwhile, we could do it with a root() function, that could then have the desired behavior for odd integer roots.