///////////////////////////////
///
///        This demo uses a Lerp() function to
///        simulate a fake perspective using 
///        a single focal point. There is no
///        FOV but the Z position of the focal
///        point to adjust the perspective
///        effect.
///
///////////////////////////////

const canvas document.getElementById("pageCanvas");
const fps 60;

let ctx canvas.getContext("2d");
let mouse = {'x':0'y':0};
let center = {'x':canvas.width/2'y':canvas.height/2};

let vertices2D [];            // List of the 2D points that will have to be drawn
let object3DList [];            // List of 3D objects to translate and draw
let missilesList [];
let starList [];
let stars2D [];                // List of every 2D-translated star to draw


const missileSpeed 2.5;
const starAmount 100;
let shipSpeed 0.3;
let pointSize 3;            // Size of a single star
let scaleFactor 100;        // Used to make every model bigger
let depth 6*scaleFactor;     // depth is the maximum positive value Z can have. Increase this value decreases the fake-fov
let drawStyle "#ecf0f1";        // Pencil color 
let lineWidth 0.2;            // Thickness of the plane wires
let pitch 15;                    // Amount of "wind" pitching when the user is not moving the plane
let turbulenceReduction 800;    // Increasing this value reduces the "wind pitching" speed

const shootCooldown 8/fps;    // Increasing that value decreases the firerate
let shootTimeout null;

///////////////////////////////
///
///        MODELS 
///
///////////////////////////////

// Default polygon to use as a fallback in case
// no models is loaded
let shipObject3D = {
        "v":[    // vertices
            {x0y:1z:0},    //A
            {x1y:-1z:0},    //B
            {x: -1y:-1z:0},    //C
            {x0y:-1z:1},    //D
        ],
        
        "f":[    // faces
            01],
            [ 03],
            [ 02],
            [ 12]
        ],
        
        "position":[
            {x:0y:0z:0}
        ]
    }

// Unless loaded otherwise, the ship missile is
// nothing but a line
let shipMissile3D = {
        "v":[
            {x0y:0z:-0.3},    //A
            {x0y:0z:0},    //B
        ],
        
        "f":[
            [ 01],
        ],
        
        "position":[
            {x:0y:0z:0}
        ]
    }


///////////////////////////////
///
///        GAME OBJECTS
///
///////////////////////////////

const missile = class {
      constructor(xyz) {
              
            let missileInstance = clone(shipMissile3D);
            this.missileInstance.v;
            this.missileInstance.f
            this.position missileInstance.position;
            
            this.position.x;
            this.position.y;
            this.position.z;
            
          }
    };

const star = class {
      constructor(xyz) {
            this.x;
            this.y;
            this.z;
          }
    };


///////////////////////////////
///
///        HIGH LEVEL LOOP
///        This function will be called 
///        every 1/fps second
///
///////////////////////////////

function tick(){
        clear();        // Clears the canvas 
        readjust();        // Resizes the canvas based on window size 
        calculate();    // Updates 3D objects position and projects 3D objects to 2D layer 
        draw();            // Draws the 2D layer
    }


///////////////////////////////
///
///        LOOP ELEMENTS
///
///////////////////////////////

// Resets every drawing list
function clear(){
        stars2D [];
        vertices2D [];
        object3DList [];
        ctx.clearRect(00canvas.widthcanvas.height);
    }

// Moves the ship based on the input + turbulence effect
function calculate(){    
        
        ///////////
        ///    SHIP POSITION & ROTATION
        ///////////
        
        // Fake "wind turbulence" effect
        let angleDeg Math.atan2(center.mouse.ycenter.mouse.x) * 180 Math.PI;
        const wannaPitch angleDeg 90;
        const turbulence Math.cos(performance.now()/turbulenceReduction)*10;
        pitch fastLerp(pitch, (wannaPitch turbulence), 3/fps);
        
        // Rotating the ship is made by copying it entirely and rotating every of its points in the process
        let pitchedShip =  {
                'v':[],
                'f':[],
                'position':[]        
            }; 
        
        // Rotating every point
        for (let i 0shipObject3D["v"].lengthi++){
                const point shipObject3D["v"][i];
                const rotated rotatePoint(00point.xpoint.ypitch);
                pitchedShip["v"][i] = {x:rotated.xy:rotated.yz:point.z};
            }
        
        // Copying every face
        for (let i 0shipObject3D["f"].lengthi++){
                const faceSet shipObject3D["f"][i];
                pitchedShip["f"][i] = faceSet;
            }
        
        pitchedShip.position.mouse.x;
        pitchedShip.position.mouse.y;
        pitchedShip.position.0;    // The ship is always in the front (depth of 0)
        
        // Adding the ship to the queue of 3D objects to be drawn
        object3DList.push(pitchedShip); 
        
        ///////////
        /// MISSILES
        ///////////    
        let newList [];
        for (let i 0missilesList.lengthi++){
                let thisMissile missilesList[i];
                
                // We make each missile advance by a bit
                thisMissile.position.+= missileSpeed/fps;
                
                // Unless they're too far, we add them to draw
                if (thisMissile.position.1){
                        newList.push(thisMissile);
                        object3DList.push(thisMissile);
                    }
                
            }
        missilesList newList;
        
        ///////////
        ///    STARS
        ///////////
        for (let i 0starList.lengthi++){
                 starList[i].-= shipSpeed/fps;
                 
                 if (starList[i].0){
                        let nova = {x:0y:0};
                        // Either they're rushing for the top/bottom, or for the left/right
                        if (Math.random() > 0.5){
                                nova.= (Math.floor(Math.random()*2) * canvas.width);
                                nova.Math.random()*canvas.height;
                            }
                        else{
                                nova.Math.random()*canvas.width;
                                nova.= (Math.floor(Math.random()*2) * canvas.height);
                            }
                        starList[i].nova.x;
                        starList[i].nova.y;
                        starList[i].1;
                     }
                 
            }    
            
        ///////////
        ///    PROJECTION
        /// This should be the LAST STEP
        /// Once every 3D object updated,
        /// we can project their vertices
        /// on a 2D plane.
        ///////////
        
        for (let h 0object3DList.lengthh++){
                const object3D object3DList[h];
                let object2D [];
                
                // Each vertex (.v) gets projected in a 2D space
                for (let i 0object3D.v.lengthi++){
                        const thisVertix object3D.v[i];
                        object2D.push(
                            {    // The higher the Z value is, the nearer we bring the X and Y positions from the center of the screen using a Lerp
                                    xfastLerp(object3D.position.thisVertix.x*scaleFactorcenter.x, (thisVertix.z*scaleFactor)/depth object3D.position.z), 
                                    yfastLerp(object3D.position.thisVertix.y*scaleFactorcenter.y, (thisVertix.z*scaleFactor)/depth object3D.position.z)
                                    // This is a perfectly working way to simulate perspective [as long as we don't ever have to move the camera]
                                }
                        );
                    }
                
                // The projected object gets added to the queue of objects to draw 
                vertices2D.push(object2D);
            }
        for (let i 0starList.lengthi++){
                thisStar starList[i];
                // Same projection calculation goes on for the stars, but a bit simpler (no need to worry for scale)
                stars2D.push(
                    {
                        xfastLerp(thisStar.xcenter.x, (thisStar.z)), 
                        yfastLerp(thisStar.ycenter.y, (thisStar.z)),
                        size: (thisStar.z)
                        }
                )
            }
    }

function draw(){
        
        // Drawing stars
        for (let i 0stars2D.lengthi++){
                const thisPoint stars2D[i];
                thisPointSize = (1-thisPoint.size)*pointSize;
                
                ctx.beginPath();
                ctx.arc(
                    thisPoint.x,
                    thisPoint.y,
                    thisPointSize0Math.PIfalse);
                ctx.fillStyle drawStyle;
                ctx.fill();
            }
        
        // Drawing every 3D object
        for (let h 0object3DList.lengthh++){
                const object3D object3DList[h];
                
                for (let i 0object3D.f.lengthi++){
                        const thisFace object3D.f[i];
                        
                        // As we kept the index of the points intact during the projection, we can still
                        // connect the vertices of every face as they would have been connected in a 3D
                        // space
                        for (let j 0thisFace.lengthj++){
                            
                                ctx.beginPath();
                                ctx.strokeStyle drawStyle;
                                
                                // First point ever doesn't have to be connected to the previous one
                                const firstPoint vertices2D[h][thisFace[j]];
                                const secondPoint = 
                                    (== thisFace.length-? 
                                        vertices2D[h][thisFace[0]]
                                        : vertices2D[h][thisFace[j+1]]
                                    );
                                    
                                ctx.moveTo(firstPoint.xfirstPoint.y);
                                ctx.lineTo(secondPoint.xsecondPoint.y);
                                
                                ctx.lineWidth lineWidth;
                                ctx.stroke();
                            }
                    }
            }
    }



///////////////////////////////
///
///        GAME FUNCTIONS
///
///////////////////////////////

// Loads a model for the ship
function setObject(json_string){
        shipObject3D JSON.parse(json_string);
        shipObject3D["position"] = {x:0y:0z:0};
    }

// Loads a model for the missiles
function setMissile(json_string){
        shipMissile3D JSON.parse(json_string);
        shipMissile3D["position"] = {x:0y:0z:0};
    }

// Resizes the canvas depending on the screen / page size
function readjust(){
        let ow = [canvas.widthcanvas.height];
        canvas.width canvas.parentElement.clientWidth;
        canvas.height canvas.parentElement.clientHeight;
        center = {'x':canvas.width/2'y':canvas.height/2};
        if (ow[0] != canvas.width || ow[1] != canvas.height){
                starList [];
                for (let i 0starAmounti++){
                        let nova = {x:0y:0};
                        if (Math.random() > 0.5){
                                nova.= (Math.round(Math.random()) *canvas.width);
                                nova.Math.random()*canvas.height;
                            }
                        else{
                                nova.Math.random()*canvas.width;
                                nova.= (Math.round(Math.random()) *canvas.height);
                            }    
                        starList.push(new star(
                        nova.x,
                        nova.y,
                        Math.random()));
                    }
            }
    }

// Creates 4 missiles and adds them to the missile list, then triggers shoot timeout
function shoot(){
            
        let rotated rotatePoint(mouse.xmouse.ymouse.x+20mouse.y, -pitch);
        missilesList.push(new missile(rotated.xrotated.y0));
        
        rotated rotatePoint(mouse.xmouse.ymouse.x+13mouse.y, -pitch);
        missilesList.push(new missile(rotated.xrotated.y0));
        
        rotated rotatePoint(mouse.xmouse.ymouse.x-13mouse.y, -pitch);
        missilesList.push(new missile(rotated.xrotated.y0));
        
        rotated rotatePoint(mouse.xmouse.ymouse.x-20mouse.y, -pitch);
        missilesList.push(new missile(rotated.xrotated.y0));
        
        shootTimeout setTimeout(shootshootCooldown*1000);
    }

// The player stops shooting
function clearShoot(){
        clearTimeout(shootTimeout);
    }

// Changes pencil color 
function setDrawStyle(str){
        drawStyle str;
    }

// Calculate mouse position with scrolling
function updateMousePos(canvasevt) {
        const doc document.documentElement;
        const left = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
        const top = (window.pageYOffset || doc.scrollTop)  - (doc.clientTop || 0);
        const posX evt.clientX;
        const posY evt.clientY;
        let x posX canvas.offsetLeft left;
        let y posY canvas.offsetTop top;
        mouse = {"x":x"y":y};
    }


///////////////////////////////
///
///        UTILITY FUNCTIONS
///
///////////////////////////////

function rotatePoint(cxcyxyangle) {
        let radians = (Math.PI 180) * angle,
            cos Math.cos(radians),
            sin Math.sin(radians),
            nx = (cos * (cx)) + (sin * (cy)) + cx,
            ny = (cos * (cy)) - (sin * (cx)) + cy;
        return {x:nxy:ny};
    }

// Basic lerp implementation
function fastLerp(value1value2position){
        return (value1*(1-position) + value2*(position));
    }

// Utility function to clone objects
function clone(object){ 
        return JSON.parse(JSON.stringify(object));
    }


///////////////////////////////
///
///        GAME START
///
///////////////////////////////

let game setInterval(tick, (1/fps)*1000);


///////////////////////////////
///                         ///
///    Made by Rackover     ///
///   [Beerware License]    ///
///                         ///
///                   / V\  ///
///                  / `  / ///
///                 <<   |  ///
///                 /    |  ///
///               /      |  ///
///             /        |  ///
///           /    \  \ /   ///
///          (      ) | |   ///
///  ________|   _/_  | |   ///
///<__________\______)\__)  ///
///                         ///
///////////////////////////////