A 3D math playground visualizing on a canvas trait which the user needs to implement e.g. using XCB or a HTML5 Canvas for drawing as WebAssembly application. (Both exists in separate projects.)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

264 lines
8.7 KiB

//
// Some trigonometic functions with Fractions results.
// Currently only sin, cos and tan are implemented.
// As I was unable to find a really good integral approximation for them I
// implement them as a table which is predefined using the floating point
// function f64::sin and then transformed into a fraction of a given
// PRECISION.
// These approximations are quite good and for a few edge cases
// even better than the floating point implementations.
//
// Georg Hopp <georg@steffers.org>
//
// Copyright © 2019 Georg Hopp
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
use std::cmp::Ordering;
use std::ops::Neg;
use std::marker::Sized;
use crate::{Fractional, Error};
use crate::continuous::Continuous;
pub trait Trig {
fn pi() -> Self;
fn recip(self) -> Self;
fn sqrt(self) -> Result<Self, Error> where Self: Sized;
fn sintab() -> Vec<Self> where Self: Sized;
fn tantab() -> Vec<Self> where Self: Sized;
fn sin(d :i32) -> Self
where Self: Sized + Neg<Output = Self> + Copy {
match d {
0 ..=90 => Self::sintab()[d as usize],
91 ..=180 => Self::sintab()[180 - d as usize],
181..=270 => -Self::sintab()[d as usize - 180],
271..=359 => -Self::sintab()[360 - d as usize],
_ => {
Self::sin(if d < 0 { d % 360 + 360 } else { d % 360 })
},
}
}
fn cos(d :i32) -> Self
where Self: Sized + Neg<Output = Self> + Copy {
match d {
0 ..=90 => Self::sintab()[90 - d as usize],
91 ..=180 => -Self::sintab()[90 - (180 - d as usize)],
181..=270 => -Self::sintab()[90 - (d as usize - 180)],
271..=359 => Self::sintab()[90 - (360 - d as usize)],
_ => {
Self::cos(if d < 0 { d % 360 + 360 } else { d % 360 })
},
}
}
fn tan(d :i32) -> Self where Self: Sized + Copy {
match d {
0 ..=179 => Self::tantab()[d as usize],
180..=359 => Self::tantab()[d as usize - 180],
_ => {
Self::tan(if d < 0 { d % 360 + 360 } else { d % 360 })
},
}
}
}
// Try to keep precision as high as possible while having a denominator
// as small as possible. The values are taken by try and error.
const PRECISION :i64 = 1000000;
const MAX_DENOMINATOR :i64 = 7000;
// This is a really close fractional approximation for pi.
impl Trig for Fractional {
fn pi() -> Self {
Fractional(355, 113)
}
fn recip(self) -> Self {
let Fractional(n, d) = self;
Fractional(d, n)
}
// This is a really bad approximation of sqrt for a fractional...
// for (9/3) it will result 3 which if way to far from the truth,
// which is ~1.7320508075
// BUT we can use this value as starting guess for creating a
// continous fraction for the sqrt... and create a much better
// fractional representation of the sqrt.
// So, if inner converges, but is not a perfect square (does not
// end up in an Ordering::Equal - which is the l > h case)
// we use the l - 1 as starting guess for sqrt_cfrac.
// taken from:
// https://www.geeksforgeeks.org/square-root-of-an-integer/
fn sqrt(self) -> Result<Self, Error> {
// find the sqrt of x in O(log x/2).
// This stops if a perfect sqare was found. Else it passes
// the found value as starting guess to the continous fraction
// sqrt function.
fn floor_sqrt(x :i64) -> Fractional {
fn inner(l :i64, h :i64, x :i64) -> Fractional {
if l > h {
(&Continuous::sqrt(x, l - 1)).into()
} else {
let m = (l + h) / 2;
match x.cmp(&(m * m)) {
Ordering::Equal => m.into(),
Ordering::Less => inner(l, m - 1, x),
Ordering::Greater => inner(m + 1, h, x),
}
}
}
match x {
0 => 0.into(),
1 => 1.into(),
_ => inner(1, x / 2, x),
}
}
let Fractional(n, d) = self;
let n = match n.cmp(&0) {
Ordering::Equal => 0.into(),
Ordering::Less => return Err("sqrt on negative undefined"),
Ordering::Greater => floor_sqrt(n),
};
let d = match d.cmp(&0) {
Ordering::Equal => 0.into(),
Ordering::Less => return Err("sqrt on negative undefined"),
Ordering::Greater => floor_sqrt(d),
};
Ok(n / d)
}
fn sintab() -> Vec<Self> {
// hold sin Fractionals from 0 to 89 ...
// luckily with a bit of index tweeking this can also be used for
// cosine values.
lazy_static::lazy_static! {
static ref SINTAB :Vec<Fractional> =
(0..=90).map(|x| _sin(x)).collect();
}
// fractional sin from f64 sin. (From 0° to 90°)
fn _sin(d: u32) -> Fractional {
match d {
0 => Fractional(0, 1),
90 => Fractional(1, 1),
_ => reduce(d, PRECISION, &f64::sin),
}
}
SINTAB.to_vec()
}
fn tantab() -> Vec<Self> {
// This table exists only because the sin(α) / cos(α) method
// yields very large unreducable denominators in a lot of cases.
lazy_static::lazy_static! {
static ref TANTAB :Vec<Fractional> =
(0..180).map(|x| _tan(x)).collect();
}
// fractional tan from f64 tan. (From 0° to 179°)
fn _tan(d: u32) -> Fractional {
match d {
0 => Fractional(0, 1),
45 => Fractional(1, 1),
90 => Fractional(1, 0), // although they are both inf and -inf.
135 => -Fractional(1, 1),
_ => reduce(d, PRECISION, &f64::tan),
}
}
TANTAB.to_vec()
}
}
impl Trig for f64 {
fn pi() -> Self {
std::f64::consts::PI
}
fn recip(self) -> Self {
self.recip()
}
fn sqrt(self) -> Result<Self, Error> {
let x = self.sqrt();
match x.is_nan() {
true => Err("sqrt on negative undefined"),
false => Ok(x),
}
}
fn sintab() -> Vec<Self> {
lazy_static::lazy_static! {
static ref SINTAB :Vec<f64> =
(0..=90).map(|x| _sin(x)).collect();
}
// f64 sin. (From 0° to 90°)
fn _sin(d: u32) -> f64 {
match d {
0 => 0.0,
90 => 1.0,
_ => (d as f64).to_radians().sin(),
}
}
SINTAB.to_vec()
}
fn tantab() -> Vec<Self> {
// This table exists only because the sin(α) / cos(α) method
// yields very large unreducable denominators in a lot of cases.
lazy_static::lazy_static! {
static ref TANTAB :Vec<f64> =
(0..180).map(|x| _tan(x)).collect();
}
// fractional tan from f64 tan. (From 0° to 179°)
fn _tan(d: u32) -> f64 {
match d {
0 => 0.0,
45 => 1.0,
90 => std::f64::INFINITY,
135 => -1.0,
_ => (d as f64).to_radians().tan(),
}
}
TANTAB.to_vec()
}
}
// search for a fraction with a denominator less than MAX_DENOMINATOR that
// provides the minimal PRECISION criteria.
// !! With f = &f64::tan and d close to the inf boundarys of tan
// we get very large numerators because the numerator becomes a
// multiple of the denominator.
fn reduce(d :u32, p :i64, f :&dyn Fn(f64) -> f64) -> Fractional {
// This is undefined behaviour for very large f64, but our f64
// is always between 0.0 and 1000000.0 which should be fine.
let s = (f((d as f64).to_radians()) * p as f64).round() as i64;
let Fractional(n, dn) = Fractional(s, p).reduce();
match dn.abs().cmp(&MAX_DENOMINATOR) {
Ordering::Less => Fractional(n, dn),
_ => reduce(d, p + 1, f),
}
}