///////////////////////////////
///
/// 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", { alpha: false });
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;
let isMouseInside = false;
///////////////////////////////
///
/// 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
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(0, 0, canvas.width, canvas.height);
}
function refreshInput() {
if (!isMouseInside) {
resetMouse(4 / fps);
}
}
// 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: Math.sqrt(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 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": y };
isMouseInside = true;
}
function resetMouse(delta) {
const targetX = canvas.width / 2 + Math.sin(new Date().getTime() * 0.0001) * (canvas.width / 5);
const targetY = canvas.height * 2 / 3;
mouse.x = fastLerp(mouse.x, targetX, delta);
mouse.y = fastLerp(mouse.y, targetY, delta);
}
function mouseLeft() {
isMouseInside = false;
clearShoot();
}
///////////////////////////////
///
/// 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
///
///////////////////////////////
resetMouse(1);
const game = setInterval(tick, (1 / fps) * 1000);
///////////////////////////////
/// ///
/// Made by Louve Hurlante ///
/// [GPL3 License] ///
/// ///
/// / V\ ///
/// / ` / ///
/// << | ///
/// / | ///
/// / | ///
/// / | ///
/// / \ \ / ///
/// ( ) | | ///
/// ________| _/_ | | ///
///<__________\______)\__) ///
/// ///
///////////////////////////////