///////////////////////////////
///
/// 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
{x: 0, y:1, z:0}, //A
{x: 1, y:-1, z:0}, //B
{x: -1, y:-1, z:0}, //C
{x: 0, y:-1, z:1}, //D
],
"f":[ // faces
[ 0, 1, 2 ],
[ 0, 3, 1 ],
[ 0, 2, 1 ],
[ 1, 2, 3 ]
],
"position":[
{x:0, y:0, z:0}
]
}
// Unless loaded otherwise, the ship missile is
// nothing but a line
let shipMissile3D = {
"v":[
{x: 0, y:0, z:-0.3}, //A
{x: 0, y:0, z:0}, //B
],
"f":[
[ 0, 1],
],
"position":[
{x:0, y:0, z:0}
]
}
///////////////////////////////
///
/// GAME OBJECTS
///
///////////////////////////////
const missile = class {
constructor(x, y, z) {
let missileInstance = clone(shipMissile3D);
this.v = missileInstance.v;
this.f = missileInstance.f
this.position = missileInstance.position;
this.position.x = x;
this.position.y = y;
this.position.z = z;
}
};
const star = class {
constructor(x, y, z) {
this.x = x;
this.y = y;
this.z = 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(0, 0, canvas.width, canvas.height);
}
// Moves the ship based on the input + turbulence effect
function calculate(){
///////////
/// SHIP POSITION & ROTATION
///////////
// Fake "wind turbulence" effect
let angleDeg = Math.atan2(center.y - mouse.y, center.x - 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 = 0; i < shipObject3D["v"].length; i++){
const point = shipObject3D["v"][i];
const rotated = rotatePoint(0, 0, point.x, point.y, pitch);
pitchedShip["v"][i] = {x:rotated.x, y:rotated.y, z:point.z};
}
// Copying every face
for (let i = 0; i < shipObject3D["f"].length; i++){
const faceSet = shipObject3D["f"][i];
pitchedShip["f"][i] = faceSet;
}
pitchedShip.position.x = mouse.x;
pitchedShip.position.y = mouse.y;
pitchedShip.position.z = 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 = 0; i < missilesList.length; i++){
let thisMissile = missilesList[i];
// We make each missile advance by a bit
thisMissile.position.z += missileSpeed/fps;
// Unless they're too far, we add them to draw
if (thisMissile.position.z < 1){
newList.push(thisMissile);
object3DList.push(thisMissile);
}
}
missilesList = newList;
///////////
/// STARS
///////////
for (let i = 0; i < starList.length; i++){
starList[i].z -= shipSpeed/fps;
if (starList[i].z < 0){
let nova = {x:0, y:0};
// Either they're rushing for the top/bottom, or for the left/right
if (Math.random() > 0.5){
nova.x = (Math.floor(Math.random()*2) * canvas.width);
nova.y = Math.random()*canvas.height;
}
else{
nova.x = Math.random()*canvas.width;
nova.y = (Math.floor(Math.random()*2) * canvas.height);
}
starList[i].x = nova.x;
starList[i].y = nova.y;
starList[i].z = 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 = 0; h < object3DList.length; h++){
const object3D = object3DList[h];
let object2D = [];
// Each vertex (.v) gets projected in a 2D space
for (let i = 0; i < object3D.v.length; i++){
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
x: fastLerp(object3D.position.x + thisVertix.x*scaleFactor, center.x, (thisVertix.z*scaleFactor)/depth + object3D.position.z),
y: fastLerp(object3D.position.y - thisVertix.y*scaleFactor, center.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 = 0; i < starList.length; i++){
thisStar = starList[i];
// Same projection calculation goes on for the stars, but a bit simpler (no need to worry for scale)
stars2D.push(
{
x: fastLerp(thisStar.x, center.x, (thisStar.z)),
y: fastLerp(thisStar.y, center.y, (thisStar.z)),
size: (thisStar.z)
}
)
}
}
function draw(){
// Drawing stars
for (let i = 0; i < stars2D.length; i++){
const thisPoint = stars2D[i];
thisPointSize = (1-thisPoint.size)*pointSize;
ctx.beginPath();
ctx.arc(
thisPoint.x,
thisPoint.y,
thisPointSize, 0, 2 * Math.PI, false);
ctx.fillStyle = drawStyle;
ctx.fill();
}
// Drawing every 3D object
for (let h = 0; h < object3DList.length; h++){
const object3D = object3DList[h];
for (let i = 0; i < object3D.f.length; i++){
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 = 0; j < thisFace.length; j++){
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 =
(j == thisFace.length-1 ?
vertices2D[h][thisFace[0]]
: vertices2D[h][thisFace[j+1]]
);
ctx.moveTo(firstPoint.x, firstPoint.y);
ctx.lineTo(secondPoint.x, secondPoint.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:0, y:0, z:0};
}
// Loads a model for the missiles
function setMissile(json_string){
shipMissile3D = JSON.parse(json_string);
shipMissile3D["position"] = {x:0, y:0, z:0};
}
// Resizes the canvas depending on the screen / page size
function readjust(){
let ow = [canvas.width, canvas.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 = 0; i < starAmount; i++){
let nova = {x:0, y:0};
if (Math.random() > 0.5){
nova.x = (Math.round(Math.random()) *canvas.width);
nova.y = Math.random()*canvas.height;
}
else{
nova.x = Math.random()*canvas.width;
nova.y = (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.x, mouse.y, mouse.x+20, mouse.y, -pitch);
missilesList.push(new missile(rotated.x, rotated.y, 0));
rotated = rotatePoint(mouse.x, mouse.y, mouse.x+13, mouse.y, -pitch);
missilesList.push(new missile(rotated.x, rotated.y, 0));
rotated = rotatePoint(mouse.x, mouse.y, mouse.x-13, mouse.y, -pitch);
missilesList.push(new missile(rotated.x, rotated.y, 0));
rotated = rotatePoint(mouse.x, mouse.y, mouse.x-20, mouse.y, -pitch);
missilesList.push(new missile(rotated.x, rotated.y, 0));
shootTimeout = setTimeout(shoot, shootCooldown*1000);
}
// The player stops shooting
function clearShoot(){
clearTimeout(shootTimeout);
}
// Changes pencil color
function setDrawStyle(str){
drawStyle = str;
}
// Calculate mouse position with scrolling
function updateMousePos(canvas, evt) {
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(cx, cy, x, y, angle) {
let radians = (Math.PI / 180) * angle,
cos = Math.cos(radians),
sin = Math.sin(radians),
nx = (cos * (x - cx)) + (sin * (y - cy)) + cx,
ny = (cos * (y - cy)) - (sin * (x - cx)) + cy;
return {x:nx, y:ny};
}
// Basic lerp implementation
function fastLerp(value1, value2, position){
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\ ///
/// / ` / ///
/// << | ///
/// / | ///
/// / | ///
/// / | ///
/// / \ \ / ///
/// ( ) | | ///
/// ________| _/_ | | ///
///<__________\______)\__) ///
/// ///
///////////////////////////////