blob: 4f41ed4907aac57f00aa2238220eabb32dc50980 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef UI_GFX_GEOMETRY_SIN_COS_DEGREES_H_
#define UI_GFX_GEOMETRY_SIN_COS_DEGREES_H_
#include <algorithm>
#include <array>
#include <cmath>
#include <numbers>
#include <utility>
#include "base/numerics/angle_conversions.h"
namespace gfx {
struct SinCos {
double sin;
double cos;
bool IsZeroAngle() const { return sin == 0 && cos == 1; }
};
inline SinCos SinCosDegrees(double degrees) {
// Some math libraries have poor accuracy with large arguments,
// so range-reduce explicitly before we call sin() or cos(). However, unless
// we're _really_ large (out of range of an int), we can do that faster than
// fmod(), since we have an integer divisor (and as an extra bonus, we've
// already got it precomputed). We pick a pretty arbitrary limit that should
// be safe.
//
// We range-reduce to [0..45]. This should hit the fast path of sincos()
// on most platforms (since no further reduction is needed; reducing
// accurately modulo a trancendental can we slow), using only branches that
// should be possible to do using conditional operations; using a switch
// instead would be possible, but benchmarked much slower on M1.
// For platforms that don't use sincos() (e.g., it seems Clang doesn't
// manage the rewrite on Linux), we also save on having the range reduction
// done only once.
if (degrees > -90000000.0 && degrees < 90000000.0) {
// Make sure 0, 90, 180 and 270 degrees get exact results. (We also have
// precomputed values for 45, 135, etc., but only as a side effect of using
// 45 instead of 90, for the benefit of the range reduction algorithm below.
// The error for e.g. sin(45 degrees) is typically only 1 ulp.)
double n45degrees = degrees / 45.0;
int octant = static_cast<int>(n45degrees);
if (octant == n45degrees) {
constexpr auto kSinCosN45 = std::to_array<SinCos>({
{0, 1},
{std::numbers::sqrt2 / 2, std::numbers::sqrt2 / 2},
{1, 0},
{std::numbers::sqrt2 / 2, -std::numbers::sqrt2 / 2},
{0, -1},
{-std::numbers::sqrt2 / 2, -std::numbers::sqrt2 / 2},
{-1, 0},
{-std::numbers::sqrt2 / 2, std::numbers::sqrt2 / 2},
});
return kSinCosN45[octant & 7];
}
if (degrees < 0) {
// This will cause the range-reduction below to move us
// into [0..45], as desired, instead of [-45..0].
--octant;
}
degrees -= octant * 45.0; // Range-reduce to [0..45].
// Deal with 45..90 the same as 45..0. This also covers
// 135..180, 225..270 and 315..360, i.e. the odd octants.
// The relevant trigonometric identities is that
// sin(90 - a) = cos(a) and vice versa; we do the sin/cos
// flip below.
if (octant & 1) {
degrees = 45.0 - degrees;
}
double rad = base::DegToRad(degrees);
double s = std::sin(rad);
double c = std::cos(rad);
// 45..135 and -135..-45 can be moved into the opposite areas
// simply by flipping the x and y axis (in conjunction with
// the conversion from CW to CCW done above).
using std::swap;
if ((octant + 1) & 2) {
swap(s, c);
}
// For sine, 180..360 (lower half) is the same as 0..180,
// except negative.
if (octant & 4) {
s = -s;
}
// For cosine, 90..270 (right half) is the same as -90..90,
// except negative.
if ((octant + 2) & 4) {
c = -c;
}
return SinCos{s, c};
}
// Slow path for extreme cases.
degrees = std::fmod(degrees, 360.0);
double rad = base::DegToRad(degrees);
return SinCos{std::sin(rad), std::cos(rad)};
}
} // namespace gfx
#endif // UI_GFX_GEOMETRY_SIN_COS_DEGREES_H_