///////////////////////////////
///
///        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 120;

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

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 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 fps;    // Increasing that value decreases the firerate
let shootTimeout null;

let isMouseInside false;

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

// Default polygon to use as a fallback in case
// no models is loaded
let shipObject3D = {
        "v": [    // vertices
            x0y1z},    //A
            x1y: -1z},    //B
            x: -1y: -1z},    //C
            x0y: -1z},    //D
        ],
    
        "f": [    // faces
            [012],
            [031],
            [021],
            [123]
        ],
    
        "position": [
            { x0y0z}
        ]
    }

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


///////////////////////////////
///
///        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 
        refreshInput();
        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);
    }

function refreshInput() {
        if (!isMouseInside) {
                resetMouse(fps);
            }
    }

// 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), 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] = { xrotated.xyrotated.yzpoint.};
            }
    
        // 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 = { x0y};
                        // 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.scaleFactorcenter.x, (thisVertix.scaleFactor) / depth object3D.position.z),
                                    yfastLerp(object3D.position.thisVertix.scaleFactorcenter.y, (thisVertix.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)),
                            sizeMath.sqrt(thisStar.z)
                        }
                )
            }
    }

function draw() {
    
        // Drawing stars
        for (let i 0stars2D.lengthi++) {
                const thisPoint stars2D[i];
                thisPointSize = (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[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"] = { x0y0z};
    }

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

// 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 };
        if (ow[0] != canvas.width || ow[1] != canvas.height) {
                starList [];
                for (let i 0starAmounti++) {
                        let nova = { x0y};
                        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.20mouse.y, -pitch);
        missilesList.push(new missile(rotated.xrotated.y0));
    
        rotated rotatePoint(mouse.xmouse.ymouse.13mouse.y, -pitch);
        missilesList.push(new missile(rotated.xrotated.y0));
    
        rotated rotatePoint(mouse.xmouse.ymouse.13mouse.y, -pitch);
        missilesList.push(new missile(rotated.xrotated.y0));
    
        rotated rotatePoint(mouse.xmouse.ymouse.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 top 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"};
        isMouseInside true;
    }

function resetMouse(delta) {
        const targetX canvas.width Math.sin(new Date().getTime() * 0.0001) * (canvas.width 5);
        const targetY canvas.height 3;
        mouse.fastLerp(mouse.xtargetXdelta);
        mouse.fastLerp(mouse.ytargetYdelta);
    }

function mouseLeft() {
        isMouseInside false;
        clearShoot();
    }


///////////////////////////////
///
///        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 { xnxyny };
    }

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

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


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

resetMouse(1);
const game setInterval(tick, (fps) * 1000);


///////////////////////////////
///                         ///
/// Made by Louve Hurlante  ///
/// [GPL3 License]          ///
///                         ///
///                   / V\  ///
///                  / `  / ///
///                 <<   |  ///
///                 /    |  ///
///               /      |  ///
///             /        |  ///
///           /    \  \ /   ///
///          (      ) | |   ///
///  ________|   _/_  | |   ///
///<__________\______)\__)  ///
///                         ///
///////////////////////////////