I have been asked by a friend to add a high score table to a game where you maneuver a snake that gets larger as it eats food. My friend wanted me to add a high score to the game.
here is the original code;
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple Snake Game</title>
<!-- Basic styling, centering of the canvas. -->
<style>
canvas {
display: block;
position: absolute;
border: 1px solid #000;
margin: auto;
top: 0;
bottom: 0;
right: 0;
left: 0;
}
</style>
</head>
<body>
<script>
var
/**
* Constats
*/
COLS = 26,
ROWS = 26,
EMPTY = 0,
SNAKE = 1,
FRUIT = 2,
LEFT = 0,
UP = 1,
RIGHT = 2,
DOWN = 3,
KEY_LEFT = 37,
KEY_UP = 38,
KEY_RIGHT = 39,
KEY_DOWN = 40,
/**
* Game objects
*/
canvas, /* HTMLCanvas */
ctx, /* CanvasRenderingContext2d */
keystate, /* Object, used for keyboard inputs */
frames, /* number, used for animation */
score; /* number, keep track of the player score */
high_score = [];
/**
* Grid datastructor, usefull in games where the game world is
* confined in absolute sized chunks of data or information.
*
* #type {Object}
*/
grid = {
width: null, /* number, the number of columns */
height: null, /* number, the number of rows */
_grid: null, /* Array<any>, data representation */
/**
* Initiate and fill a c x r grid with the value of d
* #param {any} d default value to fill with
* #param {number} c number of columns
* #param {number} r number of rows
*/
init: function(d, c, r) {
this.width = c;
this.height = r;
this._grid = [];
for (var x=0; x < c; x++) {
this._grid.push([]);
for (var y=0; y < r; y++) {
this._grid[x].push(d);
}
}
},
/**
* Set the value of the grid cell at (x, y)
*
* #param {any} val what to set
* #param {number} x the x-coordinate
* #param {number} y the y-coordinate
*/
set: function(val, x, y) {
this._grid[x][y] = val;
},
/**
* Get the value of the cell at (x, y)
*
* #param {number} x the x-coordinate
* #param {number} y the y-coordinate
* #return {any} the value at the cell
*/
get: function(x, y) {
return this._grid[x][y];
}
}
/**
* The snake, works as a queue (FIFO, first in first out) of data
* with all the current positions in the grid with the snake id
*
* #type {Object}
*/
snake = {
direction: null, /* number, the direction */
last: null, /* Object, pointer to the last element in
the queue */
_queue: null, /* Array<number>, data representation*/
/**
* Clears the queue and sets the start position and direction
*
* #param {number} d start direction
* #param {number} x start x-coordinate
* #param {number} y start y-coordinate
*/
init: function(d, x, y) {
this.direction = d;
this._queue = [];
this.insert(x, y);
},
/**
* Adds an element to the queue
*
* #param {number} x x-coordinate
* #param {number} y y-coordinate
*/
insert: function(x, y) {
// unshift prepends an element to an array
this._queue.unshift({x:x, y:y});
this.last = this._queue[0];
},
/**
* Removes and returns the first element in the queue.
*
* #return {Object} the first element
*/
remove: function() {
// pop returns the last element of an array
return this._queue.pop();
}
};
/**
* Set a food id at a random free cell in the grid
*/
function setFood() {
var empty = [];
// iterate through the grid and find all empty cells
for (var x=0; x < grid.width; x++) {
for (var y=0; y < grid.height; y++) {
if (grid.get(x, y) === EMPTY) {
empty.push({x:x, y:y});
}
}
}
// chooses a random cell
var randpos = empty[Math.round(Math.random()*(empty.length - 1))];
grid.set(FRUIT, randpos.x, randpos.y);
}
/**
* Starts the game
*/
function main() {
// create and initiate the canvas element
canvas = document.createElement("canvas");
canvas.width = COLS*20;
canvas.height = ROWS*20;
ctx = canvas.getContext("2d");
// add the canvas element to the body of the document
document.body.appendChild(canvas);
// sets an base font for bigger score display
ctx.font = "12px Helvetica";
frames = 0;
keystate = {};
// keeps track of the keybourd input
document.addEventListener("keydown", function(evt) {
keystate[evt.keyCode] = true;
});
document.addEventListener("keyup", function(evt) {
delete keystate[evt.keyCode];
});
// intatiate game objects and starts the game loop
init();
loop();
}
/**
* Resets and inits game objects
*/
function init() {
score = 0;
grid.init(EMPTY, COLS, ROWS);
var sp = {x:Math.floor(COLS/2), y:ROWS-1};
snake.init(UP, sp.x, sp.y);
grid.set(SNAKE, sp.x, sp.y);
setFood();
}
/**
* The game loop function, used for game updates and rendering
*/
function loop() {
update();
draw();
// When ready to redraw the canvas call the loop function
// first. Runs about 60 frames a second
window.requestAnimationFrame(loop, canvas);
}
/**
* Updates the game logic
*/
function update() {
frames++;
// changing direction of the snake depending on which keys
// that are pressed
if (keystate[KEY_LEFT] && snake.direction !== RIGHT) {
snake.direction = LEFT;
}
if (keystate[KEY_UP] && snake.direction !== DOWN) {
snake.direction = UP;
}
if (keystate[KEY_RIGHT] && snake.direction !== LEFT) {
snake.direction = RIGHT;
}
if (keystate[KEY_DOWN] && snake.direction !== UP) {
snake.direction = DOWN;
}
// each five frames update the game state.
if (frames%5 === 0) {
// pop the last element from the snake queue i.e. the
// head
var nx = snake.last.x;
var ny = snake.last.y;
// updates the position depending on the snake direction
switch (snake.direction) {
case LEFT:
nx--;
break;
case UP:
ny--;
break;
case RIGHT:
nx++;
break;
case DOWN:
ny++;
break;
}
// checks all gameover conditions
if (0 > nx || nx > grid.width-1 ||
0 > ny || ny > grid.height-1 ||
grid.get(nx, ny) === SNAKE
) {
return init();
}
// check wheter the new position are on the fruit item
if (grid.get(nx, ny) === FRUIT) {
// increment the score and sets a new fruit position
score++;
setFood();
} else {
// take out the first item from the snake queue i.e
// the tail and remove id from grid
var tail = snake.remove();
grid.set(EMPTY, tail.x, tail.y);
}
// add a snake id at the new position and append it to
// the snake queue
grid.set(SNAKE, nx, ny);
snake.insert(nx, ny);
}
}
/**
* Render the grid to the canvas.
*/
function draw() {
// calculate tile-width and -height
var tw = canvas.width/grid.width;
var th = canvas.height/grid.height;
// iterate through the grid and draw all cells
for (var x=0; x < grid.width; x++) {
for (var y=0; y < grid.height; y++) {
// sets the fillstyle depending on the id of
// each cell
switch (grid.get(x, y)) {
case EMPTY:
ctx.fillStyle = "#fff";
break;
case SNAKE:
ctx.fillStyle = "#0ff";
break;
case FRUIT:
ctx.fillStyle = "#f00";
break;
}
ctx.fillRect(x*tw, y*th, tw, th);
}
}
// changes the fillstyle once more and draws the score
// message to the canvas
ctx.fillStyle = "#000";
ctx.fillText("SCORE: " + score, 10, canvas.height-10);
}
// start and run the game
main();
</script>
</body>
</html>
EDI: here is where I'm having problems
I added a high score variable here:
/**
* Game objects
high_scores = []; // new code */
canvas, /* HTMLCanvas */
ctx, /* CanvasRenderingContext2d */
keystate, /* Object, used for keyboard inputs */
frames, /* number, used for animation */
score, /* number, keep track of the player score */
As well as here:
} else {
// take out the first item from the snake queue i.e
// the tail and remove id from grid
var tail = snake.remove();
grid.set(EMPTY, tail.x, tail.y);
high_scores = high_scores.push(score)
}
i don't get how a couple of javascript changes make the layout disappear. I've never coded in javascript that much and I dont understand why javaScript changes are affecting the layout.
I found a solution. there was an error in my syntax here
/**
* Game objects
*/
canvas, /* HTMLCanvas */
ctx, /* CanvasRenderingContext2d */
keystate, /* Object, used for keyboard inputs */
frames, /* number, used for animation */
score, /* number, keep track of the player score */
high_scores = [];
Related
The bounty expires in 2 days. Answers to this question are eligible for a +500 reputation bounty.
handle wants to draw more attention to this question:
Locate and fix the error causing erratic movement, so the "vehicle" moves in a circle. Ideally, the main body of the vehicle causes drag and lift, so that it will begin to move straight and eventually stop if thrust and rudder angle are set to zero.
My attempt to implement/port parts of the rigid body 3D simulator (airplane) from "Physics for Game Developers, 2nd Edition" to Javascript and Three.js seems to be failing somewhere around the rotational part: while the vehicle (body and rudder) accelerates OK it does not properly move and turn:
At times (higher value for RHO) it appears to oscillate, resulting in "weird jerky" motion
The control surface lift then seems to toggle sign
While the calculations are 3D, there are currently no vertical forces (Z) like gravity/lift/bouyancy, so motion takes place on the X-Y plane.
It's likely that I've made mistakes porting the Vector/Matrix/Quaternion calculations or that the model parameters are way off (vLocalInertia vectors that determine the inertia tensor are a likely candidate) or the behavior is due to the simple Euler-type integration.
Of course it's also possible the original code (listings) is erroneous (submitted errata).
I'd like to be able to "drive" circles on the screen by setting the rudder and minimum thrust. Ideally, the vehicles body also causes drag and lift so it will tend to go straight and slow down if no external force (thrust) is applied.
body {
margin: 0px;
}
canvas {
display: block;
}
<div id="container"></div>
<!--script src="https://unpkg.com/es-module-shims#1.3.6/dist/es-module-shims.js"></script-->
<script type="importmap">
{ "imports": { "three": "https://unpkg.com/three/build/three.module.js", "three/addons/": "https://unpkg.com/three/examples/jsm/" } }
</script>
<script type="module">
import * as THREE from "three";
import { Euler, Matrix3, Vector3 } from "three";
/** degree to radian */
function rad(deg) {
return (deg / 180) * Math.PI;
}
/** radian to degree */
function deg(rad) {
return (rad / Math.PI) * 180;
}
/** */
function vstr(v) {
return `X${v.x.toFixed(3)}, Y${v.y.toFixed(3)}, Z${v.z.toFixed(3)}, `
}
/** */
class Vehicle {
constructor(scene) {
this.length = 0.3;
this.width = 0.15;
this.fMass = 0.0; // total mass
this.mInertia = new Matrix3(); // mass moment of inertia in body coordinates
this.mInertiaInverse = new Matrix3(); // inverse of mass moment of inertia
this.vPosition = new Vector3(0, 0, 0.001); // position in earth coordinates
this.vVelocity = new Vector3(); // velocity in earth coordinates
this.vVelocityBody = new Vector3(); // velocity in body coordinates
this.vAngularVelocity = new Vector3(); // angular velocity in body coordinates
this.vEulerAngles = new Vector3(0, 0, 0); // Euler angles in body coordinates
this.fSpeed = 0.0; // speed (magnitude of the velocity)
this.qOrientation = new THREE.Quaternion(); // orientation in earth coordinates
this.qOrientation.setFromEuler(new Euler().setFromVector3(this.vEulerAngles));
// console.log("vEulerAngles", this.vEulerAngles);
// console.log("qOrientation", this.qOrientation);
this.vForces = new Vector3(); // total force on body
this.vMoments = new Vector3(); // total moment (torque) on body
this.elements = [
{
type: "main",
fMass: 1.0,
vDCoords: new Vector3(0.0, 0.0, 0.0),
vCGCoords: null,
vLocalInertia: new Vector3(.1, .1, .1), // TODO:
rotates: false,
fIncidence: 0.0,
fDihedral: 90.0,
fArea: 0.5,
},
{
type: "rudder",
fMass: 0.01,
vDCoords: new Vector3(-.15, 0.0, 0.0),
vCGCoords: null,
vLocalInertia: new Vector3(.01, .01, .01), // TODO:
rotates: true,
fIncidence: 0.0,
fDihedral: 90.0,
fArea: 0.2,
},
];
// Calculate the vector normal (perpendicular) to each lifting surface.
// This is required when you are calculating the relative air velocity for lift and drag calculations.
for (let e of this.elements) {
let inc = rad(e.fIncidence);
let dih = rad(e.fDihedral);
e.vNormal = new Vector3(
Math.sin(inc), //
Math.cos(inc) * Math.sin(dih), //
Math.cos(inc) * Math.cos(dih) //
);
e.vNormal.normalize();
// console.log(e.vNormal);
}
// Calculate total mass
for (let e of this.elements) {
this.fMass += e.fMass;
}
console.log("this.fMass ", this.fMass);
// Calculate combined center of gravity location
let vMoment = new Vector3();
for (let e of this.elements) {
// vMoment += e.fMass * e.vDCoords;
vMoment.add(e.vDCoords.clone().multiplyScalar(e.fMass))
}
// CG = vMoment/mass;
let CG = vMoment.divideScalar(this.fMass);
console.log("CG", CG);
// Calculate coordinates of each element with respect to the combined CG
for (let e of this.elements) {
// e.vCGCoords = e.vDCoords - CG;
e.vCGCoords = e.vDCoords.clone().sub(CG);
console.log("e.vCGCoords", e.vCGCoords);
}
// (This inertia matrix (tensor) is in body coordinates)
let Ixx = 0;
let Iyy = 0;
let Izz = 0;
let Ixy = 0;
let Ixz = 0;
let Iyz = 0;
for (let e of this.elements) {
Ixx += e.vLocalInertia.x + e.fMass * (e.vCGCoords.y * e.vCGCoords.y + e.vCGCoords.z * e.vCGCoords.z);
Iyy += e.vLocalInertia.y + e.fMass * (e.vCGCoords.z * e.vCGCoords.z + e.vCGCoords.x * e.vCGCoords.x);
Izz += e.vLocalInertia.z + e.fMass * (e.vCGCoords.x * e.vCGCoords.x + e.vCGCoords.y * e.vCGCoords.y);
Ixy += e.fMass * (e.vCGCoords.x * e.vCGCoords.y);
Ixz += e.fMass * (e.vCGCoords.x * e.vCGCoords.z);
Iyz += e.fMass * (e.vCGCoords.y * e.vCGCoords.z);
}
// mass, inertia matrix and the inverse
// this.fMass = mass;
this.mInertia.e11 = Ixx;
this.mInertia.e12 = -Ixy;
this.mInertia.e13 = -Ixz;
this.mInertia.e21 = -Ixy;
this.mInertia.e22 = Iyy;
this.mInertia.e23 = -Iyz;
this.mInertia.e31 = -Ixz;
this.mInertia.e32 = -Iyz;
this.mInertia.e33 = Izz;
// console.log(this.mInertia);
console.log(this.mInertia.e11, this.mInertia.e12, this.mInertia.e13,);
console.log(this.mInertia.e21, this.mInertia.e22, this.mInertia.e23,);
console.log(this.mInertia.e31, this.mInertia.e32, this.mInertia.e33,);
this.mInertiaInverse = this.mInertia.clone().invert();
// angular
this.angle = 0; // [rad]
// [deg]
this.rudder = 8;
this.thrust = 0.1;
this.object = new THREE.Group();
this.object.position.copy(this.vPosition);
this.object.rotation.z = this.angle;
const axes = new THREE.AxesHelper(0.1);
this.object.add(axes);
// add self to scene
scene.add(this.object);
}
physics(dt) {
let Fb = new Vector3();
let Mb = new Vector3();
// reset forces and moments:
this.vForces.x = 0.0;
this.vForces.y = 0.0;
this.vForces.z = 0.0;
this.vMoments.x = 0.0;
this.vMoments.y = 0.0;
this.vMoments.z = 0.0;
this.elements[1].fIncidence = this.rudder;
// Define the thrust vector, which acts through the plane's CG
let ThrustForce = this.thrust;
let Thrust = new Vector3(1, 0, 0).multiplyScalar(ThrustForce);
// Calculate forces and moments in body space:
let vLocalVelocity = new Vector3();
let fLocalSpeed = 0;
let vDragVector = new Vector3();
let vLiftVector = new Vector3();
let fAttackAngle = 0;
let tmp = 0.0;
let vResultant = new Vector3();
// let i = 0;
let vtmp = new Vector3();
// Given the attack angle, this function returns the proper lift coefficient for a symmetric (no camber) airfoil without flaps.
function RudderLiftCoefficient(angle) {
let clf0 = [0.16, 0.456, 0.736, 0.968, 1.144, 1.12, 0.8];
let a = [0.0, 4.0, 8.0, 12.0, 16.0, 20.0, 24.0];
let cl = 0;
let aa = Math.abs(angle);
for (let i = 0; i < 8; i++) {
if ((a[i] <= aa) && (a[i + 1] > aa)) {
cl = clf0[i] - (a[i] - aa) * (clf0[i] - clf0[i + 1]) / (a[i] - a[i + 1]);
if (angle < 0) cl = -cl;
break;
}
}
return cl;
}
// Given the attack angle, this function returns the proper drag coefficient for a symmetric (no camber) airfoil without flaps.
function RudderDragCoefficient(angle) {
let cdf0 = [0.0032, 0.0072, 0.0104, 0.0184, 0.04, 0.096, 0.168];
let a = [0.0, 4.0, 8.0, 12.0, 16.0, 20.0, 24.0];
let cd = 0.5;
let aa = Math.abs(angle);
for (let i = 0; i < 8; i++) {
if ((a[i] <= aa) && (a[i + 1] > aa)) {
cd = cdf0[i] - (a[i] - aa) * (cdf0[i] - cdf0[i + 1]) / (a[i] - a[i + 1]);
break;
}
}
return cd;
}
// let Stalling = false;
// loop through the seven lifting elements skipping the fuselage
for (let e of this.elements) {
if (e.rotates) // The tail/rudder is a special case since it can rotate;
{
// thus, you have to recalculate the normal vector.
let inc, dih;
inc = rad(e.fIncidence); // incidence angle
dih = rad(e.fDihedral); // dihedral angle
e.vNormal = new Vector3(
Math.sin(inc), // x?
(Math.cos(inc) * Math.sin(dih)), // y?
(Math.cos(inc) * Math.cos(dih)) // z?
);
e.vNormal.normalize();
// console.log("e.vNormal", e.vNormal);
}
// Calculate local velocity at element
// The local velocity includes the velocity due to linear motion of the airplane,
// plus the velocity at each element due to the rotation of the airplane.
// Here's the rotational part
// vtmp = this.vAngularVelocity ^ e.vCGCoords;
vtmp.crossVectors(this.vAngularVelocity, e.vCGCoords);
// vLocalVelocity = this.vVelocityBody + vtmp;
vLocalVelocity.addVectors(this.vVelocityBody, vtmp);
// Calculate local air speed
fLocalSpeed = vLocalVelocity.length();
// Find the direction in which drag will act.
// Drag always acts inline with the relative velocity but in the opposing direction
const dragthreshold = 0;//.1; // 1.0 // TODO: magic number
if (fLocalSpeed > dragthreshold) {
// vDragVector = -vLocalVelocity / fLocalSpeed;
vDragVector = vLocalVelocity.clone().divideScalar(-fLocalSpeed);
//console.log("vDragVector",vDragVector);
}
// else{
// console.log("no drag");
// }
// Find the direction in which lift will act.
// Lift is always perpendicular to the drag vector
// vLiftVector = (vDragVector ^ e.vNormal) ^ vDragVector;
vLiftVector = new THREE.Vector3().crossVectors(new THREE.Vector3().crossVectors(vDragVector, e.vNormal), vDragVector);
// tmp = vLiftVector.length(); // superfluous line in book
vLiftVector.normalize();
// debugger;
// console.log("drag ", vstr(vDragVector)); // has speed threshold
// console.log("normal", vstr(e.vNormal));
// console.log("lift ", vstr(vLiftVector)); // sign toggles near zero?
// seems ok, drag -x, lift +y
// Find the angle of attack.
// The attack angle is the angle between the lift vector and the element normal vector.
// Note, the sine of the attack angle is equal to the cosine of the angle between the drag vector and the normal vector.
// tmp = vDragVector * e.vNormal;
tmp = vDragVector.dot(e.vNormal);
if (tmp > 1) tmp = 1;
if (tmp < -1) tmp = -1;
fAttackAngle = deg(Math.asin(tmp));
// console.log("fAttackAngle", fAttackAngle); // toggles sign like vLift
// Determine the resultant force (lift and drag) on the element.
// const RHO = 1.225; // kg/m^3 for AIR
const RHO = 1225; // kg/m^3
tmp = 0.5 * RHO * fLocalSpeed ** 2 * e.fArea;
if (e.type == "rudder") // Tail/rudder
{
// vResultant = (vLiftVector * RudderLiftCoefficient(fAttackAngle) + vDragVector * RudderDragCoefficient(fAttackAngle)) * tmp;
const lc = RudderLiftCoefficient(fAttackAngle);
const dc = RudderDragCoefficient(fAttackAngle);
const lift = vLiftVector.multiplyScalar(lc)
const drag = vDragVector.multiplyScalar(dc);
const liftanddrag = lift.add(drag);
vResultant = liftanddrag.multiplyScalar(tmp);
//vResultant.set(-0.0, -0.1, 0);
console.log("vResultant", vstr(vResultant));
}
// Keep a running total of these resultant forces (total force)
// Fb += vResultant;
Fb.add(vResultant);
// Calculate the moment about the CG of this element's force
// and keep a running total of these moments (total moment)
// vtmp = e.vCGCoords ^ vResultant;
vtmp.crossVectors(e.vCGCoords, vResultant);
// console.log("e.vCGCoords", vstr(e.vCGCoords));
// console.log("moment", vstr(vtmp));
// Mb += vtmp;
Mb.add(vtmp);
// debugger;
} // elements
// console.log("Mb", vstr(Mb));
// Now add the thrust
// Fb += Thrust;
Fb.add(Thrust);
// Convert forces from model space to earth space
// this.vForces = QVRotate(this.qOrientation, Fb);
this.vForces = Fb.clone().applyQuaternion(this.qOrientation);
// console.log(this.vForces);
// gravity
// const g = -9.81;
// this.vForces.z += g * this.fMass;
// moments
// this.vMoments += Mb;
this.vMoments.add(Mb);
// console.log("vMoments", vstr(this.vMoments));
// INTEGRATOR (EULER)
// calculate the acceleration of the airplane in earth space:
// Ae = this.vForces / this.fMass;
let vAe = this.vForces.clone().divideScalar(this.fMass);
// console.log(Ae);
// calculate the velocity of the airplane in earth space:
// this.vVelocity += Ae * dt;
vAe.multiplyScalar(dt);
// console.log(Ae);
this.vVelocity.add(vAe);
// console.log(this.vVelocity);
// calculate the position of the airplane in earth space:
// this.vPosition += this.vVelocity * dt;
this.vPosition.add(this.vVelocity.clone().multiplyScalar(dt));
// handle the rotations:
// calculate the angular velocity in body space:
console.log("this.vAngularVelocity", this.vAngularVelocity);
// this.vAngularVelocity += this.mInertiaInverse * (this.vMoments - (this.vAngularVelocity ^ (this.mInertia * this.vAngularVelocity))) * dt;
let mv = this.vAngularVelocity.clone().applyMatrix3(this.mInertia);
let cp = new Vector3().crossVectors(this.vAngularVelocity, mv);
let sub = this.vMoments.clone().sub(cp);
sub.multiplyScalar(dt);
this.vAngularVelocity.add(sub.applyMatrix3(this.mInertiaInverse));
console.log("this.vAngularVelocity", this.vAngularVelocity);
// calculate the new rotation quaternion:
// this.qOrientation += (this.qOrientation * this.vAngularVelocity) * (0.5 * dt);
// Quaternion * Vector
let q = this.qOrientation.clone();
let v = this.vAngularVelocity.clone();
let vq = new THREE.Quaternion(v.x, v.y, v.z, 0);
let qv = q.clone().multiply(vq);
// Q * Scalar
const scalar = 0.5 * dt;
qv.x *= scalar;
qv.y *= scalar;
qv.z *= scalar;
qv.w *= scalar;
// Q+Q
this.qOrientation.x += qv.x;
this.qOrientation.y += qv.y;
this.qOrientation.z += qv.z;
this.qOrientation.w += qv.w;
// console.log("qO",this.qOrientation);
// normalize the orientation quaternion:
// let mag = 0.0;
// mag = this.qOrientation.Magnitude();
// if (mag != 0)
// this.qOrientation /= mag;
this.qOrientation.normalize();
// console.log(this.qOrientation);
// calculate the velocity in body space:
// (we'll need this to calculate lift and drag forces)
// this.vVelocityBody = QVRotate(~this.qOrientation, this.vVelocity);
this.vVelocityBody = this.vVelocity.clone().applyQuaternion(this.qOrientation.clone().conjugate());
// calculate the air speed:
// this.fSpeed = this.vVelocity.Magnitude();
this.fSpeed = this.vVelocity.length();
// get the Euler angles for our information
// u = MakeEulerAnglesFromQ(this.qOrientation);
let u = new Vector3().setFromEuler(new Euler().setFromQuaternion(this.qOrientation));
this.vEulerAngles.x = u.x; // roll
this.vEulerAngles.y = u.y; // pitch
this.vEulerAngles.z = u.z; // yaw
// threedee
this.object.position.copy(this.vPosition);
this.object.rotation.z = this.vEulerAngles.z;
} // physics
} // Vehicle
let renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
let canvas = renderer.domElement;
document.body.appendChild(canvas);
let scene = new THREE.Scene();
scene.background = new THREE.Color("gray");
const axes = new THREE.AxesHelper();
scene.add(axes);
const grid = new THREE.GridHelper(20, 20);
grid.rotation.x = Math.PI / 2; // Z is up
scene.add(grid);
let camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 50);
camera.up.set(0, 0, 1); // Z is up
camera.position.set(-0, -0, 3);
let craft = new Vehicle(scene);
// simulation
let timeinitial = performance.now();
const timestart = timeinitial;
function step(time) {
let timedelta = (time - timeinitial) / 1000; // [s]
timeinitial = time;
// simulate
craft.physics(timedelta);
renderer.render(scene, camera);
if(time - timestart < 5*1000){
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
window.addEventListener("resize", e => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
EDIT: removed controls, added constant thrust and rudder angle.
EDIT: simplified code for vResultant - the issue seems to originate in the calculation of lift...
I'm looking for a very basic implementation of the Z-buffer, ideally in JS. I am trying to take a look at a very simple code, for example two polygons overlapping, one hiding the other.
I can't find such basic example, while I can find a couple of "well-above my current level and understanding" samples.
Is there a resource you could recommend to get started?
Thank you for your help and recommendations!
A Z-Buffer(also known as depth buffer) is nothing more than a 2D pixel array(think image). Instead of RGB it only stores a single value in each pixel, the distance from the current viewpoint:
// one value for each pixel in our screen
const depthBuffer = new Array(screenWidth * screenHeight);
It augments the color buffer that contains the actual image you present to the user:
// create buffer for color output
const numChannels = 3; // R G B
const colorBuffer = new Array(screenWidth * screenHeight * numChannels);
For every pixel of a shape you draw you check the Z-Buffer to see if there's anything closer to the camera that occludes the current pixel, if so you don't draw it. This way you can draw things in any order and they're still properly occluded on a per pixel level.
Z-Buffering may not only be used in 3D but also in 2D to achieve draw order-independence. Lets say we want to draw a few boxes, this will be our box class:
class Box {
/** #member {Object} position of the box storing x,y,z coordinates */
position;
/** #member {Object} size of the box storing width and height */
size;
/** #member {Object} color of the box given in RGB */
color;
constructor (props) {
this.position = props.position;
this.size = props.size;
this.color = props.color;
}
/**
* Check if given point is in box
* #param {Number} px coordinate of the point
* #param {Number} py coordinate of the point
* #return {Boolean} point in box
*/
pointInBox (px,py) {
return this.position.x < px && this.position.x + this.size.width > px
&& this.position.y < py && this.position.y + this.size.height > py;
}
}
With this class we can now create a few boxes and draw them:
const boxes = [
new Box({
position: { x: 50, y: 50, z: 10 },
size: { width: 50, height: 20 },
color: { r: 255, g: 0, b:0 }
}),
// green box
new Box({
position: { x: 80, y: 30, z: 5 },
size: { width: 10, height: 50 },
color: { r: 0, g: 255, b:0 }
}),
// blue
new Box({
position: { x: 60, y: 55, z: 8 },
size: { width: 50, height: 10 },
color: { r: 0, g: 0, b: 255 }
})
];
With our shapes specified we can now draw them:
for(const box of boxes) {
for(let x = 0; x < screenWidth; x++) {
for(let y = 0; y < screenHeight; y++) {
// check if our pixel is within the box
if (box.pointInBox(x,y)) {
// check if this pixel of our box is covered by something else
// compare depth value in depthbuffer against box position
// this is commonly referred to as "depth-test"
if (depthBuffer[x + y * screenWidth] < box.position.z) {
// something is already closer to the viewpoint than our current primitive, don't draw this pixel:
continue;
}
// we passed the depth test, put our current depth value in the z-buffer
depthBuffer[x + y * screenWidth] = box.position.z;
// put the color in the color buffer, channel by channel
colorBuffer[(x + y * screenWidth)*numChannels + 0] = box.color.r;
colorBuffer[(x + y * screenWidth)*numChannels + 1] = box.color.g;
colorBuffer[(x + y * screenWidth)*numChannels + 2] = box.color.b;
}
}
}
}
Note that this code is exemplary so it's overly verbose and inefficient for the sake of laying out the concept.
const ctx = document.getElementById("output").getContext('2d');
const screenWidth = 200;
const screenHeight = 200;
// one value for each pixel in our screen
const depthBuffer = new Array(screenWidth * screenHeight);
// create buffer for color output
const numChannels = 3; // R G B
const colorBuffer = new Array(screenWidth * screenHeight * numChannels);
/**
* Represents a 2D box
* #class
*/
class Box {
/** #member {Object} position of the box storing x,y,z coordinates */
position;
/** #member {Object} size of the box storing width and height */
size;
/** #member {Object} color of the box given in RGB */
color;
constructor (props) {
this.position = props.position;
this.size = props.size;
this.color = props.color;
}
/**
* Check if given point is in box
* #param {Number} px coordinate of the point
* #param {Number} py coordinate of the point
* #return {Boolean} point in box
*/
pointInBox (px,py) {
return this.position.x < px && this.position.x + this.size.width > px
&& this.position.y < py && this.position.y + this.size.height > py;
}
}
const boxes = [
// red box
new Box({
position: { x: 50, y: 50, z: 10 },
size: { width: 150, height: 50 },
color: { r: 255, g: 0, b:0 }
}),
// green box
new Box({
position: { x: 80, y: 30, z: 5 },
size: { width: 10, height: 150 },
color: { r: 0, g: 255, b:0 }
}),
// blue
new Box({
position: { x: 70, y: 70, z: 8 },
size: { width: 50, height: 40 },
color: { r: 0, g: 0, b: 255 }
})
];
const varyZ = document.getElementById('varyz');
varyZ.onchange = draw;
function draw () {
// clear depth buffer of previous frame
depthBuffer.fill(10);
for(const box of boxes) {
for(let x = 0; x < screenWidth; x++) {
for(let y = 0; y < screenHeight; y++) {
// check if our pixel is within the box
if (box.pointInBox(x,y)) {
// check if this pixel of our box is covered by something else
// compare depth value in depthbuffer against box position
if (depthBuffer[x + y * screenWidth] < box.position.z) {
// something is already closer to the viewpoint that our current primitive, don't draw this pixel:
if (!varyZ.checked) continue;
if (depthBuffer[x + y * screenWidth] < box.position.z + Math.sin((x+y))*Math.cos(x)*5) continue;
}
// we passed the depth test, put our current depth value in the z-buffer
depthBuffer[x + y * screenWidth] = box.position.z;
// put the color in the color buffer, channel by channel
colorBuffer[(x + y * screenWidth)*numChannels + 0] = box.color.r;
colorBuffer[(x + y * screenWidth)*numChannels + 1] = box.color.g;
colorBuffer[(x + y * screenWidth)*numChannels + 2] = box.color.b;
}
}
}
}
// convert to rgba for presentation
const oBuffer = new Uint8ClampedArray(screenWidth*screenHeight*4);
for (let i=0,o=0; i < colorBuffer.length; i+=3,o+=4) {
oBuffer[o]=colorBuffer[i];
oBuffer[o+1]=colorBuffer[i+1];
oBuffer[o+2]=colorBuffer[i+2];
oBuffer[o+3]=255;
}
ctx.putImageData(new ImageData(oBuffer, screenWidth, screenHeight),0,0);
}
document.getElementById('redz').oninput = e=>{boxes[0].position.z=parseInt(e.target.value,10);draw()};
document.getElementById('greenz').oninput = e=>{boxes[1].position.z=parseInt(e.target.value,10);draw()};
document.getElementById('bluez').oninput = e=>{boxes[2].position.z=parseInt(e.target.value,10);draw()};
draw();
canvas {
border:1px solid black;
float:left;
margin-right: 2rem;
}
label {display:block;}
label span {
display:inline-block;
width: 100px;
}
<canvas width="200" height="200" id="output"></canvas>
<label><span>Red Z</span>
<input type="range" min="0" max="10" value="10" id="redz"/>
</label>
<label><span>Green Z</span>
<input type="range" min="0" max="10" value="5" id="greenz"/>
</label>
<label><span>Blue Z</span>
<input type="range" min="0" max="10" value="8" id="bluez"/>
</label>
<label><span>Vary Z Per Pixel</span>
<input type="checkbox" id="varyz"/>
</label>
I tried to smoothly move div(circle), but I can not do it. Div immediately moves to the last point.
I tried to simulate the process of the ball falling.
I used method animate with second param 0, but this did not help me.
How to do it?
"use strict";
function calculateH(h, t) {
return h - ((Math.pow(t, 2)*9.8)/2);
}
/**
* [getTrip function is calculate Y coordinates for the ball thrown down]
* #param {[number]} h [The height from which the ball falls]
* #param {[number]} t [Time elapsed from the beginning of the fall of the ball]
* #param {[number]} interval [EPS]
* #param {[number]} k [Ratio of height to screen height. It is necessary that the ball fell to the site bottom]
* #return {[array]} [Array of Y coordinates. {0, 0.2, 1.2 ... h}]
*/
function getTrip(h, t, interval, k) {
var calculations = new Array();
for(t; calculateH(h, t) > 0; t += interval)
calculations.push((h - calculateH(h, t))*k);
return calculations;
}
$('document').ready(function() {
var bol = $('#mycircle');
var h = 100;
var t = 0;
var interval = 0.001; // eps
/**
* [k is the ratio of height of the screen to start the ball drop height]
* #type {[number]}
*/
var k = ($(document).height()-bol.height()) / h;
var calculations = getTrip(h, t, interval, k);
// Problem is there.
// I want animate of fell ball, but this code just move in last Y coord.
calculations.forEach(function(y) {
bol.css({'margin-top': y+'px'});
});
bol.animate({'margin-top': h*k+'px'}, 1); // prees to the bottom
});
https://jsfiddle.net/82agzc2e/4/
Why are you using a loop, and not animate the margin-top directly to last position?
bol.animate({'margin-top': calculations[calculations.length - 1]+'px'}, 1000);
Working example.
Repro steps:
Visit http://playclassicsnake.com/play
See 3 green squares and 1 red circle
Resize screen small enough to make the grid that the snake lives on be smaller
The red circle will be resized and replaced appropriately, and so will 1 green square, seemingly
console.log(SG.snake.links.length); to see that there are indeed 3 links on the snake. If you look at the grid coordinates of each of the three individually with SG.snake.links[k].pos.x, SG.snake.links[k].pos.y for k=1,2,3 you will see that they live on the same coordinate.
Source of the problem:
The source has to be my implementation of the function that handles the resizing of the board and the elements on the board. It is
this.rescale = function ( newWidth )
{
// newWidth: new width of the div containing
this.board.setSize(newWidth, newWidth); // set the size of the board to be that of the containing div
var blockWidth = this.board.getSize().width / this.numBlocks; // width in pixels of a block on the board
this.food.elem.remove(); // remove old food element
this.food.elem = this.board.circle(this.food.pos.x * blockWidth + blockWidth / 2, this.food.pos.y * blockWidth + blockWidth / 2, blockWidth / 2).attr('fill', '#cf6a4c'); // create food element to replace old one, in same grid location (see http://raphaeljs.com/reference.html#Paper.circle)
for (var i in this.snake.links)
{
var thisLink = this.snake.links[i];
thisLink.elem.remove(); // remove old link element
thisLink.elem = this.board.rect(thisLink.pos.x * blockWidth, thisLink.pos.y * blockWidth, blockWidth, blockWidth).attr('fill', '#19FF19'); // creata new link to replace old one http://raphaeljs.com/reference.html#Paper.circle
}
}
Code dump:
If you want to see the full logic of the object representing the game, it is below.
// define game object
function Game ( board, numBlocks ) {
// board: Raphael object that the snake will live on
// numBlocks: Number of blocks both horizontally AND vertically -- the grid structure should be squares
this.board = board;
this.numBlocks = numBlocks;
this.snake; // Snake object on the board
this.coords = []; // map whose key-value pairs represent whether a coordinate is open or taken
this.food = null; // food element on board
this.getCoords = function ( )
{
// returns a nested list gridList of all grid coordinates on the canvas,
// acting like a map so that gridList[i,j]=true if the coordinate i,j is
// occupied, and gridList[i,j]=false if the coordinate is not occupied
var gridList = [];
for (var i = 0; i < this.numBlocks; ++i)
{
var innerList = [];
for (var j = 0; j < this.numBlocks; ++j) innerList.push(true);
gridList.push(innerList);
}
return gridList;
}
this.elementOnGrid = function (elem, xpos, ypos) {
// elem: Rapael element (see: http://raphaeljs.com/reference.html#Element)
// xpos, ypos: x and y grid coordinates of the current position of the element
return { elem: elem, pos: { x: xpos, y: ypos } };
}
this.rescale = function ( newWidth )
{
// newWidth: new width of the div containing
this.board.setSize(newWidth, newWidth); // set the size of the board to be that of the containing div
var blockWidth = this.board.getSize().width / this.numBlocks; // width in pixels of a block on the board
this.food.elem.remove(); // remove old food element
this.food.elem = this.board.circle(this.food.pos.x * blockWidth + blockWidth / 2, this.food.pos.y * blockWidth + blockWidth / 2, blockWidth / 2).attr('fill', '#cf6a4c'); // create food element to replace old one, in same grid location (see http://raphaeljs.com/reference.html#Paper.circle)
for (var i in this.snake.links)
{
var thisLink = this.snake.links[i];
thisLink.elem.remove(); // remove old link element
thisLink.elem = this.board.rect(thisLink.pos.x * blockWidth, thisLink.pos.y * blockWidth, blockWidth, blockWidth).attr('fill', '#19FF19'); // creata new link to replace old one http://raphaeljs.com/reference.html#Paper.circle
}
}
this.Snake = function ( game )
{
// game: the Game function/object containing this function
this.links; // list of
this.createNew = function ( )
{
this.links = [];
var blockWidth = game.board.getSize().width / game.numBlocks; // width in pixels of a block on the board
var centerCoordXY = Math.round(game.numBlocks / 2); // x-y grid coordinate of center
for (var i = 0; i < 3; ++i) // start with 3 blocks in the center-ish
{
var newX = centerCoordXY + i;
this.links.push(new game.elementOnGrid(
game.board.rect(newX * blockWidth, centerCoordXY * blockWidth, blockWidth, blockWidth).attr('fill', '#19FF19'), // http://raphaeljs.com/reference.html#Paper.circle
centerCoordXY,
centerCoordXY
) // add element of type elementOnGrid to the links
);
game.coords[newX][centerCoordXY] = false; // indicate that coordinates of element just added to snake is no longer open
}
}
}
this.placeFood = function ( )
{
do {
var randXCoord = randInt(0, this.coords.length), randYCoord = randInt(0, this.coords.length);
}
while (this.coords[randXCoord][randYCoord] === false); // get random unused x-y coordinate
var blockWidth = this.board.getSize().width / this.numBlocks; // width in pixels of a block on the board
if (this.food == null) // if food element hasn't been initialized
{
// initialize the food element
this.food = new this.elementOnGrid(
this.board.circle(randXCoord * blockWidth + blockWidth / 2, randYCoord * blockWidth + blockWidth / 2, blockWidth / 2).attr('fill', '#cf6a4c'), // place circle in random location on the board (see http://raphaeljs.com/reference.html#Paper.circle)
randXCoord,
randYCoord
); // set food to be new element of type elementOnGrid
}
else // food element has been initialized (game is in play)
{
// move the food element
// ...
}
this.coords[randXCoord][randYCoord] = false; // indicate that coordinates of the good element is not open
}
this.startNew = function ( ) {
this.coords = this.getCoords();
this.snake = new this.Snake(this);
this.snake.createNew();
this.placeFood();
}
}
$(function () { // equivalent to $(document).ready(function() {
// div that holds the game area
var snakeBoardHolder = $('#snake-board-holder');
// make it have the same height as width
snakeBoardHolder.height(snakeBoardHolder.width());
// draw canvas for the snake to live on
// http://raphaeljs.com/reference.html#Raphael
if (!Raphael.svg) throw new Error("Your browser does not support SVG elements! Game won't work.");
snakeBoard = Raphael("snake-board-holder", snakeBoardHolder.width(), snakeBoardHolder.height());
// start new snake game
SG = new Game(snakeBoard, 16);
SG.startNew();
// make the game area (div) have height always equal to width,
// and make the Raphel object (canvas) inside it and all its elements
// to be resized proportionally
$(window).resize(function () {
var w = snakeBoardHolder.width();
snakeBoardHolder.height(w);
SG.rescale(w);
});
});
Any help in determing the piece of logic that is causing the bug would be greatly appreciated!
Inside this.Snake, I believe createNew() should be:
this.createNew = function ( )
{
this.links = [];
var blockWidth = game.board.getSize().width / game.numBlocks; // width in pixels of a block on the board
var centerCoordXY = Math.round(game.numBlocks / 2); // x-y grid coordinate of center
for (var i = 0; i < 3; ++i) // start with 3 blocks in the center-ish
{
var newX = centerCoordXY + i;
this.links.push(new game.elementOnGrid(
game.board.rect(newX * blockWidth, centerCoordXY * blockWidth, blockWidth, blockWidth).attr('fill', '#19FF19'), // http://raphaeljs.com/reference.html#Paper.circle
newX,
centerCoordXY
) // add element of type elementOnGrid to the links
);
game.coords[newX][centerCoordXY] = false; // indicate that coordinates of element just added to snake is no longer open
}
}
Inside this.links.push I've replaced an instance of centerCoordXY with newX.
You've got a lot of duplicated data (positions stored in 3 different places and in two different formats?) which is likely to cause issues like this if you fail to keep them all in sync. It may be better to use Canvas rather than SVG. If you're set on SVG, I'd recommend more helper functions. For example, instead of
new game.elementOnGrid(
game.board.rect(newX * blockWidth, centerCoordXY * blockWidth, blockWidth, blockWidth).attr('fill', '#19FF19'), // http://raphaeljs.com/reference.html#Paper.circle
newX,
centerCoordXY
)
consider a function that would allow you to do something like
newGridElement(newX, centerCoordXy, "#19FF19");
Anyway, good luck!
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 8 years ago.
Improve this question
I am trying to do some complicated effect, and to do it i have to break it down into its components, upon which i can build on and hopefully they will come together.
Now to make a circle in canvas is easy. But i want to make it myself. So I want to write a function that would be given a point that's center, radius, and then it will draw a circle with 1 px stroke width.
How would i go about it? If i look at from math perspective, what comes to mind is use circle distance formula and increment by small values, like .3 degrees, and make a dot at circumference. But if my circle is too small, like 2 px radius. Then it will waste lot of time drawing that won't matter, and if it's big enough you will see spaces between dots.
so i want my circle drawing function to draw a
dot if radius is 1px.
4 dots around the center if radius is 2px.
..and so on.
also if this gonna make my circle look rigid, i want there to be antialiasing too :D
I suppose once i know how to make outline filling it in won't be a problem..all i'd've to do is reduce the radius and keep drawing until radius is 1px.
You have the center x0, y0 and the radius r. Basically you need the parametric equation of circle:
x = x0 + r * cos(t)
y = y0 + r * sin(t)
Where t is the angle between a radial segment and normalized x-axis, and you need to divide it up as needed. For example for your four points case you do
360/4 = 90
and so use 0, 90, 180, 270 to get the four points.
OK, I've re-factored my earlier code as a jQuery plugin named "canvasLens". It accepts a bunch of options to control things like image src, lens size and border color. You can even choose between two different lens effects, "fisheye" or "scaledSquare".
I've tried to make it as self-explanatory as possible with a header block and plenty of other comments.
/*
* Copyright (c) 2014 Roamer-1888
* "canvasLens"
* a jQuery plugin for a lens effect on one or more HTML5 canvases
* by Roamer-1888, 2014-11-09
* http://stackoverflow.com/users/3478010/roamer-1888
*
* Written in response to aa question by Muhammad Umer, here
* http://stackoverflow.com/questions/26793321/
*
* Invoke on a canvas element as follows
* $("#canvas").lens({
* imgSrc: 'path/to/image',
* imgCrossOrigin: '' | 'anonymous' | 'use-credentials', //[1]
* drawImageCoords: [ //[2]
* 0, 0, //(sx,st) Source image sub-rectangle Left,Top.
* 1350, 788, //(sw/sh) Source image sub-rectangle Width,Height.
* 0, 0, //(dx/dy) Destination Left,Top.
* 800, 467 //(dw/dh) Destination image sub-rectangle Width,Height.
* ],
* effect: 'fisheye' | 'scaledSquare',
* scale: 2 //currently affects only 'scaledSquare'
* size: 100, //diameter/side-length of the lens in pixels
* hideCursor: true | false,
* border: [0, 0, 0, 255] //[r,g,b,alpha] (base-10) | 'none'
* });
*
* Demo: http://jsfiddle.net/7z6by3o3/1/
*
* Further reading :
* [1] imgCrossOrigin -
* https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes
* [2] drawImageCoords -
* https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D
*
* Licence: MIT - http://en.wikipedia.org/wiki/MIT_License
*
* Please keep this header block intact, with amendments
* to reflect any changes made to the code.
*
*/
(function($){
// *******************************
// ***** Start: Private vars *****
// *******************************
var pluginName = 'canvasLens';
// *****************************
// ***** Fin: Private vars *****
// *****************************
// **********************************
// ***** Start: Private Methods *****
// **********************************
// Note that in all private methods,
// `this` is the canvas on which
// the plugin is invoked.
// Most private methods are called
// with `methodName.call(this)`.
// **********************************
function animate() {
var data = $(this).data(pluginName);
if(data) {
draw.call(this);
requestAnimationFrame(animate.bind(this));
}
}
function draw() {
var data = $(this).data(pluginName);
data.ctx.drawImage(data.m_can, 0, 0);
if(data.showLens) {
if(data.settings.effect == 'scaledSquare') {
scaledSquare.call(this);
} else {
fisheye.call(this);
}
}
}
function putBg() {
var data = $(this).data(pluginName);
data.m_ctx.drawImage.apply(data.m_ctx, [data.img].concat(data.settings.drawImageCoords));
}
function scaledSquare() {
var data = $(this).data(pluginName),
xt = data.settings.scale,
h = data.settings.size;
data.ctx.drawImage(data.m_can,
data.mouse.x - h/xt/2, data.mouse.y - h/xt/2, //sx,st Source image sub-rectangle Left,Top coordinates.
h/xt, h/xt, //sw/sh Source image sub-rectangle Width,Height.
data.mouse.x - h/2, data.mouse.y - h/2, //dx/dy Destination Left,Top coordinates.
h, h //dw/dh The Width,Height to draw the image in the destination canvas.
);
}
function fisheye() {
var data = $(this).data(pluginName),
d = data.settings.size,
mx = data.mouse.x, my = data.mouse.y,
srcpixels = data.m_ctx.getImageData(mx - d/2, my - d/2, d, d);
fisheyeTransform.call(this, srcpixels.data, data.xpixels.data, d, d);
data.ctx.putImageData(data.xpixels, mx - d/2, my - d/2);
}
function fisheyeTransform(srcData, xData, w, h) {
/*
* Fish eye effect (barrel distortion)
* *** adapted from ***
* tejopa, 2012-04-29
* http://popscan.blogspot.co.ke/2012/04/fisheye-lens-equation-simple-fisheye.html
*/
var data = $(this).data(pluginName),
y, x, ny, nx, ny2, nx2, r, nr, theta, nxn, nyn, x2, y2, pos, srcpos;
for (var y=0; y<h; y++) { // for each row
var ny = ((2 * y) / h) - 1; // normalize y coordinate to -1 ... 1
ny2 = ny * ny; // pre calculate ny*ny
for (x=0; x<w; x++) { // for each column
pos = 4 * (y * w + x);
nx = ((2 * x) / w) - 1; // normalize x coordinate to -1 ... 1
nx2 = nx * nx; // pre calculate nx*nx
r = Math.sqrt(nx2 + ny2); // calculate distance from center (0,0)
if(r > 1) {
/* 1-to-1 pixel mapping outside the circle */
/* An improvement would be to make this area transparent. ?How? */
xData[pos+0] = srcData[pos+0];//red
xData[pos+1] = srcData[pos+1];//green
xData[pos+2] = srcData[pos+2];//blue
xData[pos+3] = srcData[pos+3];//alpha
}
else if(data.settings.border && data.settings.border !== 'none' && r > (1-3/w) && r < 1) { // circular border around fisheye
xData[pos+0] = data.settings.border[0];//red
xData[pos+1] = data.settings.border[1];//green
xData[pos+2] = data.settings.border[2];//blue
xData[pos+3] = data.settings.border[3];//alpha
}
else if (0<=r && r<=1) { // we are inside the circle, let's do a fisheye transform on this pixel
nr = Math.sqrt(1 - Math.pow(r,2));
nr = (r + (1 - nr)) / 2; // new distance is between 0 ... 1
if (nr<=1) { // discard radius greater than 1.0
theta = Math.atan2(ny, nx); // calculate the angle for polar coordinates
nxn = nr * Math.cos(theta); // calculate new x position with new distance in same angle
nyn = nr * Math.sin(theta); // calculate new y position with new distance in same angle
x2 = Math.floor(((nxn + 1) * w) / 2); // map from -1 ... 1 to image coordinates
y2 = Math.floor(((nyn + 1) * h) / 2); // map from -1 ... 1 to image coordinates
srcpos = Math.floor(4 * (y2 * w + x2));
if (pos >= 0 && srcpos >= 0 && (pos+3) < xData.length && (srcpos+3) < srcData.length) { // make sure that position stays within arrays
/* get new pixel (x2,y2) and put it to target array at (x,y) */
xData[pos+0] = srcData[srcpos+0];//red
xData[pos+1] = srcData[srcpos+1];//green
xData[pos+2] = srcData[srcpos+2];//blue
xData[pos+3] = srcData[srcpos+3];//alpha
}
}
}
}
}
}
// ********************************
// ***** Fin: Private methods *****
// ********************************
// *********************************
// ***** Start: Public Methods *****
// *********************************
var methods = {
'init': function(options) {
//"this" is a jquery object on which this plugin has been invoked.
return this.each(function(index) {
var can = this,
$this = $(this);
var data = $this.data(pluginName);
if (!data) { // If the plugin hasn't been initialized yet
data = {
target: $this,
showLens: false,
mouse: {x:0, y:0}
};
$this.data(pluginName, data);
var settings = {
imgSrc: '',
imgCrossOrigin: '',
drawImageCoords: [
0, 0, //sx,st Source image sub-rectangle Left,Top coordinates.
500, 500, //sw/sh Source image sub-rectangle Width,Height.
0, 0, //dx/dy Destination Left,Top coordinates.
500, 500 //(dw/dh) Destination image sub-rectangle Width,Height.
],
effect: 'fisheye',
scale: 2,
size: 100,
border: [0, 0, 0, 255], //[r,g,b,alpha] base-10
hideCursor: false
};
if(options) {
$.extend(true, settings, options);
}
data.settings = settings;
if(settings.hideCursor) {
data.originalCursor = $this.css('cursor');
$this.css('cursor', 'none');
}
$this.on('mouseenter.'+pluginName, function(e) {
data.showLens = true;
}).on('mousemove.'+pluginName, function(e) {
data.mouse.x = e.offsetX;
data.mouse.y = e.offsetY;
}).on('mouseleave.'+pluginName, function(e) {
data.showLens = false;
});
data.m_can = $("<canvas>").attr({
'width': can.width,
'height': can.height
})[0];
data.ctx = can.getContext("2d"); // lens effect
data.m_ctx = data.m_can.getContext('2d'); // background image
data.xpixels = data.ctx.getImageData(0, 0, settings.size, settings.size);
data.img = new Image();
data.img.onload = function() {
putBg.call(can);
animate.call(can);
};
data.img.crossOrigin = settings.imgCrossOrigin;
data.img.src = settings.imgSrc;
}
});
},
'destroy': function() {
return this.each(function(index) {
var $this = $(this),
data = $this.data(pluginName);
$this.off('mouseenter.'+pluginName)
.off('mousemove.'+pluginName)
.off('mouseleave.'+pluginName);
if(data && data.originalCursor) {
$this.css('cursor', data.originalCursor);
}
$this.data(pluginName, null);
});
}
};
// *******************************
// ***** Fin: Public Methods *****
// *******************************
// *****************************
// ***** Start: Supervisor *****
// *****************************
$.fn[pluginName] = function( method ) {
if ( methods[method] ) {
return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof method === 'object' || !method ) {
return methods.init.apply( this, arguments );
} else {
$.error( 'Method ' + method + ' does not exist in jQuery.' + pluginName );
}
};
// ***************************
// ***** Fin: Supervisor *****
// ***************************
})(jQuery);
And here's a Demo.
Edit
Here's an attempt at explaining the fisheye (barrel distortion) calculations ...
Starting with a blank lens of w x h pixels.
The code loops through all pixels (target pixels).
For each target pixel, chooses a pixel (source pixel) from the background image.
The source pixel is always selected from those on (or close to) the same radial ray as the target, but at a smaller radial distance (using a formula for barrel distortion) from the lens's center.
This is mechanised by calculation of the polar coordinates (nr, theta) of the source pixel, then the application a standard math formula for converting polar back to rectangular coordinates nxn = nr * Math.cos(theta) and nxn = nr * Math.sin(theta). Up to this point, all calculations have been made in normalised -1...0...1 space.
The rest of the code in the block denormaises (rescales), and (the bit I had to heavily adapt) actually implements the source to target pixel mapping by indexing into the 1-dimensional source and target data.