Skip to content

[css-values] Trigonometric functions #2331

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
LeaVerou opened this issue Feb 17, 2018 · 35 comments
Closed

[css-values] Trigonometric functions #2331

LeaVerou opened this issue Feb 17, 2018 · 35 comments

Comments

@LeaVerou
Copy link
Member

LeaVerou commented Feb 17, 2018

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
Copy link

ghost commented Feb 17, 2018

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
Copy link
Contributor

Crissov commented Feb 17, 2018

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

@LeaVerou
Copy link
Member Author

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
Copy link

MeFoDy commented Feb 17, 2018

Some awesome use cases in presentation by @askd:

Triangle
/* 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
/* 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
/* 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
Copy link
Contributor

Crissov commented Feb 17, 2018

@LeaVerou Because @tabatkins said so in #309 (comment)

@liamquin
Copy link

liamquin commented Feb 17, 2018 via email

@AmeliaBR
Copy link
Contributor

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
Copy link
Contributor

Crissov commented Feb 19, 2018

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
Copy link
Contributor

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
Copy link
Contributor

Crissov commented Feb 20, 2018

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

@tabatkins
Copy link
Member

@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
Copy link

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.

@fantasai fantasai added the css-values-4 Current Work label Mar 4, 2018
@fuchsia
Copy link

fuchsia commented Apr 6, 2018

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
Copy link
Member

css-meeting-bot commented Feb 27, 2019

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

  • RESOLVED: Add sin() cos() tan() acos() asin() atan() atan2() hypot() sqrt() pow()
The full IRC log of that discussion <astearns> topic: trig
<astearns> github: https://github.com//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
Copy link
Contributor

Crissov commented Feb 27, 2019

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
Copy link
Member

tabatkins commented Feb 27, 2019

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
Copy link
Contributor

valtlai commented Feb 28, 2019

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
Copy link
Member

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
Copy link

liamquin commented Feb 28, 2019 via email

@Crissov
Copy link
Contributor

Crissov commented Feb 28, 2019

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
Copy link
Contributor

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
Copy link
Contributor

valtlai commented Feb 28, 2019

How about spaces?

width: pow(2% 4);

@ewilligers
Copy link
Contributor

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
Copy link
Contributor

AmeliaBR commented Mar 1, 2019

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
Copy link

@jonathantneal this from #2331 (comment):

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
Copy link
Contributor

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
Copy link
Contributor

AmeliaBR commented Mar 1, 2019

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
Copy link

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
Copy link
Member

  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
Copy link

Jakobud commented Mar 14, 2019

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
Copy link

Afif13 commented Mar 15, 2019

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
Copy link
Member

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
Copy link

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
Copy link
Member

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
Copy link

@tabatkins even better, thanks for making me aware!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests