Trigonometry In Sass

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

Sass provides mathematical operators like addition and multiplication, and basic language constructs like conditionals and loops, but not much more. I’ve always relied on built-in methods, like Math.sin and Math.cos in JavaScript or Ruby, for these kinds of complex calculations…

Is there a way to approximate a sine or a cosine iteratively? It turns out there are plenty.

Numerical methods

If I had paid more attention in numerical analysis class, I may have been able to tell you everything about the CORDIC algorithm, the Chebyshev polynomials, or the Remez algorithm. But I didn’t, so the best I can do is to copy and paste the Taylor expansions for the sine and cosine functions:

$$ \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} $$

Believe it or not, these equations give approximations that are good enough for our needs. So now we only have to translate them 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, but this is even more straightforward:

$$ 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);
}

Let’s see if they work as expected:

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

High fives all around!

Alternatives

If you are using Compass you already have plenty of math helpers at your disposal, so you don’t need to write your own.

If you are not using Compass, but 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 (e.g. 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 are doing there is declaring the pi, sin, cos and tan functions so that they are accessible from our Sass stylesheets, and then delegating all the real work to the Math.* functions 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 your own functions, but it’s much more efficient.