From a0c638c5575fd5b43dde1ecfc4b1e7b7ec1c990e Mon Sep 17 00:00:00 2001 From: Georg Hopp Date: Fri, 10 Jan 2020 18:08:52 +0100 Subject: [PATCH] Start implement 3d from fractional as html5 canvas --- tutorial/wasm-game-of-life/Cargo.toml | 1 + tutorial/wasm-game-of-life/src/easel.rs | 520 ++++++++++++++++++ tutorial/wasm-game-of-life/src/geometry.rs | 377 +++++++++++++ tutorial/wasm-game-of-life/src/lib.rs | 129 +++++ tutorial/wasm-game-of-life/src/transform.rs | 186 +++++++ .../wasm-game-of-life/src/trigonometry.rs | 143 +++++ tutorial/wasm-game-of-life/src/vector.rs | 139 +++++ tutorial/wasm-game-of-life/www/index.html | 1 + tutorial/wasm-game-of-life/www/index.js | 21 +- 9 files changed, 1515 insertions(+), 2 deletions(-) create mode 100644 tutorial/wasm-game-of-life/src/easel.rs create mode 100644 tutorial/wasm-game-of-life/src/geometry.rs create mode 100644 tutorial/wasm-game-of-life/src/transform.rs create mode 100644 tutorial/wasm-game-of-life/src/trigonometry.rs create mode 100644 tutorial/wasm-game-of-life/src/vector.rs diff --git a/tutorial/wasm-game-of-life/Cargo.toml b/tutorial/wasm-game-of-life/Cargo.toml index f19795d..dcf18a8 100644 --- a/tutorial/wasm-game-of-life/Cargo.toml +++ b/tutorial/wasm-game-of-life/Cargo.toml @@ -12,6 +12,7 @@ default = ["console_error_panic_hook"] [dependencies] wasm-bindgen = "0.2" +lazy_static = "1.4.0" # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/tutorial/wasm-game-of-life/src/easel.rs b/tutorial/wasm-game-of-life/src/easel.rs new file mode 100644 index 0000000..c9a3f7e --- /dev/null +++ b/tutorial/wasm-game-of-life/src/easel.rs @@ -0,0 +1,520 @@ +// +// This is an abstraction over a drawing environment. +// Future note: z-Buffer is described here: +// https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/perspective-correct-interpolation-vertex-attributes +// +// Georg Hopp +// +// 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 . +// +use std::cmp; +use std::fmt::{Formatter, Debug, Display, Result}; +use std::ops::{Add, Sub, Div}; +use std::sync::mpsc; + +pub trait Easel { + //fn canvas(&mut self, width :u16, height :u16) -> Option<&dyn Canvas>; +} + +pub trait Canvas { + fn init_events(&self); + fn start_events(&self, tx :mpsc::Sender); + + fn width(&self) -> u16; + fn height(&self) -> u16; + + fn clear(&mut self); + fn draw(&mut self, c :&dyn Drawable, ofs :Coordinate, color :u32); + fn put_text(&self, ofs :Coordinate, s :&str); + fn set_pixel(&mut self, c :Coordinate, color :u32); + fn show(&self); +} + +pub trait Drawable { + fn plot(&self) -> Coordinates; +} + +pub trait Fillable +where T: Add + Sub + Div + + Debug + Copy + From { + fn fill(&self, canvas :&mut dyn Canvas, color :u32); +} + +#[derive(Debug, Clone, Copy)] +pub struct Coordinate(pub i32, pub i32, pub T); + +#[derive(Debug, Clone)] +pub struct Coordinates(pub Vec>); + +#[derive(Debug, Clone, Copy)] +pub struct LineIterator where T: Debug { + a :Option> + , b :Coordinate + , dx :i32 + , dy :i32 + , dz :T + , sx :i32 + , sy :i32 + , err :i32 + , only_edges :bool +} + +impl Iterator for LineIterator +where T: Add + Debug + Copy + From { + type Item = Coordinate; + + fn next(&mut self) -> Option { + match self.a { + None => None, + Some(a) => { + let Coordinate(ax, ay, az) = a; + let Coordinate(bx, by, _) = self.b; + + if ax != bx || ay != by { + match (2 * self.err >= self.dy, 2 * self.err <= self.dx ) { + (true, false) => { + let r = self.a; + self.a = Some(Coordinate( ax + self.sx + , ay + , az + self.dz )); + self.err = self.err + self.dy; + if self.only_edges { self.next() } else { r } + }, + (false, true) => { + let r = self.a; + self.a = Some(Coordinate( ax + , ay + self.sy + , az + self.dz )); + self.err = self.err + self.dx; + r + }, + _ => { + let r = self.a; + self.a = Some(Coordinate( ax + self.sx + , ay + self.sy + , az + self.dz )); + self.err = self.err + self.dx + self.dy; + r + }, + } + } else { + self.a = None; + Some(self.b) + } + } + } + } +} + +impl Coordinate +where T: Add + Sub + Div + + Debug + Clone + Copy + From { + fn iter(self, b :&Self, only_edges :bool) -> LineIterator { + let Coordinate(ax, ay, az) = self; + let Coordinate(bx, by, bz) = *b; + + let dx = (bx - ax).abs(); + let dy = -(by - ay).abs(); + + LineIterator { a: Some(self) + , b: *b + , dx: dx + , dy: dy + , dz: (bz - az) / cmp::max(dx, -dy).into() + , sx: if ax < bx { 1 } else { -1 } + , sy: if ay < by { 1 } else { -1 } + , err: dx + dy + , only_edges: only_edges + } + } + + fn line_iter(self, b :&Self) -> LineIterator { + self.iter(b, false) + } + + fn line(self, b :&Self) -> Vec { + self.line_iter(b).collect() + } + + fn edge_iter(self, b :&Self) -> LineIterator { + self.iter(b, true) + } + + fn edge(self, b :&Self) -> Vec { + self.edge_iter(b).collect() + } + + fn face(edges :&[Self]) -> Vec { + edges.to_vec() + } +} + +impl Display for Coordinate { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "<{},{}>", self.0, self.1) + } +} + +impl Display for Coordinates where T: Copy { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let Coordinates(is) = self; + + let c = match is[..] { + [] => String::from(""), + [a] => format!("{}", a), + _ => { + let mut a = format!("{}", is[0]); + for i in is[1..].iter() { + a = a + &format!(",{}", i); + } + a + } + }; + + write!(f, "Coordinates[{}]", c) + } +} + + +#[derive(Debug, Clone, Copy)] +pub struct Point(pub Coordinate); + +impl Drawable for Point where T: Copy { + fn plot(&self) -> Coordinates { + let Point(c) = *self; + Coordinates(vec!(c)) + } +} + +impl Display for Point { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let Point(p) = self; + write!(f, "Point[{}]", p) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Line(pub Coordinate, pub Coordinate); + +impl Drawable for Line +where T: Add + Sub + Div + + Debug + Clone + Copy + From { + fn plot(&self) -> Coordinates { + let Line(a, b) = *self; + Coordinates(a.line(&b)) + } +} + +impl Display for Line { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let Line(a, b) = self; + write!(f, "Line[{},{}]", a, b) + } +} + +#[derive(Debug, Clone)] +pub struct Polyline(pub Coordinates); + +impl Drawable for Polyline +where T: Add + Sub + Div + + Debug + Clone + Copy + From { + fn plot(&self) -> Coordinates { + let Polyline(Coordinates(cs)) = self; + + match cs[..] { + [] => Coordinates(Vec::>::new()), + [a] => Coordinates(vec!(a)), + [a, b] => Coordinates(a.line(&b)), + _ => { + let (a, b) = (cs[0], cs[1]); + let mut r = a.line(&b); + let mut i = b; + for j in cs[2..].iter() { + r.append(&mut i.line(j)[1..].to_vec()); + i = *j; + } + Coordinates(r) + }, + } + } +} + +impl Display for Polyline where T: Copy { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let Polyline(a) = self; + write!(f, "PLine[{}]", a) + } +} + +#[derive(Debug, Clone, Copy)] +enum Direction { Left, Right } + +#[derive(Debug, Clone)] +pub struct Polygon(pub Coordinates); + +#[derive(Debug, Clone)] +enum VertexIteratorMode { Vertex, Edge } +#[derive(Debug, Clone)] +pub struct VertexIterator<'a,T> where T: Debug { + p :&'a Polygon, + top :usize, + current :Option, + edge :Option>, + mode :VertexIteratorMode, + direction :Direction, +} + +impl<'a,T> VertexIterator<'a,T> +where T: Add + Sub + Div + + Debug + Copy + From { + fn edge(p :&'a Polygon, direction :Direction) -> Self { + let top = p.vert_min(direction); + let next = p.next_y(top, direction); + let edge = match next { + None => None, + Some(next) => Some(p.vertex(top).edge_iter(&p.vertex(next))), + }; + + VertexIterator { p: p + , top: top + , current: next + , edge: edge + , mode: VertexIteratorMode::Edge + , direction: direction } + } + + fn vertex(p :&'a Polygon, direction :Direction) -> Self { + let top = p.vert_min(direction); + let next = p.next_y(top, direction); + + VertexIterator { p: p + , top: top + , current: next + , edge: None + , mode: VertexIteratorMode::Vertex + , direction: direction } + } + + // if this yields "None" we are finished. + fn next_edge(&mut self) -> Option> { + let current = self.current?; + let next = self.p.next_y(current, self.direction)?; + let mut edge = self.p.vertex(current).edge_iter(&self.p.vertex(next)); + + match edge.next() { + // It should be impossible that a new edge iterator has no values + // at all… anyway, just in case I handle it here. + None => self.next_edge(), + Some(_) => { + self.current = Some(next); + self.edge = Some(edge); + self.edge + }, + } + } +} + +impl<'a,T> Iterator for VertexIterator<'a,T> +where T: Add + Sub + Div + + Debug + Copy + From { + type Item = Coordinate; + + fn next(&mut self) -> Option { + match self.mode { + VertexIteratorMode::Edge => { + // if for whatever reason edge is "None" finish this iterator. + let next = self.edge.as_mut()?.next(); + + match next { + Some(_) => next, + None => { + self.next_edge()?; + self.next() + }, + } + }, + VertexIteratorMode::Vertex => { + let current = self.current?; + self.current = self.p.next_y(current, self.direction); + Some(self.p.vertex(current)) + }, + } + } +} + +impl Polygon +where T: Add + Sub + Div + + Copy + Debug + From { + #[inline] + fn vertex(&self, v :usize) -> Coordinate { + let Polygon(Coordinates(cs)) = self; + cs[v] + } + + fn vert_min<'a>(&'a self, d :Direction) -> usize { + let Polygon(Coordinates(cs)) = self; + + type ICoord<'a,T> = (usize, &'a Coordinate); + + // TODO I guess the problem here is that it does not account for the + // same y vertex on the beggining and the end. So i guess correct + // would be finding the first one and then dependings on the + // given direction either search left or right for same y's. + let fold = |acc :Option>, x :ICoord<'a,T>| + match acc { + None => Some(x), + Some(a) => { + let Coordinate(_, ay, _) = a.1; + let Coordinate(_, xy, _) = x.1; + if xy < ay {Some(x)} else {Some(a)} + }, + }; + + let mut min = cs.iter().enumerate().fold(None, fold).unwrap().0; + let mut next = self.step(min, d); + + while self.vertex(min).1 == self.vertex(next).1 { + min = next; + next = self.step(min, d); + } + + min + } + + fn left_edge(&self) -> VertexIterator { + VertexIterator::edge(self, Direction::Left) + } + + fn right_edge(&self) -> VertexIterator { + VertexIterator::edge(self, Direction::Right) + } + + fn left_vertices(&self) -> VertexIterator { + VertexIterator::vertex(self, Direction::Left) + } + + fn right_vertices(&self) -> VertexIterator { + VertexIterator::vertex(self, Direction::Right) + } + + fn left(&self, v :usize) -> usize { + let Polygon(Coordinates(cs)) = self; + + match v { + 0 => cs.len() - 1, + _ => v - 1, + } + } + + fn right(&self, v :usize) -> usize { + let Polygon(Coordinates(cs)) = self; + + (v + 1) % cs.len() + } + + fn step(&self, v :usize, d :Direction) -> usize { + match d { + Direction::Left => self.left(v), + Direction::Right => self.right(v), + } + } + + fn next_y(&self, c :usize, d :Direction) -> Option { + fn inner( p :&Polygon + , c :usize + , n :usize + , d :Direction) -> Option + where T: Add + Sub + Div + + Copy + Debug + From { + if c == n { + None + } else { + let Coordinate(_, cy, _) = p.vertex(c); + let Coordinate(_, ny, _) = p.vertex(n); + + if ny < cy { None } else { Some(n) } + } + } + + inner(self, c, self.step(c, d), d) + } + + pub fn debug(&self) { + let mut left = self.left_vertices(); + let mut right = self.right_vertices(); + + if left.find(|l| right.find(|r| l.0 == r.0).is_some()).is_some() { + let left :Vec> = self.left_vertices().collect(); + let right :Vec> = self.right_vertices().collect(); + + println!("==="); + println!("== poly : {:?}", self); + println!("== ltop : {:?}", self.vert_min(Direction::Left)); + println!("== rtop : {:?}", self.vert_min(Direction::Right)); + println!("== left : {:?}", left); + println!("== right : {:?}", right); + println!("==="); + } + } +} + +impl Drawable for Polygon +where T: Add + Sub + Div + + Debug + Clone + Copy + From { + fn plot(&self) -> Coordinates { + let Polygon(Coordinates(cs)) = self; + + match cs[..] { + [] => Coordinates(Vec::>::new()), + [a] => Coordinates(vec!(a)), + [a, b] => Coordinates(a.line(&b)), + _ => { + let (a, b) = (cs[0], cs[1]); + let mut r = a.line(&b); + let mut i = b; + for j in cs[2..].iter() { + r.append(&mut i.line(j)[1..].to_vec()); + i = *j; + } + let mut j = a.line(&i); + let l = j.len(); + if l > 1 { + r.append(&mut j[1..l-1].to_vec()); + } + Coordinates(r) + }, + } + } +} + +impl Fillable for Polygon +where T: Add + Sub + Div + + Debug + Clone + Copy + From { + fn fill(&self, canvas :&mut dyn Canvas, color :u32) { + let scanlines = self.left_edge().zip(self.right_edge()); + + for l in scanlines.flat_map(|(l, r)| l.line_iter(&r)) { + canvas.set_pixel(l, color); + } + } +} + +impl Display for Polygon where T: Copy { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let Polygon(a) = self; + write!(f, "Poly[{}]", a) + } +} diff --git a/tutorial/wasm-game-of-life/src/geometry.rs b/tutorial/wasm-game-of-life/src/geometry.rs new file mode 100644 index 0000000..0320378 --- /dev/null +++ b/tutorial/wasm-game-of-life/src/geometry.rs @@ -0,0 +1,377 @@ +// +// Basic geometric things... +// +// Georg Hopp +// +// 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 . +// +use std::convert::{From, Into}; +use std::ops::{Add,Sub,Neg,Mul,Div}; +use std::fmt::Debug; + +use crate::easel::{Canvas, Coordinate, Coordinates, Polygon}; +use crate::transform::{TMatrix, Transformable}; +use crate::trigonometry::Trig; +use crate::vector::Vector; + +#[derive(Debug, Clone)] +pub struct Face +where T: Add + Sub + Neg + Mul + Div + Copy + Trig { + corners :Vec, + normal :Option>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct Point(pub Vector, T) + where T: Add + Sub + Neg + Mul + Div + PartialEq + Copy + Trig; + +impl Point +where T: Add + Sub + Neg + + Mul + Div + + PartialEq + Trig + Copy + From { + pub fn new(x :T, y :T, z :T) -> Self { + Self(Vector(x, y, z), 1.into()) + } +} + +impl Add for Point +where T: Add + Sub + Neg + + Mul + Div + + PartialEq + Trig + Copy { + type Output = Self; + + fn add(self, other :Self) -> Self { + let Point(v1, w1) = self; + let Point(v2, w2) = other; + Self(v1 + v2, w1 + w2) + } +} + +impl Neg for Point +where T: Add + Sub + Neg + + Mul + Div + + PartialEq + Trig + Copy { + type Output = Self; + + fn neg(self) -> Self { + let Point(v, w) = self; + Self(-v, -w) + } +} + +impl Sub for Point +where T: Add + Sub + Neg + + Mul + Div + + PartialEq + Trig + Copy { + type Output = Self; + + fn sub(self, other :Self) -> Self { + self + -other + } +} + +impl Mul for Point +where T: Add + Sub + Neg + + Mul + Div + + PartialEq + Trig + Copy + From { + type Output = Self; + + fn mul(self, other :Self) -> Self { + let a :Vector = self.into(); + let b :Vector = other.into(); + + Point(a * b, 1.into()) + } +} + +impl From> for Point +where T: Add + Sub + Neg + + Mul + Div + + PartialEq + Trig + Copy + From { + fn from(v :Vector) -> Self { + Point(v, 1.into()) + } +} + +impl Into> for Point +where T: Add + Sub + Neg + + Mul + Div + + PartialEq + Trig + Copy + From { + fn into(self) -> Vector { + let Point(v, w) = self; + + if w == 0.into() { + v + } else { + v.mul(&w.recip()) + } + } +} + +impl Transformable for Point +where T: Add + Sub + Neg + + Mul + Div + + PartialEq + Debug + Trig + Copy + From { + fn transform(&self, m :&TMatrix) -> Self { + let Point(v, w) = *self; + let (v, w) = m.apply(&v, w); + + if w == 0.into() { + v.into() + } else { + v.mul(&w.recip()).into() + } + } +} + +#[derive(Debug)] +pub struct Polyeder +where T: Add + Sub + Neg + Mul + Div + PartialEq + Copy + Trig { + points :Vec>, + faces :Vec>, +} + +pub trait Primitives +where T: Add + Sub + Neg + Mul + Div + Debug + Copy + Trig + From { + fn transform(&self, m :&TMatrix) -> Self; + fn project( &self + , camera :&Camera + , light :&DirectLight + , col :u32 ) -> Vec<(Polygon, u32)>; +} + +#[derive(Debug, Clone, Copy)] +pub struct Camera +where T: Add + Sub + Neg + Mul + Div + Debug + Copy + Trig + From { + width :T, + height :T, + distance :T, + project :TMatrix, +} + +pub struct DirectLight +where T: Add + Sub + Neg + Mul + Div + Debug + Copy + Trig + From { + direction: Vector, +} + +impl Camera +where T: Add + Sub + Neg + + Mul + Div + + PartialEq + Debug + Copy + Trig + From { + // This code assumes that the size of the viewport is always + // equal to the size of the physical screen… e.g. window/canvas thus some + // effects can't be done. See book for examples with different viewport + // and screen sizes. + pub fn new(c :&dyn Canvas, angle :i32) -> Self { + let width :T = (c.width() as i32).into(); + let height :T = (c.height() as i32).into(); + let d :T = 1.into(); + let fov = T::cot(angle) * width; + let wh = width / 2.into(); + let hh = height / 2.into(); + + Camera { width: width + , height: height + , distance: d + , project: TMatrix::new( + ( fov, 0.into(), wh, 0.into()) + , (0.into(), fov, hh, 0.into()) + , (0.into(), 0.into(), d, 1.into()) + , (0.into(), 0.into(), 1.into(), 0.into()) ) } + } + + pub fn get_distance(&self) -> T { + self.distance + } + + pub fn get_projection(&self) -> TMatrix { + self.project + } + + pub fn project(&self, p :Point) -> Point { + p.transform(&self.project) + } +} + +impl DirectLight +where T: Add + Sub + Neg + + Mul + Div + + Debug + Copy + Trig + From { + pub fn new(v :Vector) -> Self { + DirectLight{ direction: v } + } + + pub fn dir(&self) -> Vector { + self.direction + } +} + +impl Face +where T: Add + Sub + Neg + + Mul + Div + + PartialEq + Debug + Copy + Trig + From { + fn new(corners :Vec, ps :&[Point]) -> Self { + let mut f = Face{ corners: corners, normal: None }; + f.update_normal(ps); + f + } + + fn update_normal(&mut self, ps :&[Point]) { + let edge10 :Vector = (ps[self.corners[1]] - ps[self.corners[0]]).into(); + let edge12 :Vector = (ps[self.corners[1]] - ps[self.corners[2]]).into(); + self.normal = Some(edge10 * edge12); + } +} + +impl Polyeder +where T: Add + Sub + Neg + + Mul + Div + + PartialEq + Debug + Copy + Trig + From { + fn update_normals(&mut self) { + for f in self.faces.iter_mut() { + f.update_normal(&self.points); + } + } + + // construct via cube, see polyhedra.pdf + pub fn tetrahedron(a :T) -> Polyeder { + let f2 :T = 2.into(); + let ch = a / (f2 * T::sqrt(f2).unwrap()); + + let ps = vec!( Point::new(-ch, -ch, ch) // A + , Point::new(-ch, ch, -ch) // C + , Point::new( ch, -ch, -ch) // E + , Point::new( ch, ch, ch) ); // G + + // bottom: 1, 2, 3 + let fs = vec!( Face::new(vec!(2, 1, 0), &ps) // bottom + , Face::new(vec!(3, 2, 0), &ps) + , Face::new(vec!(0, 1, 3), &ps) + , Face::new(vec!(1, 2, 3), &ps) ); + //let fs = vec!( Face::new(vec!(0, 1, 2), &ps) // bottom + // , Face::new(vec!(0, 2, 3), &ps) + // , Face::new(vec!(3, 1, 0), &ps) + // , Face::new(vec!(3, 2, 1), &ps) ); + + Polyeder{ points: ps, faces: fs } + } + + pub fn triangle(a :T) -> Polyeder { + let f0 :T = 0.into(); + let f3 :T = 3.into(); + let f6 :T = 6.into(); + let zi :T = T::sqrt(f3).unwrap() / f6 * a; + let zc :T = T::sqrt(f3).unwrap() / f3 * a; + let ah :T = a / 2.into(); + + let ps = vec!( Point::new(-ah, f0, -zi) + , Point::new( f0, f0, zc) + , Point::new( ah, f0, -zi) ); + + let fs = vec!(Face::new(vec!(0, 1, 2), &ps)); + + Polyeder{ points: ps, faces: fs } + } + + pub fn cube(a :T) -> Polyeder { + let ah :T = a / From::::from(2); + + let ps = vec!( Point::new(-ah, ah, -ah) // 0 => front 1 + , Point::new(-ah, -ah, -ah) // 1 => front 2 + , Point::new( ah, -ah, -ah) // 2 => front 3 + , Point::new( ah, ah, -ah) // 3 => front 4 + , Point::new(-ah, ah, ah) // 4 => back 1 + , Point::new(-ah, -ah, ah) // 5 => back 2 + , Point::new( ah, -ah, ah) // 6 => back 3 + , Point::new( ah, ah, ah) ); // 7 => back 4 + + let fs = vec!( Face::new(vec!(0, 1, 2, 3), &ps) // front + , Face::new(vec!(7, 6, 5, 4), &ps) // back + , Face::new(vec!(1, 5, 6, 2), &ps) // top + , Face::new(vec!(0, 3, 7, 4), &ps) // bottom + , Face::new(vec!(0, 4, 5, 1), &ps) // left + , Face::new(vec!(2, 6, 7, 3), &ps) ); // right + + Polyeder{ points: ps, faces: fs } + } +} + +impl Primitives for Polyeder +where T: Add + Sub + Neg + + Mul + Div + + Debug + Copy + Trig + From + PartialOrd { + // TODO Maybe this should also be an instance of Transformable… + fn transform(&self, m :&TMatrix) -> Self { + let Polyeder{ points: ps, faces: fs } = self; + + let mut p = Polyeder{ + points: ps.iter().map(|p| p.transform(m)).collect() + , faces: fs.to_vec() + }; + + // TODO alternatively we could rotate the normals too, but this cannot + // done with the original matrix… the question is, what is faster. + p.update_normals(); + p + } + + fn project( &self + , camera :&Camera + , light :&DirectLight + , color :u32 ) -> Vec<(Polygon, u32)> { + // Helper to create a Polygon from Coordinates… + // TODO probably there needs to be a Polygon constructor for this. + fn polygon(c :I) -> Polygon + where I: Iterator> { + Polygon(Coordinates(c.collect())) + } + + // this one does the projection... as the projection was the last + // matrix we do not need to do it here. + let to_coord = |p :&usize| { + let Point(v, _) = camera.project(self.points[*p]); + Coordinate(T::round(&v.x()), T::round(&v.y()), v.z() - 1.into()) + }; + let to_poly = |f :&Face| { + let pg = polygon(f.corners.iter().map(to_coord)); + let mut r :T = (((color >> 16) & 0xFF) as i32).into(); + let mut g :T = (((color >> 8) & 0xFF) as i32).into(); + let mut b :T = (((color ) & 0xFF) as i32).into(); + let lf :T = match f.normal { + None => 1.into(), + Some(n) => n.dot(light.dir()) + / (n.mag() * light.dir().mag()), + }; + + // this "if" represents a first simple backface culling + // approach. We only return face that face towards us. + if lf < 0.into() { + r = r * -lf; + g = g * -lf; + b = b * -lf; + + let c :u32 = (r.round() as u32) << 16 + | (g.round() as u32) << 8 + | (b.round() as u32); + + Some((pg, c)) + } else { + None + }}; + + self.faces.iter().filter_map(to_poly).collect() + } +} diff --git a/tutorial/wasm-game-of-life/src/lib.rs b/tutorial/wasm-game-of-life/src/lib.rs index d5c453c..72b206e 100644 --- a/tutorial/wasm-game-of-life/src/lib.rs +++ b/tutorial/wasm-game-of-life/src/lib.rs @@ -1,6 +1,24 @@ +extern crate lazy_static; + +pub type Error = &'static str; + +pub mod easel; +pub mod transform; +pub mod trigonometry; +pub mod vector; +pub mod geometry; + mod utils; +use vector::Vector; +use easel::{Canvas, Coordinate, Drawable, Fillable}; +use geometry::{Camera, DirectLight, Polyeder, Primitives}; +use transform::{TMatrix}; + use std::fmt::{Display, Formatter, Result}; +use std::ptr; +use std::sync::mpsc; +use std::time::Instant; use wasm_bindgen::prelude::*; // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global @@ -9,6 +27,117 @@ use wasm_bindgen::prelude::*; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; +#[wasm_bindgen] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Color(u8, u8, u8, u8); + +#[wasm_bindgen] +pub struct View3d { width :u16 + , height :u16 + , size :usize + , start :Instant + , tetrahedron :Polyeder + , cube :Polyeder + , camera :Option> + , light :DirectLight + , zbuf :Vec + , image :Vec +} + +#[wasm_bindgen] +impl View3d { + pub fn new(width :u16, height :u16) -> Self { + let size = width as usize * height as usize; + let light_vector = Vector(0.0, 0.0, 1.0); + + let mut view3d = Self { width: width + , height: height + , size: size + , start: Instant::now() + , tetrahedron: Polyeder::tetrahedron(100.0) + , cube: Polyeder::cube(56.25) + , camera: None + , light: DirectLight::new(light_vector) + , zbuf: vec!(0.0; size) + , image: vec!(Color(0, 0, 0, 0); size) }; + + view3d.camera = Some(Camera::::new(&view3d, 45)); + view3d + } + + pub fn update(mut self) { + let deg = ((self.start.elapsed() / 25).as_millis() % 360) as i32; + + let t = TMatrix::translate(Vector(0.0, 0.0, 150.0)); + let rz = TMatrix::rotate_z(deg); + let rx = TMatrix::rotate_x(-deg*2); + let ry = TMatrix::rotate_y(-deg*2); + + let rot1 = TMatrix::combine(vec!(rz, rx, t)); + let rot2 = TMatrix::combine(vec!(rz, ry, t)); + + let objects = vec!( (self.tetrahedron.transform(&rot1), 0xFFFF00) + , ( self.cube.transform(&rot2), 0x0000FF) ); + + self.clear(); + + match self.camera { + None => {}, + Some(camera) => { + for (o, color) in objects { + for (pg, c) in o.project(&camera, &self.light, color) { + (&pg).fill(&mut self, c); + } + } + }, + } + } + + pub fn image(&self) -> *const Color { + self.image.as_ptr() + } +} + +impl Canvas for View3d { + fn width(&self) -> u16 { + self.width + } + + fn height(&self) -> u16 { + self.height + } + + fn clear(&mut self) { + self.zbuf = vec!(0.0; self.size); + unsafe { + let ptr = self.image.as_mut_ptr(); + ptr::write_bytes(ptr, 0, self.size); + } + } + + fn set_pixel(&mut self, c :Coordinate, color :u32) { + let Coordinate(x, y, zr) = c; + let idx :usize = (y * (self.width as i32) + x) as usize; + + let r = ((color >> 16) & 0xFF) as u8; + let g = ((color >> 8) & 0xFF) as u8; + let b = ( color & 0xFF) as u8; + + if self.zbuf[idx] < zr { + self.zbuf[idx] = zr; + self.image[idx] = Color(r, g, b, 0xFF); + } + } + + // Empty implementations for now… mostly not needed because it is + // done from JavaScript… + fn init_events(&self) {} + fn start_events(&self, _ :mpsc::Sender) {} + fn draw( &mut self, _ :&dyn Drawable, _ :Coordinate, _ :u32 ) {} + fn put_text(&self, _ :Coordinate, _ :&str) {} + fn show(&self) {} +} + #[wasm_bindgen] #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/tutorial/wasm-game-of-life/src/transform.rs b/tutorial/wasm-game-of-life/src/transform.rs new file mode 100644 index 0000000..e7e3142 --- /dev/null +++ b/tutorial/wasm-game-of-life/src/transform.rs @@ -0,0 +1,186 @@ +// +// Transformation of vectors in a given coordinate system... +// +// Georg Hopp +// +// 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 . +// +use std::ops::{Add, Sub, Neg, Mul, Div}; +use std::fmt::Debug; + +use crate::Vector; +use crate::trigonometry::Trig; + +#[derive(Debug, Clone, Copy)] +pub struct TMatrix( (T, T, T, T) + , (T, T, T, T) + , (T, T, T, T) + , (T, T, T, T) ) + where T: Add + Sub + Neg + Mul + Div + Debug + Trig + From + Copy; + +pub trait Transformable +where T: Add + Sub + Neg + Mul + Div + Debug + Trig + From + Copy { + fn transform(&self, m :&TMatrix) -> Self; +} + +impl TMatrix +where T: Add + Sub + Neg + + Mul + Div + + Debug + Trig + From + Copy { + pub fn new( r1 :(T, T, T, T) + , r2 :(T, T, T, T) + , r3 :(T, T, T, T) + , r4 :(T, T, T, T) ) -> Self { + TMatrix(r1, r2, r3, r4) + } + + pub fn unit() -> Self { + Self::new( (1.into(), 0.into(), 0.into(), 0.into()) + , (0.into(), 1.into(), 0.into(), 0.into()) + , (0.into(), 0.into(), 1.into(), 0.into()) + , (0.into(), 0.into(), 0.into(), 1.into()) ) + } + + pub fn translate(v :Vector) -> Self { + let Vector(x, y, z) = v; + + Self::new( (1.into(), 0.into(), 0.into(), x) + , (0.into(), 1.into(), 0.into(), y) + , (0.into(), 0.into(), 1.into(), z) + , (0.into(), 0.into(), 0.into(), 1.into()) ) + } + + pub fn rotate_x(a :i32) -> Self { + let sin :T = Trig::sin(a); + let cos :T = Trig::cos(a); + + Self::new( (1.into(), 0.into(), 0.into(), 0.into()) + , (0.into(), cos , -sin , 0.into()) + , (0.into(), sin , cos , 0.into()) + , (0.into(), 0.into(), 0.into(), 1.into()) ) + } + + pub fn rotate_y(a :i32) -> Self { + let sin :T = Trig::sin(a); + let cos :T = Trig::cos(a); + + Self::new( (cos , 0.into(), sin , 0.into()) + , (0.into(), 1.into(), 0.into(), 0.into()) + , (-sin , 0.into(), cos , 0.into()) + , (0.into(), 0.into(), 0.into(), 1.into()) ) + } + + pub fn rotate_z(a :i32) -> Self { + let sin :T = Trig::sin(a); + let cos :T = Trig::cos(a); + + Self::new( (cos , -sin , 0.into(), 0.into()) + , (sin , cos , 0.into(), 0.into()) + , (0.into(), 0.into(), 1.into(), 0.into()) + , (0.into(), 0.into(), 0.into(), 1.into()) ) + } + + pub fn rotate_v(v :&Vector, a :i32) -> Self { + let Vector(x, y, z) = *v; + + let sin :T = Trig::sin(a); + let cos :T = Trig::cos(a); + + let zero :T = 0.into(); + let one :T = 1.into(); + + Self::new( ( (one - cos) * x * x + cos + , (one - cos) * x * y - sin * z + , (one - cos) * x * z + sin * y + , zero ) + , ( (one - cos) * x * y + sin * z + , (one - cos) * y * y + cos + , (one - cos) * y * z - sin * x + , zero ) + , ( (one - cos) * x * z - sin * y + , (one - cos) * y * z + sin * x + , (one - cos) * z * z + cos + , zero ) + , (0.into(), 0.into(), 0.into(), 1.into()) ) + } + + pub fn scale(v :Vector) -> Self { + let Vector(x, y, z) = v; + + Self::new( ( x, 0.into(), 0.into(), 0.into()) + , (0.into(), y, 0.into(), 0.into()) + , (0.into(), 0.into(), z, 0.into()) + , (0.into(), 0.into(), 0.into(), 1.into()) ) + } + + pub fn combine(mi :I) -> TMatrix + where I: IntoIterator> { + + mi.into_iter().fold(Self::unit(), |acc, x| x * acc) + } + + pub fn apply(&self, v :&Vector, w :T) -> (Vector, T) { + let TMatrix( (a11, a12, a13, a14) + , (a21, a22, a23, a24) + , (a31, a32, a33, a34) + , (a41, a42, a43, a44) ) = *self; + let Vector(x, y, z) = *v; + + let v = Vector( a11 * x + a12 * y + a13 * z + a14 * w + , a21 * x + a22 * y + a23 * z + a24 * w + , a31 * x + a32 * y + a33 * z + a34 * w ); + let w = a41 * x + a42 * y + a43 * z + a44 * w; + + //v.mul(&w.recip()) + (v, w) + } +} + +impl Mul for TMatrix +where T: Add + Sub + Neg + + Mul + Div + + Debug + Trig + From + Copy { + type Output = Self; + + // ATTENTION: This is not commutative, nor assoziative. + fn mul(self, other :Self) -> Self { + let TMatrix( (a11, a12, a13, a14) + , (a21, a22, a23, a24) + , (a31, a32, a33, a34) + , (a41, a42, a43, a44) ) = self; + let TMatrix( (b11, b12, b13, b14) + , (b21, b22, b23, b24) + , (b31, b32, b33, b34) + , (b41, b42, b43, b44) ) = other; + + TMatrix( ( a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41 + , a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42 + , a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43 + , a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44 ) + , ( a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41 + , a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42 + , a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43 + , a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44 ) + , ( a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41 + , a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42 + , a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43 + , a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44 ) + , ( a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41 + , a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42 + , a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43 + , a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44 ) ) + } +} diff --git a/tutorial/wasm-game-of-life/src/trigonometry.rs b/tutorial/wasm-game-of-life/src/trigonometry.rs new file mode 100644 index 0000000..e52bb8c --- /dev/null +++ b/tutorial/wasm-game-of-life/src/trigonometry.rs @@ -0,0 +1,143 @@ +// +// 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 +// +// 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 . +// +use std::ops::Div; +use std::ops::Neg; +use std::marker::Sized; +use crate::Error; + +pub trait Trig { + fn pi() -> Self; + fn recip(self) -> Self; + fn round(&self) -> i32; + fn sqrt(self) -> Result where Self: Sized; + fn sintab() -> Vec where Self: Sized; + fn tantab() -> Vec where Self: Sized; + + fn sin(d :i32) -> Self + where Self: Sized + Neg + 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 + 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 }) + }, + } + } + + fn cot(d :i32) -> Self + where Self: Sized + Copy + From + Div { + Into::::into(1) / Self::tan(d) + } +} + +impl Trig for f64 { + fn pi() -> Self { + std::f64::consts::PI + } + + fn recip(self) -> Self { + self.recip() + } + + fn round(&self) -> i32 { + f64::round(*self) as i32 + } + + fn sqrt(self) -> Result { + let x = self.sqrt(); + match x.is_nan() { + true => Err("sqrt on negative undefined"), + false => Ok(x), + } + } + + fn sintab() -> Vec { + lazy_static::lazy_static! { + static ref SINTAB :Vec = + (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 { + // 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 = + (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() + } +} diff --git a/tutorial/wasm-game-of-life/src/vector.rs b/tutorial/wasm-game-of-life/src/vector.rs new file mode 100644 index 0000000..28ad18c --- /dev/null +++ b/tutorial/wasm-game-of-life/src/vector.rs @@ -0,0 +1,139 @@ +// +// Stuff for manipulating 3 dimensional vectors. +// +// Georg Hopp +// +// 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 . +// +use std::fmt::{Debug, Display, Formatter, Result}; +use std::ops::{Add, Sub, Neg, Mul, Div}; + +use crate::trigonometry::Trig; +use crate::transform::{TMatrix, Transformable}; + +#[derive(Debug, Eq, Clone, Copy)] +pub struct Vector(pub T, pub T, pub T) + where T: Add + Sub + Neg + Mul + Div + Trig + Copy; + +impl Vector +where T: Add + Sub + Neg + + Mul + Div + Trig + Copy { + pub fn x(self) -> T { self.0 } + pub fn y(self) -> T { self.1 } + pub fn z(self) -> T { self.2 } + + pub fn mag(self) -> T { + let Vector(x, y, z) = self; + (x * x + y * y + z * z).sqrt().unwrap() + } + + pub fn mul(self, s :&T) -> Self { + let Vector(x, y, z) = self; + Vector(x * *s, y * *s, z * *s) + } + + pub fn dot(self, other :Self) -> T { + let Vector(x1, y1, z1) = self; + let Vector(x2, y2, z2) = other; + + x1 * x2 + y1 * y2 + z1 * z2 + } + + pub fn norm(self) -> Self { + // TODO This can result in 0 or inf Vectors… + // Maybe we need to handle zero and inf magnitude here… + self.mul(&self.mag().recip()) + } + + pub fn distance(self, other :Self) -> T { + (self - other).mag() + } +} + +impl Display for Vector +where T: Add + Sub + Neg + Mul + Div + Trig + Display + Copy { + fn fmt(&self, f :&mut Formatter<'_>) -> Result { + let Vector(x, y, z) = self; + write!(f, "({}, {}, {})", x, y, z) + } +} + +impl PartialEq for Vector +where T: Add + Sub + Neg + Mul + Div + Trig + PartialEq + Copy { + fn eq(&self, other :&Self) -> bool { + let Vector(x1, y1, z1) = self; + let Vector(x2, y2, z2) = other; + x1 == x2 && y1 == y2 && z1 == z2 + } +} + +impl Add for Vector +where T: Add + Sub + Neg + + Mul + Div + Trig + Copy { + type Output = Self; + + fn add(self, other :Self) -> Self { + let Vector(x1, y1, z1) = self; + let Vector(x2, y2, z2) = other; + Vector(x1 + x2, y1 + y2, z1 + z2) + } +} + +impl Sub for Vector +where T: Add + Sub + Neg + + Mul + Div + Trig + Copy { + type Output = Self; + + fn sub(self, other :Self) -> Self { + self + -other + } +} + +impl Neg for Vector +where T: Add + Sub + Neg + + Mul + Div + Trig + Copy { + type Output = Self; + + fn neg(self) -> Self { + let Vector(x, y, z) = self; + Self(-x, -y, -z) + } +} + +impl Mul for Vector +where T: Add + Sub + Neg + + Mul + Div + Trig + Copy { + type Output = Self; + + fn mul(self, other :Self) -> Self { + let Vector(ax, ay, az) = self; + let Vector(bx, by, bz) = other; + + Vector( ay * bz - az * by + , az * bx - ax * bz + , ax * by - ay * bx ) + } +} + +impl Transformable for Vector +where T: Add + Sub + Neg + + Mul + Div + + Trig + Copy + Debug + From { + fn transform(&self, m :&TMatrix) -> Self { + let (v, _) = m.apply(self, 0.into()); + v + } +} diff --git a/tutorial/wasm-game-of-life/www/index.html b/tutorial/wasm-game-of-life/www/index.html index ec89e14..0be0f4b 100644 --- a/tutorial/wasm-game-of-life/www/index.html +++ b/tutorial/wasm-game-of-life/www/index.html @@ -18,6 +18,7 @@ + diff --git a/tutorial/wasm-game-of-life/www/index.js b/tutorial/wasm-game-of-life/www/index.js index 6139ba9..ac7c97e 100644 --- a/tutorial/wasm-game-of-life/www/index.js +++ b/tutorial/wasm-game-of-life/www/index.js @@ -1,13 +1,28 @@ -import { Universe, Cell } from "wasm-game-of-life"; +import { Universe, Cell, View3d, Color } from "wasm-game-of-life"; import { memory } from "wasm-game-of-life/wasm_game_of_life_bg"; +// 3D canvas stuff +const view3d = View3d.new(151, 151); + +const view3d_canvas = document.getElementById("view3d"); +view3d_canvas.width = view3d.width(); +view3d_canvas.width = view3d.height(); + +const view3d_ctx = view3d_canvas.getContext('2d'); + +const view3d_renderLoop = () => { + view3d.update(); + requestAnimationFrame(view3d_renderLoop); +} + +// game of life stuff const CELL_SIZE = 5; // px const GRID_COLOR = "#CCCCCC"; const DEAD_COLOR = "#FFFFFF"; const ALIVE_COLOR = "#000000"; const universe = Universe.new(); -const width = universe.height(); +const width = universe.width(); const height = universe.height(); const canvas = document.getElementById("game-of-life-canvas"); @@ -74,6 +89,8 @@ const drawCells = () => { ctx.stroke(); }; +// start everything ... drawGrid(); drawCells(); requestAnimationFrame(renderLoop); +requestAnimationFrame(view3d_renderLoop);