Raphael JS combined elements with separate control: Removing Event Part 2 - events

This is a continuation of an earlier thread, that was answered by Neil- thanks Neil! I probably should have included this in the first question, but I wanted to simplify things...
Another feature that I need is to have a "dialogue box" that holds a title and some text that animates on and off next to the circle when it comes on. I achieved this in my earlier version prior to the help from Neil. I have spent some time trying to integrate it into the new and improved code and get some unexpected results. For example, if I rollover the first circle on the right, it works as it should, however, if I try to rollover the middle and right circles, they don't work. Oddly, if I refresh and start on the right circle, each will work when moving right to left, until I reach the left one, and then the middle and right don't work- but the left one continues to work. Additionally, if I click on the left circle, it works as it should, but then the others don't work. And conversely, if I click on the right one first, and then move to the middle, the middle works on click, but then the right one does not.
The behavior that I am looking for is that each circle, animate up with the rectangle next to the circle fading in with dynamic text on mouseover and animate down, with the rectangle with text fading out on mouseout. The rectangle with text should fade out on click and not fade up again if the user mousesover the clicked circle (need to remove the mouseover event as well I guess). One additional thing that needs to happen is that the rectangle needs to appear in a different place on the circle, depending where on the map it is- so that it doesn't fall off the map. I did this successfully in the earlier version, but have left that out on the previous post for better clarity. I will include it here so you get the gist of what I'm doing.
My guess is that I need to create a set() of the rectangle/text component and place it in an another set() along with the circle?
Any help on this is truly appreciated! Thanks
// JavaScript Document
var arr = [
[50, 50, "this", "Is text that is to be the abstract"],
[100, 50, "that", "Is text that I hope is here"],
[150, 50, "another thing", "Even more text"]
];
var currRect;
var currTitleTxt;
var currTeaseTxt;
var prevItem;
doMe();
function doMe() {
var paper = new Raphael(document.getElementById('canvas_container'), 696, 348);
for (var i = 0; i < arr.length; i++) {
paper.circle(arr[i][0], arr[i][1], 6).attr({
fill: '#fff',
'fill-opacity': 0.5
}).data("i", [arr[i][0], arr[i][1], arr[i][2], arr[i][3]]).click(function () {
this.unmouseout();
}).click(function () {
if (this.data('selected') != 'true') {
if (prevItem != null) {
prevItem.data('selected', '');
handleOutState(prevItem);
}
prevItem = this.data('selected', 'true');
currRect.animate({"fill-opacity":0, "stroke-opacity":0}, 150 );
currTitleTxt.animate({"fill-opacity":0}, 150 );
currTeaseTxt.animate({"fill-opacity":0}, 150 );
}
}).mouseover(function () {
handleOverState(this);
if(this.data("i")[0] <= 350){ //this is where I test for the position on the map
paper.setStart(); //create rectangle and text set
currRect =paper.rect(17, -20, 265,90).attr({fill:"#999","fill-opacity":0.5});
currTitleTxt = paper.text(25, -8, this.data("i")[2]).attr({"text-anchor":"start",fill:'#ffffff',"font-size": 14, "font-weight":"bold","fill-opacity":0});
currTeaseTxt = paper.text(25, 30).attr({"text-anchor":"start",fill:'#eeeeee',"font-size": 11, "font-weight":"bold","fill-opacity":0});
var maxWidth = 250;
var content = this.data("i")[3];
var words = content.split(" ");
var tempText = ""; //since Raphael doesn't have native word wrap, I break the line manually
for (var i=0; i<words.length; i++) {
currTeaseTxt.attr("text", tempText + " " + words[i]);
if (currTeaseTxt.getBBox().width > maxWidth) {
tempText += "\n" + words[i];
} else {
tempText += " " + words[i];
}
}
currTeaseTxt.attr("text", tempText.substring(1));
var st = paper.setFinish();
st.translate(this.data("i")[0]+10, this.data("i")[1]+0).animate({"fill-opacity":1}, 150 );
}else if(this.data("i")[0] >= 351){ //this is where I test for the position on the map
paper.setStart();
currRect = paper.rect(-280, -20, 250,50).attr({fill:"#999","fill-opacity":0.5});
currTitleTxt = paper.text(-270, -10, this.data("i")[2]).attr({"text-anchor":"start",fill:'#ffffff',"font-size": 14, "font-weight":"bold","fill-opacity":0});
currTeaseTxt =paper.text(-270, 5, this.data("i")[3]).attr({"text-anchor":"start",fill:'#cccccc',"font-size": 12, "font-weight":"bold","fill-opacity":0});
var st = paper.setFinish();
st.translate(this.data("i")[0]+10, this.data("i")[1]+0).animate({"fill-opacity":1}, 150 );
}
}).mouseout(function () {
currRect.animate({"fill-opacity":0, "stroke-opacity":0}, 150 );
currTitleTxt.animate({"fill-opacity":0}, 150 );
currTeaseTxt.animate({"fill-opacity":0}, 150 );
if (this.data('selected') != 'true') handleOutState(this);
});
}
function handleOverState(el) {
el.animate({
r: 8
}, 250).animate({
"fill-opacity": 1
}, 150);
}
function handleOutState(el) {
el.animate({
r: 6
}, 250).animate({
"fill-opacity": 0.5
}, 150);
}
}

Related

How to add countdown timer to Phaser game

I am trying to add a simple countdown timer to a Phaser game. When the timer ends I want the game to restart. After adding the relevant code there are no errors in the console but I can't get the timer to appear on the screen. I am new to Phaser and am still learning Javascript. Where I am going wrong please? I have posted only the relevant code below, and the code used for already existing text in the game that is working fine (text to count coins collected).
PlayState = {};
PlayState.init = function () {
//....
};
PlayState.preload = function () {
this.game.load.image('font:numbers', 'images/numbers.png'); //for the
//HUD coin count - not the timer
};
PlayState.create = function () {
//TIMER CODE:
this.timeInSeconds = 120;
this.timeText = this.game.add.text(this.game.world.centerX,
this.game.world.centerY, "0:00",{font: '15px Arial', fill: '#FFFFFF', align:
'center'});
this.timeText.anchor.set(0.5, 0.5);
this.timer = this.game.time.events.loop(Phaser.Timer.SECOND,
this.updateTimer, this);
};
PlayState.update = function () {
this.coinFont.text = `x${this.coinPickupCount}`; //for HUD coin count not
//the timer
};
//TIMER CODE:
PlayState.updateTimer = function() {
this.timeInSeconds--;
var minutes = Math.floor(this.timeInSeconds / 60);
var seconds = this.timeInSeconds - (minutes * 60);
var timeString = this.addZeros(minutes) + ":" + this.addZeros(seconds);
this.timeText.text = timeString;
if (this.timeInSeconds == 0) {
this.game.state.restart();
}
};
//add leading zeros to any number less than 10
//for example turn 1 to 01
PlayState.addZeros = function(num) {
if (num < 10) {
num = "0" + num;
}
return num;
};
//BELOW IS CODE FOR THE COIN COUNT NOT THE TIMER
PlayState._createHud = function () {
this.keyIcon = this.game.make.image(0, 30, 'icon:key');
this.keyIcon.anchor.set(0, 0.5);
const NUMBERS_STR = '0123456789X ';
this.coinFont = this.game.add.retroFont('font:numbers', 20,
26,NUMBERS_STR, 6);
let coinIcon = this.game.make.image(this.keyIcon.width + 7,
0, 'icon:coin');
let coinScoreImg = this.game.make.image(coinIcon.x +
coinIcon.width, coinIcon.height / 2, this.coinFont);
coinScoreImg.anchor.set(0, 0.5);
this.hud = this.game.add.group();
this.hud.add(coinIcon);
this.hud.position.set(10, 10);
this.hud.add(coinScoreImg);
this.hud.add(this.keyIcon);
this.hud.fixedToCamera = true;
};
window.onload = function () {
let game = new Phaser.Game(1280, 800, Phaser.CANVAS, 'game');
game.state.add('play', PlayState);
game.state.start('play');
};
I have finally solved the issue. The text was not showing because it was being rendered AFTER the background image in create. So it was there but being hidden by the background image. I simply moved the timer code to the end of create and it now works.
PlayState.create = function () {
this.game.world.setBounds(0, 0, 2560, 800);
background1 = this.game.add.sprite(0, 0, 'background');
background2 = this.game.add.sprite(1280, 0, 'background2');
this.game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
this.game.scale.setMinMax(480,320,1280,800);
this.game.scale.windowConstraints.bottom = 'visual';
this.game.add.image(0, 0, 'background');
this._loadLevel(this.game.cache.getJSON('level:1'));
this._createHud();
//TIMER CODE SHOULD GO HERE AND NOT AT THE BEGINNING OF CREATE
this.timeInSeconds = 120;
this.timeText = this.game.add.text(220, 30, "0:00",{font: '30px Arial', fill:
'#FFFFFF', align: 'center'});
this.timeText.anchor.set(0.5, 0.5);
this.timer = this.game.time.events.loop(Phaser.Timer.SECOND, this.updateTimer,
this);
};
You set the initial text to "0:00" and even that doesn't show on screen? First thing I would do is look at the coordinates where the text is located, maybe it's not visible off screen. Instead of this.game.world.centerX, this.game.world.centerY try something like 100,100, does it show up then? Also try to set very long initial text, so something like "blah test ABC 123" instead of "0:00" maybe makes some difference.
Secondly, maybe the Arial font is not available for some reason. If you leave out the {font: '15px..'center'} part it will use a default font, does that change anything?
Thirdly, you say you didn't post all your code here, but maybe you accidentally overwrite the variable this.timeText somewhere in you code? So check that you don't set that variable to be something else.
Finally, I would add a console.log to the updateTimer function, just to see if it is being called. so:
PlayState.updateTimer = function() {
console.log('updateTimer was called: ' + this.timeInSeconds);
this.timeInSeconds--;
// etc.

AmChart move labelText to right a few pixels

I'm trying to move label test to right few pixels because the way it's displayed now it looks like it is more to the left:
Label text is aligned to center for 2d bar charts but when you have 3d bars you have this slight offset effect to left that needs to be corrected.Label position values are: "bottom", "top", "right", "left", "inside", "middle".
I wasn't able to fine tune it.
Any ideas on this?
As mentioned in my comment, the labels are centered with respect to the angle setting for 3D charts. The API doesn't allow you to shift the label left or right, so you have to manipulate the graph SVG nodes directly through the drawn event. If you set addClassNames to true, you can retrieve the label elements using document.querySelectorAll through the generated DOM class names and then modifying the translate value in the transform attribute accordingly. You can use a technique from this SO answer to easily manipulate the transform attribute as an object:
// ...
"addClassNames": true,
"listeners": [{
"event": "drawn",
"method": function(e) {
Array.prototype.forEach.call(
document.querySelectorAll(".amcharts-graph-g4 .amcharts-graph-label"),
function(graphLabel) {
var transform = parseTransform(graphLabel.getAttribute('transform'));
transform.translate[0] = parseFloat(transform.translate[0]) + 5; //adjust X offset
graphLabel.setAttribute('transform', serializeTransform(transform));
});
}
}]
// ...
// from http://stackoverflow.com/questions/17824145/parse-svg-transform-attribute-with-javascript
function parseTransform(a) {
var b = {};
for (var i in a = a.match(/(\w+\((\-?\d+\.?\d*e?\-?\d*,?)+\))+/g)) {
var c = a[i].match(/[\w\.\-]+/g);
b[c.shift()] = c;
}
return b;
}
//serialize transform object back to an attribute string
function serializeTransform(transformObj) {
var transformStrings = [];
for (var attr in transformObj) {
transformStrings.push(attr + '(' + transformObj[attr].join(',') + ')');
}
return transformStrings.join(',');
}
Updated fiddle

hammer.js detect variables in panmove and unbind when it hits certain criteria

my goal is detect when an element has reached a certain margin-left, and than unbind or stop the panmove from continuing if it hits that threshold.
I have a "panmove" bound to an element using hammer.js, and jquery hammer plugin.
I noticed that in the panmove, console.log(e) will fire hundreds of times as you move the elements, which is expected. If you however put an if statement in the panmove function, it only goes off of the initial state of the first panmove and not the current one.
.bind("panmove", function (e) {
var count = 0;
console.log(e);
console.log(count++);
var _this = $(e.target);
var _thisDataLeft = _this.attr("data-left");
var _thisDataMaxLeft = _this.attr("data-maxleft"); // this is derived from the width of the Delete box, which can be any width.
if (Math.abs(_thisDataLeft) < Number(_thisDataMaxLeft)) {
_this.css({ left: Number(_thisDataLeft) + e.gesture.deltaX }); // controls movement of top layer
console.log(count++);
}
I noticed that the console.log(count++) always fires 1, instead of iterating up, as if it is only reading it once in the beginning.
How can I run an if statement inside of this Pan, so that it is always the current information, and not just the first iteration?
Ended up moving away from Hammer.js, was not able to get the results I needed. It looks like the more basic jquery.event.move.js was easier to use than hammer.
here is my example in js fiddle
https://jsfiddle.net/williamhowley/o9uvo50y/
$(document).ready(function () {
// http://stephband.info/jquery.event.move/
// http://stephband.info/jquery.event.swipe/
// add swipe functionality to the rows.
// I think you will need to add the swipe left, after it is activated by a HOLD down press.
// idk, how do you always make something swipable.
var wrap = $('ul#main');
$('ul#main > li')
.on('movestart', function (e) {
console.log("move start");
// var $li = $(e.target).closest('.swipable'); // this would be normal live integration
var $li = $(e.target);
if ($li.attr("data-hasplaceholder") !== "true") { // if it does not have a placeholder, add one.
createBackgroundSpacer($li);
$li.attr("data-hasplaceholder", true); // signify that a placeholder has been created for this element already.
}
// If the movestart heads off in a upwards or downwards
// direction, prevent it so that the browser scrolls normally.
if ((e.distX > e.distY && e.distX < -e.distY) ||
(e.distX < e.distY && e.distX > -e.distY)) {
e.preventDefault();
return;
}
// To allow the slide to keep step with the finger,
// temporarily disable transitions.
wrap.addClass('notransition'); // add this to the container wrapper.
})
.on('move', function (e) {
// event definitions
// startX : 184, where from left the mouse curser started.
// deltaX: ?
// distX: how far the mouse has moved, if negative moving left. Still need to account for double movement, currently can only handle one movement.
console.log("move");
console.log(e);
var maxLeft = $('.rightContent').width();
var marginLeftNum = Number($(this).css('margin-left').replace(/[^-\d\.]/g, ''));
if (marginLeftNum <= -maxLeft && e.deltaX < 0) { // Case when user is at outermost left threshold, and trying to move farther left.
$(this).css({ 'margin-left': -maxLeft });
}
else if (marginLeftNum == -maxLeft && e.deltaX > 0) { // When user is at threshold, and trying to move back right.
$(this).css({ 'margin-left': marginLeftNum + e.deltaX });
}
else if (e.target.offsetLeft>=0 && e.deltaX>0) { // If the offset is 0 or more, and the user is scrolling right (which is a positive delta, than limit the element. )
$(this).css({ 'margin-left': 0 });
}
// Must have a Negative offset, and e.deltaX is Negative so it is moving left.
else if (e.deltaX < 0) { // Case when element is at 0, and mouse movement is going left.
$(this).css({ 'margin-left': marginLeftNum + e.deltaX });
}
else { // Moving Right when not on 0
$(this).css({ 'margin-left': marginLeftNum + e.deltaX });
}
})
.on('swipeleft', function (e) {
console.log("swipeleft");
})
.on('activate', function (e) {
// not seeing this activate go off, i think this is custom function we can add on if swipe left hits a threshold or something.
console.log("activate");
})
.on('moveend', function (e) {
console.log("move end");
wrap.removeClass('notransition');
});
var createBackgroundSpacer = function ($shoppingListRow) {
var border = 2;
$shoppingListRow.css({ 'width': $shoppingListRow.width() + border, 'height': $shoppingListRow.height() + border }); // gives itself set width and height
$shoppingListRow.addClass('swipable');
// placeholder HTML
var leftPlaceholder = $('<div class="leftPlaceholder"></div>').css({ 'height': $shoppingListRow.height()});
var rightPlaceholder = $('<div class="rightPlaceholder"></div>')
var rightContent = $('<div class="rightContent">Delete</div>').css({ 'height': $shoppingListRow.height()});
rightPlaceholder.append(rightContent);
var placeHolder = $('<div class="swipePlaceholder clearfix"></div>'); // goes around the two floats.
placeHolder.css({ 'width': $shoppingListRow.width(), 'height': $shoppingListRow.height() });
placeHolder.append(leftPlaceholder, rightPlaceholder);
$shoppingListRow.before(placeHolder); // adds placeholder before the row.
$shoppingListRow.css({ 'marginTop': -($shoppingListRow.height() + border) });
};
});

Infinite Scrolling with wookmark plugins scrolling

With ref. to above subject, I am using wookmark plugin to scroll our home page data dynamically….I have studied the tutorial provided on wookmark and I m using the exact script provided by wookmark and working fine shorts of not 100% working.
Things it stucks when it reaches at bottom of the window then we slightly press the up arrow key, that loads the products again and this is happens randomly some time it scrolls perfectly and some time it stucks and if presses up arrow key it starts working again.
Kindly help me out where I m going wrong. Kindly provide me the easy working script for the same.
I m using following code :
(function ($) {
$('#main').imagesLoaded(function () {
var handler = null;
// Prepare layout options.
var options = {
itemWidth: 200, // Optional min width of a grid item
autoResize: true, // This will auto-update the layout when the browser window is resized.
container: $('#main'), // Optional, used for some extra CSS styling
offset: 20, // Optional, the distance between grid items
outerOffset: 20, // Optional the distance from grid to parent
flexibleWidth: 300 // Optional, the maximum width of a grid item
};
function applyLayout() {
$('#main').imagesLoaded(function () {
// Destroy the old handler
if (handler.wookmarkInstance) {
handler.wookmarkInstance.clear();
}
// Create a new layout handler.
handler = $('#display li');
handler.wookmark(options);
});
handler.wookmark(options);
}
/**
* When scrolled all the way to the bottom, add more tiles.
*/
function onScroll(event) {
// Check if we're within 100 pixels of the bottom edge of the broser window.
var winHeight = window.innerHeight ? window.innerHeight : $(window).height(); // iphone fix
//var closeToBottom = ($(window).scrollTop() >= $((document)).height() - $((window)).height() - $("#footer").height() - 500); //(($(window).scrollTop() - 100)); //+ "%"
var closeToBottom = ($(window).scrollTop() + winHeight > $(document).height() - 100);
if (closeToBottom) {
// Get the first then items from the grid, clone them, and add them to the bottom of the grid.
var items = $('#display li'),
firstTen = items.slice(0, 10);
//$('#display').append(firstTen.clone());
applyLayout();
}
};
// Capture scroll event.
$(window).bind('scroll', onScroll);
// Call the layout function.
handler = $('#display li');
handler.wookmark(options);
});
$(window).load(function () {
handler.wookmark(options);
});
})(jQuery);
If you commented out
//$('#display').append(firstTen.clone());
then the new items will not be loaded on the end of list. You need to uncomment that line to get new items.
In real life instead of
var items = $('#display li'),
firstTen = items.slice(0, 10);
$('#display').append(firstTen.clone());
you would need a code that will load new items.
Also I think it might make sense to change > to >=
var closeToBottom = ($(window).scrollTop() + winHeight >= $(document).height() - 100);
to load new items if scroll position is more or equal to the height of window - 100, where 100 is just some value - you could try 200 or even more to see if it will work better for you.

Creating a 3D free-camera in WebGL - why do neither of these methods work?

EDIT
OK, I've tried a camera using quaternions:
qyaw = [Math.cos(rot[0]/2), 0, Math.sin(rot[0]/2), 0];
qpitch = [Math.cos(rot[1]/2), 0, 0, Math.sin(rot[1]/2)];
rotQuat = quat4.multiply (qpitch, qyaw);
camRot = quat4.toMat4(rotQuat);
camMat = mat4.multiply(camMat,camRot);
and I get exactly the same problem. So I'm guessing it's not gimbal lock. I've tried changing the order I multiply my matrices, but it just goes camera matrix * model view matrix, then object matrix * model view. That's right isn't it?
I'm trying to build a 3d camera in webGL that can move about the world and be rotated around the x and y (right and up) axes.
I'm getting the familiar problem (possibly gimbal lock?) that once one of the axes is rotated, the rotation around the other is screwed up; for example, when you rotate around the Y axis 90degrees, rotation around the x becomes a spin around z.
I appreciate this is a common problem, and there are copious guides to building a camera that avoid this problem, but as far as I can tell, I've implemented two different solutions and I'm still getting the same problem. Frankly, it's doing my head in...
One solution I'm using is this (adapted from http://www.toymaker.info/Games/html/camera.html):
function updateCam(){
yAx = [0,1,0];
xAx = [1,0,0];
zAx = [0,0,1];
mat4.identity(camMat);
xRotMat = mat4.create();
mat4.identity(xRotMat)
mat4.rotate(xRotMat,rot[0],xAx);
mat4.multiplyVec3(xRotMat,zAx);
mat4.multiplyVec3(xRotMat,yAx);
yRotMat = mat4.create();
mat4.identity(yRotMat)
mat4.rotate(yRotMat,rot[1],yAx);
mat4.multiplyVec3(yRotMat,zAx);
mat4.multiplyVec3(yRotMat,xAx);
zRotMat = mat4.create();
mat4.identity(zRotMat)
mat4.rotate(zRotMat,rot[2],zAx);
mat4.multiplyVec3(zRotMat,yAx);
mat4.multiplyVec3(zRotMat,xAx);
camMat[0] = xAx[0];
camMat[1] = yAx[0];
camMat[2] = zAx[0];
//camMat[3] =
camMat[4] = xAx[1]
camMat[5] = yAx[1];
camMat[6] = zAx[1];
//camMat[7] =
camMat[8] = xAx[2]
camMat[9] = yAx[2];
camMat[10]= zAx[2];
//camMat[11]=
camMat[12]= -1* vec3.dot(camPos, xAx);
camMat[13]= -1* vec3.dot(camPos, yAx);
camMat[14]= -1* vec3.dot(camPos, zAx);
//camMat[15]=
var movSpeed = 1.5 * forward;
var movVec= vec3.create(zAx);
vec3.scale(movVec, movSpeed);
vec3.add(camPos, movVec);
movVec= vec3.create(xAx);
movSpeed = 1.5 * strafe;
vec3.scale(movVec, movSpeed);
vec3.add(camPos, movVec);
}
I also tried using this method using
mat4.rotate(camMat, rot[1], yAx);
instead of explicitly building the camera matrix - same result.
My second (actually first...) method looks like this (rot is an array containing the current rotations around x, y and z (z is always zero):
function updateCam(){
mat4.identity(camRot);
mat4.identity(camMat);
camRot = fullRotate(rot);
mat4.set(camRot,camMat);
mat4.translate(camMat, camPos);
}
function fullRotate(angles){
var cosX = Math.cos(angles[0]);
var sinX = Math.sin(angles[0]);
var cosY = Math.cos(angles[1]);
var sinY = Math.sin(angles[1]);
var cosZ = Math.cos(angles[2]);
var sinZ = Math.sin(angles[2]);
rotMatrix = mat4.create([cosZ*cosY, -1*sinZ*cosX + cosZ*sinY*sinX, sinZ*sinX+cosZ*sinY*cosX, 0,
sinZ*cosY, cosZ*cosX + sinZ*sinY*sinX, -1*cosZ*sinX + sinZ*sinY*cosX, 0,
-1*sinY, cosY*sinX, cosY*cosX, 0,
0,0,0,1 ] );
mat4.transpose(rotMatrix);
return (rotMatrix);
}
The code (I've taken out most of the boilerplate gl lighting stuff etc and just left the transformations) to actually draw the scene is:
function drawScene() {
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 2000.0, pMatrix);
mat4.identity(mvMatrix);
for(var i=0; i<planets.length; i++){
if (planets[i].type =="sun"){
currentProgram = perVertexSunProgram;
} else {
currentProgram = perVertexNormalProgram;
}
alpha = planets[i].alphaFlag;
mat4.identity(planets[i].rotMat);
mvPushMatrix();
//all the following puts planets in orbit around a central sun, but it's not really relevant to my current problem
var rot = [0,rotCount*planets[i].orbitSpeed,0];
var planetMat;
planetMat = mat4.create(fullRotate(rot));
mat4.multiply(planets[i].rotMat, planetMat);
mat4.translate(planets[i].rotMat, planets[i].position);
if (planets[i].type == "moon"){
var rot = [0,rotCount*planets[i].moonOrbitSpeed,0];
moonMat = mat4.create(fullRotate(rot));
mat4.multiply(planets[i].rotMat, moonMat);
mat4.translate(planets[i].rotMat, planets[i].moonPosition);
mat4.multiply(planets[i].rotMat, mat4.inverse(moonMat));
}
mat4.multiply(planets[i].rotMat, mat4.inverse(planetMat));
mat4.rotate(planets[i].rotMat, rotCount*planets[i].spinSpd, [0, 1, 0]);
//this bit does the work - multiplying the model view by the camera matrix, then by the matrix of the object we want to render
mat4.multiply(mvMatrix, camMat);
mat4.multiply(mvMatrix, planets[i].rotMat);
gl.useProgram(currentProgram);
setMatrixUniforms();
gl.drawElements(gl.TRIANGLES, planets[i].VertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
mvPopMatrix();
}
}
However, most of the transformations can be ignored, the same effect cab be seen simply displaying a sphere at world coords 0,0,0.
I thought my two methods - either rotating the axes one at a time as you go, or building up the rotation matrix in one go avoided the problem of doing two rotations one after the other. Any ideas where I'm going wrong?
PS - I'm still very much starting to learn WebGL and 3d maths, so be gentle and talk to me like someone who hadn't heard of a matrix til a couple of months ago... Also, I know quaternions are a good solution to 3d rotation, and that would be my next attempt, however, I think I need to understand why these two methods don't work first...
For the sake of clarification, think about gimbal lock this way: You've played Quake/Unreal/Call of Duty/Any First Person Shooter, right? You know how when you are looking forward and move the mouse side to side your view swings around in a nice wide arc, but if you look straight up or down and move your mouse side to side you basically just spin tightly around a single point? That's gimbal lock. It's something that pretty much any FPS game uses because it happens to mimic what we would do in real life, and thus most people don't usually think of it as a problem.
For something like a space flight sim, however, or (more commonly) skeletal animation that type of effect is undesirable, and so we use things like quaternions to help us get around it. Wether or not you care about gimbal lock for your camera depends on the effect that you are looking to achieve.
I don't think you're experiencing that, however. What it sounds like is that your order of matrix multiplication is messed up, and as a result your view is rotating in a way that you don't expect. I would try playing with the order that you do your X/Y/Z rotations in and see if you can find an order than gives you the desired results.
Now, I hate doing code dumps, but this may be useful to you so here we go: This is the code that I use in most of my newer WebGL projects to manage a free-floating camera. It is gimbal locked, but as I mentioned earlier it doesn't really matter in this case. Basically it just gives you FPS style controls that you can use to fly around your scene.
/**
* A Flying Camera allows free motion around the scene using FPS style controls (WASD + mouselook)
* This type of camera is good for displaying large scenes
*/
var FlyingCamera = Object.create(Object, {
_angles: {
value: null
},
angles: {
get: function() {
return this._angles;
},
set: function(value) {
this._angles = value;
this._dirty = true;
}
},
_position: {
value: null
},
position: {
get: function() {
return this._position;
},
set: function(value) {
this._position = value;
this._dirty = true;
}
},
speed: {
value: 100
},
_dirty: {
value: true
},
_cameraMat: {
value: null
},
_pressedKeys: {
value: null
},
_viewMat: {
value: null
},
viewMat: {
get: function() {
if(this._dirty) {
var mv = this._viewMat;
mat4.identity(mv);
mat4.rotateX(mv, this.angles[0]-Math.PI/2.0);
mat4.rotateZ(mv, this.angles[1]);
mat4.rotateY(mv, this.angles[2]);
mat4.translate(mv, [-this.position[0], -this.position[1], - this.position[2]]);
this._dirty = false;
}
return this._viewMat;
}
},
init: {
value: function(canvas) {
this.angles = vec3.create();
this.position = vec3.create();
this.pressedKeys = new Array(128);
// Initialize the matricies
this.projectionMat = mat4.create();
this._viewMat = mat4.create();
this._cameraMat = mat4.create();
// Set up the appropriate event hooks
var moving = false;
var lastX, lastY;
var self = this;
window.addEventListener("keydown", function(event) {
self.pressedKeys[event.keyCode] = true;
}, false);
window.addEventListener("keyup", function(event) {
self.pressedKeys[event.keyCode] = false;
}, false);
canvas.addEventListener('mousedown', function(event) {
if(event.which == 1) {
moving = true;
}
lastX = event.pageX;
lastY = event.pageY;
}, false);
canvas.addEventListener('mousemove', function(event) {
if (moving) {
var xDelta = event.pageX - lastX;
var yDelta = event.pageY - lastY;
lastX = event.pageX;
lastY = event.pageY;
self.angles[1] += xDelta*0.025;
while (self.angles[1] < 0)
self.angles[1] += Math.PI*2;
while (self.angles[1] >= Math.PI*2)
self.angles[1] -= Math.PI*2;
self.angles[0] += yDelta*0.025;
while (self.angles[0] < -Math.PI*0.5)
self.angles[0] = -Math.PI*0.5;
while (self.angles[0] > Math.PI*0.5)
self.angles[0] = Math.PI*0.5;
self._dirty = true;
}
}, false);
canvas.addEventListener('mouseup', function(event) {
moving = false;
}, false);
return this;
}
},
update: {
value: function(frameTime) {
var dir = [0, 0, 0];
var speed = (this.speed / 1000) * frameTime;
// This is our first person movement code. It's not really pretty, but it works
if(this.pressedKeys['W'.charCodeAt(0)]) {
dir[1] += speed;
}
if(this.pressedKeys['S'.charCodeAt(0)]) {
dir[1] -= speed;
}
if(this.pressedKeys['A'.charCodeAt(0)]) {
dir[0] -= speed;
}
if(this.pressedKeys['D'.charCodeAt(0)]) {
dir[0] += speed;
}
if(this.pressedKeys[32]) { // Space, moves up
dir[2] += speed;
}
if(this.pressedKeys[17]) { // Ctrl, moves down
dir[2] -= speed;
}
if(dir[0] != 0 || dir[1] != 0 || dir[2] != 0) {
var cam = this._cameraMat;
mat4.identity(cam);
mat4.rotateX(cam, this.angles[0]);
mat4.rotateZ(cam, this.angles[1]);
mat4.inverse(cam);
mat4.multiplyVec3(cam, dir);
// Move the camera in the direction we are facing
vec3.add(this.position, dir);
this._dirty = true;
}
}
}
});
This camera assumes that Z is your "Up" axis, which may or may not be true for you. It's also using ECMAScript 5 style objects, but that shouldn't be an issue for any WebGL-enabled browser, and it utilizes my glMatrix library but it looks like you're already using that anyway. Basic usage is pretty simple:
// During your init code
var camera = Object.create(FlyingCamera).init(canvasElement);
// During your draw loop
camera.update(16); // 16ms per-frame == 60 FPS
// Bind a shader, etc, etc...
gl.uniformMatrix4fv(shaderUniformModelViewMat, false, camera.viewMat);
Everything else is handled internally for you, including keyboard and mouse controls. May not fit your needs exactly, but hopefully you can glean what you need to from there. (Note: This is essentially the same as the camera used in my Quake 3 demo, so that should give you an idea of how it works.)
Okay, that's enough babbling from me for one post! Good luck!
It doesn't matter how you build your matrices, using euler angle rotations (like both of your code snippets do) will always result in a transformation that shows the gimble lock problem.
You may want to have a look at https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation as a starting point for creating transformations that avoid gimble locks.
Try my new project (webGL2 part of visual-js game engine) based on glmatrix 2.0 .
Activate events for camera use : App.camera.FirstPersonController = true;
live examples
For camera important functions :
Camera interaction
App.operation.CameraPerspective = function() {
this.GL.gl.viewport(0, 0, wd, ht);
this.GL.gl.clear(this.GL.gl.COLOR_BUFFER_BIT | this.GL.gl.DEPTH_BUFFER_BIT);
// mat4.identity( world.mvMatrix )
// mat4.translate(world.mvMatrix , world.mvMatrix, [ 10 , 10 , 10] );
/* Field of view, Width height ratio, min distance of viewpoint, max distance of viewpoint, */
mat4.perspective(this.pMatrix, degToRad( App.camera.viewAngle ), (this.GL.gl.viewportWidth / this.GL.gl.viewportHeight), App.camera.nearViewpoint , App.camera.farViewpoint );
};
manifest.js :
var App = {
name : "webgl2 experimental",
version : 0.3,
events : true,
logs : false ,
draw_interval : 10 ,
antialias : false ,
camera : { viewAngle : 45 ,
nearViewpoint : 0.1 ,
farViewpoint : 1000 ,
edgeMarginValue : 100 ,
FirstPersonController : false },
textures : [] , //readOnly in manifest
tools : {}, //readOnly in manifest
download source from :
webGL 2 part of visual-js GE project
Old :
opengles 1.1
https://stackoverflow.com/a/17261523/1513187
Very fast first person controler with glmatrix 0.9 based on http://learningwebgl.com/ examples.

Resources