Skip to main content

Trigonometry in Sass

Have you ever found yourself needing trigonometric functions like sine, cosine, and tangent when writing your Sass stylesheets? Ok, probably not, but the day may come, and you’ll be glad you read this.

Update 2020-01-17: Dart Sass 1.25.0 finally added trigonometric functions to the built-in sass:math module!

Update 2019-03-26: The information in this article only applies to Ruby Sass, which has reached its end of life. Sass maintainers urge users to migrate to either Dart Sass or LibSass.

When trying to create some rotating 3D shapes with CSS, I realized that Sass lacks some key functionality. While it provides basic mathematical operators, and constructs like conditionals and loops, it doesn’t include anything related to trigonometry!

Are we able to approximate a sine or a cosine iteratively in Sass? Only one way to find out.

Numerical methods

People who know what they’re doing seem to reach for CORDIC, Chebyshev polynomials, or Remez when needing to calculate trigonometric functions.

Unfortunately, I’m not one of those people. The most my brain can parse is the Taylor expansions for sine and cosine:

$$ \begin{aligned} \sin x & = \sum _{n=0}^{\infty}{\frac {(-1)^{n}}{(2n+1)!}}x^{{2n+1}} = x - {\frac {x^{3}}{3!}} + {\frac {x^{5}}{5!}} - \cdots \\ \cos x & = \sum _{n=0}^{\infty}{\frac {(-1)^{n}}{(2n)!}}x^{{2n}} = 1 - {\frac {x^{2}}{2!}} + {\frac {x^{4}}{4!}} - \cdots \end{aligned} $$

They’ll have to do. Let’s start translating things to Sass!

Power

Numerators $x^{(2n+1)}$ and $x^{(2n)}$ require an exponentiation operator, but we don’t have one in Sass, so we’ll have to implement our own power function:

$$ \begin{aligned} b^{n} & = {\overbrace {b \times \cdots \times b}^{n}} \\ b^{-n} & = {\frac {1}{\underbrace {b \times \cdots \times b}_{n}}} \end{aligned} $$

Which could be translated into something like this:

@function pow($number, $exp) {
  $value: 1;
  @if $exp > 0 {
    @for $i from 1 through $exp {
      $value: $value * $number;
    }
  } @else if $exp < 0 {
    @for $i from 1 through -$exp {
      $value: $value / $number;
    }
  }
  @return $value;
}

Factorial

Denominators $(2n+1)!$ and $(2n)!$ require us to implement a factorial function:

$$ n! = { \begin{cases} 1 & \text{if } n = 0 \\ (n-1)! \times n & \text{if } n > 0 \end{cases} } $$

Which could look like this:

@function fact($number) {
  $value: 1;
  @if $number > 0 {
    @for $i from 1 through $number {
      $value: $value * $i;
    }
  }
  @return $value;
}

Sines, cosines, and tangents

Now we have all the necessary pieces to create our trigonometric functions, following the formulas of the Taylor expansions. Here we go:

@function pi() {
  @return 3.14159265359;
}

@function rad($angle) {
  $unit: unit($angle);
  $unitless: $angle / ($angle * 0 + 1);
  // If the angle has 'deg' as unit, convert to radians.
  @if $unit == deg {
    $unitless: $unitless / 180 * pi();
  }
  @return $unitless;
}

@function sin($angle) {
  $sin: 0;
  $angle: rad($angle);
  // Iterate a bunch of times.
  @for $i from 0 through 10 {
    $sin: $sin + pow(-1, $i) * pow($angle, (2 * $i + 1)) / fact(2 * $i + 1);
  }
  @return $sin;
}

@function cos($angle) {
  $cos: 0;
  $angle: rad($angle);
  // Iterate a bunch of times.
  @for $i from 0 through 10 {
    $cos: $cos + pow(-1, $i) * pow($angle, 2 * $i) / fact(2 * $i);
  }
  @return $cos;
}

@function tan($angle) {
  @return sin($angle) / cos($angle);
}

Do they work as expected?

@debug sin(pi() / 4); // => 0.70711
@debug cos(45deg); // => 0.70711

They do! High fives all around!

Alternatives

If you don’t mind getting your hands dirty and writing some Ruby code, you could extend the Sass::Script::Functions module with whatever functions you want. To do that, create a file (for example sass_math.rb) and paste the following contents:

require 'sass'

module Sass::Script::Functions
  module CustomMath
    def pi()
      Sass::Script::Number.new(Math::PI)
    end
    Sass::Script::Functions.declare :pi, []

    def sin(number)
      trig(:sin, number)
    end
    Sass::Script::Functions.declare :sin, [:number]

    def cos(number)
      trig(:cos, number)
    end
    Sass::Script::Functions.declare :cos, [:number]

    def tan(number)
      trig(:tan, number)
    end
    Sass::Script::Functions.declare :tan, [:number]

    private

    def trig(operation, number)
      if number.numerator_units == ['deg'] && number.denominator_units == []
        Sass::Script::Number.new(Math.send(operation, Math::PI * number.value / 180))
      else
        Sass::Script::Number.new(Math.send(operation, number.value), number.numerator_units, number.denominator_units)
      end
    end
  end

  include CustomMath
end

What we’re doing there is declaring the pi, sin, cos and tan functions so that they’re accessible from our Sass stylesheets, and then delegating all the real work to the Math module in Ruby.

Let’s invoke the sass command with our new file:

$ sass --require sass_math.rb input.scss output.css

Boom. It’s not as fun as writing everything from scratch, but it’s much more efficient.

See also