w3c / csswg-drafts

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

[css-values] Trigonometric functions #2331

Closed LeaVerou closed 5 years ago

LeaVerou commented 6 years ago

I keep stumbling on use cases that need trigonometric functions to be solved, and I always have to resort to JS or a preprocessor for them, which is a pain. Basically almost any time you need to match two things and angles are involved.

Sadly I couldn't find most of my use cases, but here are two:

Also the following use sqrt (precomputed), but would need trigonometry in the general case :

(credits: @MeFoDy for collecting these from my book)

What do you think? Can we make it happen?

ghost commented 6 years ago

My primary use case is for syncing rotation angles and scale factors or translation distances when I animate multiple elements such that corners/ edges match throughout the animation.

In this demo, I need to sync the rotation angle of the bright yellow squares and the post-skew scale factor of the orange squares. This is achieved by doing the trigonometric computations in the JS and then setting the values to the custom properties used in the transform.

A way to do it all from the CSS would be immensely useful.

I can try to fake it with different timing functions for rotation angles and scale factors or translation distances as I did in this demo, but that's just hardcoding the cubic-bezier() for every case.

Crissov commented 6 years ago

Adding these would require a solution to enter π other than 0.5turn etc. #309

LeaVerou commented 6 years ago

Adding these would require a solution to enter π other than 0.5turn etc. #309

Um, why? (Yes, I did read the issue you linked to)

MeFoDy commented 6 years ago

Some awesome use cases in presentation by @askd:

Triangle ```css /* http://askd.rocks/pres/css/#triangle */ .triangle { position: relative; width: var(--w); height: var(--h); overflow: hidden; } .triangle::before { --a: arctan(calc(var(--w) / var(--h))); content: ''; position: absolute; left: 100%; width: 100%; height: calc(100% / cos(var(--a))); background: #000; transform-origin: 0 0; transform: rotate(var(--a)); } ```
Parallelogram ```css /* http://askd.rocks/pres/css/#parallelogram1 */ .parallelogram { --w: 400; --h: 200; position: relative; width: calc(1px * var(--w)); height: calc(1px * var(--h));; } .parallelogram::before { --angle: arcsin(calc(var(--h) / var(--w))); content: ''; position: absolute; width: calc(100% - 100% * var(--h) / var(--w) * tan(var(--angle))); height: 100%; transform-origin: 0 100%; transform: skew(calc(0 - var(--angle))); background: #000; } ```
Diagonal background animation inside the button ```css /* http://askd.rocks/pres/css/#example1 */ .button { --h: 4em; height: var(--h); padding: 0 calc(var(--h) * tan(32deg)); font-size: 40px; line-height: var(--h); overflow: hidden; text-align: left; } ```
Crissov commented 6 years ago

@LeaVerou Because @tabatkins said so in https://github.com/w3c/csswg-drafts/issues/309#issuecomment-330652717

liamquin commented 6 years ago

On Sat, 2018-02-17 at 13:45 -0800, Christoph Päper wrote:

@LeaVerou Because @tabatkins said so in https://github.com/w3c/csswg- drafts/issues/309#issuecomment-330652717

Tab made a compelling case for wanting pi as a constant; others felt a unit would be better. But even Tab admits it's not strictly necessary. That is, you could add sin, cos, tan, atan, sqrt, pow, exp, without adding numeric constants. And maybe --pi works for now.

-- Liam Quin, W3C, http://www.w3.org/People/Quin/ Staff contact for Verifiable Claims WG, SVG WG, XQuery WG Improving Web Advertising: https://www.w3.org/community/web-adv/ Personal: awesome vintage art: http://www.fromoldbooks.org/

AmeliaBR commented 6 years ago

I also periodically come across situations in CSS -- and very often in SVG -- where trig functions would be very helpful for converting between angles and x/y dimensions.

In static markup, the solution is to hard-code approximate values, but that often leaves pixel gaps or discontinuities from rounding errors. In dynamic situations, as others have mentioned, the only solution is JavaScript (with lots of converting back and forth between radians for the JS functions and degrees or turns for my design and for SVG properties, which is the only time I usually need Math.PI!).

I would argue for adding the trig functions and then wait and see how much demand there is for pi constant, or a root-2 constant. As Liam notes, an author could always define an arbitrary-precision constant as a custom property.

And most importantly, separating out the two features means that we could move ahead with functions without having to first decide the best syntax for constants.

Crissov commented 6 years ago

Which exact functions are proposed here? sin(), cos(), arctan()tan(), arcsin(), arccos(), …?

Several programming languages distinguish two arcus tangens functions: atan() with a single parameter and atan2() with two parameters.

(A generic (square) root sqrt() or exponent exp() or power pow() function or a specific square root of two constant deserves its own issue.)

ewilligers commented 6 years ago

I use sqrt(3) for 30/60 degree angles just as much as sqrt(2) for 45 degree angles.

The MVP might be sin() cos() tan() arcsin() arccos() arctan() arctan2() sqrt().

Authors and libraries can use these to define --pi, the golden ratio and --degreesPerRadian.

Crissov commented 6 years ago

Why would anyone ever need to manually specify --degreesPerRadian when CSS has angular units? @ewilligers

tabatkins commented 6 years ago

@ewilligers Yeah, that set looks useful as a first pass. More general roots and powers might be useful in the future, but we should wait and see what the uptake is on these; they cover the vast majority of what you want to do in trig.

(I'd prefer the asin/etc wording, to match JS.)

As @AmeliaBR suggests, we can leave off constants for now. I think it's probably best to address them thru the env() function, as that nicely namespaces them for use everywhere.

Nadya678 commented 6 years ago

Moz has defined: sin, sinh, cos, cosh, tan, tanh, atan, atan2, atanh... Cr also. In JS. I think names for CSS can be the same. Easier to remember.

fuchsia commented 6 years ago

hypot would be nice, too - even if it's restricted to two args, My javascript uses hypot three times more often than sqrt; it's also fewer characters to type than sqrt( x * x + y * y ), more clearly expresses the intent, and can be more accurate than the naive implementation.

css-meeting-bot commented 5 years ago

The CSS Working Group just discussed trig, and agreed to the following:

The full IRC log of that discussion <astearns> topic: trig
<astearns> github: https://github.com/w3c/csswg-drafts/issues/2331
<fantasai> leaverou: We should keep it cos()/sin() maybe tan() and only accept angle
<chris> atan2 though, surely
<fantasai> leaverou: That would solve all the usecase I listed
<fantasai> AmeliaBR: So initial proposal is to add simple trig functions in calc()
<fantasai> AmeliaBR: Once you start doing graphical layouts involving arcs and stuff, you need trig functions to convert from width/height distances to angular distances
<leaverou> s/That would solve all the usecase I listed/That would solve all the use cases listed in the issue/
<fantasai> AmeliaBR: Currently to do this you either have to do it either in a preprocessor, or if in dynamic variables have to do it in JS
<fantasai> AmeliaBR: which is a pain
<fantasai> AmeliaBR: JS only take radians, SVG only takes degrees, etc.
<fantasai> AmeliaBR: Let's just do it in CSS which has everything
<fantasai> AmeliaBR: I'd also like to get the arc functions
<dbaron> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math might be an interesting source...
<fantasai> AmeliaBR: CSS lengths, calculate angles, woudl be great
<fantasai> AmeliaBR: You can't get a diagonal across the veiwport e.g. without this
<fantasai> TabAtkins: There's a demo of the Taylor expansion of sin() using stacked variable :)
<tantek> +1 dbaron get your trig funcs from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math
<fantasai> leaverou: There's lots of examples of usage
<fantasai> AmeliaBR: I asked anatudor about it the other day
<leaverou> s/There's lots of examples of usage/We can look at preprocessors for usage stats, they've been supporting trig functions for years/
<fantasai> AmeliaBR: <quotes anatudor>
<fantasai> TabAtkins: Very useful for transforms
<fantasai> myles__: People are doing this already to day, and implementation burden is close to trivial
<fantasai> myles__: If doing arc, maybe also want atan, but probably just those
<fantasai> chris: atan2 deals with that
<fantasai> TabAtkins: ...
<fantasai> dbaron: Only add functions that are on the JS math object? Not all of them, but a subset.
<fantasai> fantasai_: atan2?
<fantasai> AmeliaBR: [explains the theory of atan]
<TabAtkins> s/.../someone asked for hypot(), rather than having to do calc(sqrt(var(--x) * var(--x) + var(--y) * var(--y))/
<fantasai> dbaron: If you have x and y coords on a graph, you typically want the tangent of y over x
<fantasai> dbaron: if you have x=1 y=1 you're in Q1, i fyou're x=-1 x=-1 your'e in Q3
<fantasai> dbaron: atan(x/y) gives you Q1 always. atan2() let's you get 270deg
<dbaron> s/x\/y/y\/x/
<fantasai> myles__: so we gonna resolve on a list of functions?
<fantasai> TabAtkins: resolve on a small list, and then maybe do more research to see if anything else needed
<fantasai> TabAtkins: but can resolve on basic trigs right now
<myles__> Sin() cos() tan() acos() asin() atan() atan2()
<fantasai> emilio: angles calc...
<fantasai> emilio: Don't want sin(calc(10px + 20%))
<AmeliaBR> and probably hypot(), sqrt(), and pow()
<fantasai> emilio: what is the type of these functions?
<fantasai> TabAtkins: I think the output is always numbers
<fantasai> TabAtkins: radians are numbers
<fantasai> fantasai_: not in CSS
<fantasai> TabAtkins: Right. Inverse ones do need to return <angle>
<cbiesinger> q+
<fantasai> TabAtkins: others return <number>
<dbaron> sin() cos() tan() take an <angle> or a <number> (radians) and return a number
<dbaron> asin() acos() atan() atan2() take a <number> (or 2, for atan2) and return an angle
<fantasai_> chris: Surely you can do atan2(20px, 4em) lengths in general
<fantasai_> ?: Yes, of course
<fantasai_> leaverou: ...
<xfq> ack cbiesinger
<fantasai_> cbiesinger: You suggested that radians can be an input, but without a pi constant how do you type that in a CSS style sheet?
<dholbert> s/.../in theory we can divide lengths and get numbers in CSS, but no browser supports that yet/
<fantasai_> TabAtkins: Use the turn unit. 0.5turn = pi
<fantasai_> AmeliaBR: We don't radians much in CSS because we have better units
<leaverou> s/leaverou: .../leaverou: (to Chris) yes, CSS in theory supports dividing lengths and it gives you a number. In practice, no UA supports this yet/
<fantasai_> AmeliaBR: But if you're calcuating it somewhere else, we can bring it in
<dbaron> so atan2(<number>, <number>) or atan2(<length>, <length>) ?
<fantasai_> TabAtkins: This is why I want to accept numbers (meaning radians) in addition to <angle>
<fantasai_> myles__: So atan2() will accept lenghts and numbers?
<fantasai_> TabAtkins: Yes
<fantasai_> myles__: atan2(10px, 5) ?
<fantasai_> TabAtkins: No, type has to match
<fantasai_> dbaron: [repeats what's above]
<fantasai_> dbaron: sin() cos() tan() take an <angle> or a <number> (radians) and return a number
<leaverou> atan2(calc(45deg * 1px), 1em)?
<fantasai_> dbaron: asin() acos() atan() atan2() take a <number> (or 2, for atan2) and return an
<fantasai_> angle
<fantasai_> AmeliaBR: ...
<fantasai_> TabAtkins: Unit division calc() should be implemented.
<dbaron> s/.../We need people to implement unit division in calc() so that people can use that as an argument to asin() and acos()/
<fantasai_> astearns: OK, I think we can resolve to add these things
<fantasai_> astearns: Objections to dbaron's proposal?
<fantasai_> TabAtkins: I think the 10 from dbaron and emilio are good
<emilio> s/emilio/AmeliaBR
<fantasai_> TabAtkins: hypot() sqrt() and ?
<florian> s/?/powe/
<florian> s/?/pow/
<xfq> sin() cos() tan() acos() asin() atan() atan2() hypot() sqrt() pow()
<fantasai_> TabAtkins: They would not adjust the unit, just multiply the magnitude
<fantasai_> fremy: Can you use these directly, or has to be inside calc()
<fantasai_> TabAtkins: Both
<fantasai_> [discussion of edits]
<fantasai_> fantasai: pow() is exponents, right? we don't want to use the ^ notation?
<fantasai_> TabAtkins: JS uses **, others use ^, but everyone's function is called pow()
<fantasai_> AmeliaBR: If you multiply two lengths together you still get a single-dimension length?
<fantasai_> TabAtkins: Multiplying two lengths would give you squared unit, but pow() and sqrt() wouldn't
<fantasai_> AmeliaBR points out inconsitency of this
<tantek> +1 pow() http://php.net/manual/en/function.pow.php
<fantasai_> TabAtkins: So maybe pow() and sqrt() should only take numbers
<fantasai_> ...
<fantasai_> myles__: If you pow(3.5, 4.7) what's the dimension in CSS?
<tantek> do we need a unit(number, unit-name) function?
<fantasai_> TabAtkins: Maybe go back to pow() and sqrt() only accept numbers.
<fantasai_> AmeliaBR: and hypot() makes it easier to deal with the most common case
<fantasai_> TabAtkins: Good argument to keep hypot()
<fantasai_> AmeliaBR: So I could draft this up?
<fantasai_> fantasai: Would be edits to css-values-4
<fantasai_> AmeliaBR: Matter of 1-2 days of writing things up now that we've hashed this out
<fantasai_> RESOLVED: Add sin() cos() tan() acos() asin() atan() atan2() hypot() sqrt() pow()
Crissov commented 5 years ago

Canʼt atan2(y, x) just be atan(y, x), cf. rgba()?

I would hate to see sqrt() be extended to two parameters later on, overriding the meaning of sq. Can we call it root() instead and let a (future) second, optional parameter default to 2 for square root?

tabatkins commented 5 years ago

atan2() is attested by that name in tons of languages; we think it's more valuable to match that usage than try to be clever in saving function names.

sqrt() is just a convenience function; it's easier and more understandable than typing pow(N, .5). I doubt we'll want to add more root functions, since people can just use pow(); square-root is just a very common operation.

An important concern in our naming was to match JS's Math.* names, as they're already familiar to devs. We're not bound to sticking with those, but we'd like to stick with them if at all possible.

valtlai commented 5 years ago

Is ** syntax for pow() really considered?

JS uses **, others use ^, but everyone's function is called pow()

Do we use pow() just for consistency with other languages?

tabatkins commented 5 years ago

Consistency is a reasonable argument I think ^_^. But also it means we get to avoid all the confusing evaluation rules; what's 2 ** 3 ** 4 equal to, what's -1 ^ 2 equal to, etc etc. JS had a lot of confusion solving these, and ended up with some interesting restrictions as a result (for example, you have to parenthesize the LHS if it's a negative value, to avoid any ambiguity).

pow(X, 2), on the other hand, is 100% unambiguous all the time.

liamquin commented 5 years ago

On Wed, 2019-02-27 at 17:34 -0800, Tab Atkins Jr. wrote:

pow(X, 2), on the other hand, is 100% unambiguous all the time.

It’s also easier to search for than ** for people trying to understand a style sheet. I think that’s valuable when the feature is likely to be used, es, but not so often that it’ll be familiar to everyone.

Liam

-- Liam Quin, https://www.delightfulcomputing.com/ Available for XML/Document/Information Architecture/XSLT/ XSL/XQuery/Web/Text Processing/A11Y training, work & consulting. Web slave for vintage clipart http://www.fromoldbooks.org/

Crissov commented 5 years ago

Iʼm all for author convenience, e.g. I still want an angular pi unit for the same reason. Javascript also has cube root, cbrt(), which should mean that some people find this convenient.

It would probably be convenient to also have functions for cotangent, secant and cosecant, even if Javascript does not provide these reciprocal functions. A reciprocal function, 1/x, for just the value, i.e. without changing the unit, might be beneficial as well, but no actual use case comes to mind.

jonathantneal commented 5 years ago

Will commas (,) or slashes (/) be used as the argument delimiter in these functions?

More recently, I’ve seen slashes (/) used as the “official” argument delimiter — like with background and grid declarations or modern color functions — while commas have been used to separate values from fallbacks — like with var(). However, slashes (/) might be confused for division symbols in math functions.

With a slash:

width: pow(2% / 4);

/* becomes */

width: 16%;

With a comma:

width: pow(2%, 4);

/* becomes */

width: 16%;

Neither feel especially wrong to me, but it looks a little funny when I add some inner calc.

With a slash:

width: pow(calc(2% / 4) / 4);

/* becomes */

width: 0.0625%;

With a comma:

width: pow(calc(2% / 4), 4);

/* becomes */

width: 0.0625%;
valtlai commented 5 years ago

How about spaces?

width: pow(2% 4);
ewilligers commented 5 years ago

By requiring commas, we allow the arguments to be <calc-sum>, like for the other math functions.

Users can write atan2(1 + 2, 3 + 4) instead of atan2(calc(1 + 2) / calc(3 + 4)).

AmeliaBR commented 5 years ago

Thanks for all the input everyone! (You're making my eventual job much easier.)

A couple quick responses:

@Crissov The consensus in the room was to match JavaScript's Math functions for naming. I agree with Tab that sqrt() should be considered a convenience function. If it turns out that we need a general root function because of numerical issues with pow (i.e., with pow(pow(x, 1/3), 3) not neatly cancelling out), then a root() could be added later.

Regarding cotangent and so on: we're not saying they'll never be added, but they aren't the priority. JS has survived without them, so we think devs can work with what they're given. Again, until we start getting complaints about numerical imprecision causing real-world problems!

@jonathantneal @valtlai The way I interpret the slash delimiter is that it is added for clarity if simple space separation is ambiguous because of optional values, and commas indicate lists of repeated compound values. But, we have lots of existing functions that use commas for

The only functions (of those we're discussing at this point) that take multiple parameters are atan2, pow, and hypot.

atan2 is interesting since it is conceptually a ratio/division, but we want the function to be able to keep track of whether the numerator or denominator or both are negative. But it would probably be weird to have a slash mean "conceptual division and separator" in this one function when it is simple division in the others. And we're never going to extend it to a list, so there's no ambiguity in using a comma.

pow could be space-separated, but again there's no harm in making it a comma separator.

hypot is more a list of values, especially if we allow it to work on more than 2 (I'd like to at least support 3 values, for 3D transform constructs). So comma works there quite naturally.

@ewilligers I definitely like the idea of skipping the nested calc() function if it can be omitted unambiguously. And as you say, it's an argument against using a slash as the separator, and against using whitespace — even if whitespace between two math expressions could technically be unambiguous to the parser, it would be very ambiguous to a human author!

nigelmegitt commented 5 years ago

@jonathantneal this from https://github.com/w3c/csswg-drafts/issues/2331#issuecomment-468473201:

With a comma:

width: pow(2%, 4);

/* becomes */

width: 16%;

disturbs me from a mathematical point of view. Isn't pow(2%, 4) the same as pow(0.02, 4) which evaluates to 0.00000016 or 0.000016%?

Is there a consensus that for % values the number before the % is subjected to the calculation as though it were unit-less and then the % is added back on again at the end? That seems weird to me...

Loirooriol commented 5 years ago

To avoid this kind of ambiguities like pow(2%, 4), I support

TabAtkins: Maybe go back to pow() and sqrt() only accept numbers

Then authors can choose pow(2, 4) * 1% to get 16% or pow(2/100, 4) * 100% to get 0.000016%.

Note this is not just about percentages, e.g. if 1in is 96px, then it doesn't make much sense if pow(1in, 2) is still 1in or 96px, but pow(96px, 2) becomes 9216px. If values with non-empty types are accepted, the types should be multiplied.

AmeliaBR commented 5 years ago

disturbs me from a mathematical point of view

Not only you!

I brought this up at the end of the face-to-face discusssion, but maybe it didn't get clearly reflected in the minutes. All mathematical operations will need to apply to dimensions in a proper way, following the CSS Typed OM rules (which treat % as a unit which may have a separate meaning from its numeric equivalent, so pow(2%, 4) would return a value with % to the power of 4).

Powers and roots will need to cancel out to get regular CSS values. This is why numeric precision may become an issue. It is slightly annoying if powers and roots don't neatly cancel out on the value. But it is a big problem if they don't neatly cancel out on the unit! One suggestion at the end of the call was to limit pow() and sqrt to plain numbers without units for now. (hypot would still be available for the most common case of manipulating lengths, since it cancels out units internally.)

nigelmegitt commented 5 years ago

Ah, sorry, I was just commenting on what I saw on this issue and haven't caught up with the minutes yet. Glad I'm not alone here!

tabatkins commented 5 years ago
  1. Yeah, we'll be using commas for argument separation, so we can use <calc-sum> for the arguments. Same as min() and max().

  2. Yes, plan is for pow() and sqrt() to only accept <number>, so we sidestep the numerical precision issues with their units. You can remove and re-add units yourself easily with some token division/multiplication, it's just some busywork. (To get pow(2px, 4) == 16px, you can instead write (pow(2px / 1px, 4) * 1px).) hypot() will take dimensions, because it outputs the same unit type as it inputs, and doesn't do anything funky internally.

  3. Adding more functions is on the table if necessary; abs() and mod(), in particular, are probably worth adding. We don't want to go too deep, as there's a lot of possible math stuff one could add, and Houdini Custom Functions is the next spec on my list once I finally finish up Typed OM sufficiently to feel happy building on it.

Jakobud commented 5 years ago

Another use case for trig functions (pow() specifically) would be use it for fluid typography where you are interpolating between multiple values across breakpoints:

https://www.smashingmagazine.com/2017/05/fluid-responsive-typography-css-poly-fluid-sizing/

Afif13 commented 5 years ago

Finally we will be able to have responsive Skewing using atan

Example: https://stackoverflow.com/a/53446820/8620333

And some other responsive shapes that relies on sin/cos/tan

https://stackoverflow.com/a/55149965/8620333 https://stackoverflow.com/a/54875824/8620333 https://stackoverflow.com/a/54909491/8620333

tabatkins commented 5 years ago

For future reference, https://test262.report/browse/built-ins/Math is the JS testsuite for Math, for when I'm writing the CSS tests.

gavinmcfarland commented 4 years ago

It's great news to hear that trigonometric functions are being considered for CSS.

Bit of a shameless plug here but I use square root when calculating the typographic scale across different devices meaning sometimes the scale needs to change. This is easier to do by just changing one CSS variable and letting CSS calculate the sizes dynamically responsively. Having sqrt() in CSS will solve a lot of issues for me.

I created a workaround for now using PostCSS. https://github.com/limitlessloop/postcss-sqrt

Also created pow() but it only works for whole numbers. https://github.com/limitlessloop/postcss-pow

tabatkins commented 4 years ago

FYI @limitlessloop, but the trig functions and sqrt(), among others, aren't just being considered, they were already accepted and are in the spec. ^_^

gavinmcfarland commented 4 years ago

@tabatkins even better, thanks for making me aware!