JS Assigning Callbacks through Iteration - javascript

I have a small program that uses RaphaelJS to accept some nodes and edges, much like arborjs and sigmajs, to produce tree graphs. I would use either of the said libraries but I am trying to learn some JS and graph theory and I figured why not try something like that on my own.
The nodes are iterated through and placed onto the paper, and a callback function is given for hover over and hover out. The animation part of the hover over hover out works however the popup text shows the last value that was apart of the iteration when it was assigned.
I have found that this is because I need to use closures, however I have attempted to write a closure following several different examples that I have found.
I have posted an example on JSFiddle: link
var jgraph = {
obj_nodes : [],
obj_edges : [],
addNodes : function(obj) {
this.obj_nodes[obj.id] = obj;
},
getNodes : function() {
return this.obj_nodes;
},
addEdges : function(obj) {
this.obj_edges[obj.id] = obj;
},
getEdges : function() {
return this.obj_edges;
},
createGraph : function(paper, obj) {
var point_x, point_y,
source_x, source_y, target_x, target_y, popup;
for (var i = 0; i < obj.nodes.length; i++) {
this.obj_nodes[obj.nodes[i].id] = obj.nodes[i],
point_x = this.obj_nodes[obj.nodes[i].id].x,
point_y = this.obj_nodes[obj.nodes[i].id].y;
popup = jgraph.createPopup(paper, obj.nodes[i].id);
paper.circle(point_x, point_y, 10).attr({
fill : "#e74c3c",
stroke : "none"
}).hover(function() {
this.animate({
fill : "#3498db",
transform : "s1.5"
}, 900, "elastic");
popup.show().toFront();
}, function() {
this.animate({
fill : "#e74c3c",
transform : "s1"
}, 900, "elastic");
popup.hide();
});
}
for (var i = 0; i < obj.edges.length; i++) {
this.obj_edges[obj.edges[i].id] = obj.edges[i];
source_x = this.obj_nodes[obj.edges[i].source].x,
source_y = this.obj_nodes[obj.edges[i].source].y,
target_x = this.obj_nodes[obj.edges[i].target].x,
target_y = this.obj_nodes[obj.edges[i].target].y;
p.path("M " + source_x + " " + source_y + " L " + target_x + " " + target_y).toBack().attr({
stroke : "#e74c3c",
"stroke-width" : 4
});
}
},
createPopup : function(paper, id) {
return paper.text(this.obj_nodes[id].x, this.obj_nodes[id].y - 20, this.obj_nodes[id].label).hide().attr({
"font-size" : "13px"
});
}
};
Thanks in advance!

Closures can certainly be maddening. In this case, the problem is that you're installing your hover handlers in the context of the for loop, and the closure ties the handler's reference to popup to the variable defined outside of the context of the for loop. Naturally, by the time the hover handlers fire, the for loop has finished executing and the value of popup reflects the last label the loop handled.
Try this instead -- you're basically just declaring an anonymous function inside your for loop and passing each value of popup into that function, which makes the closure bind to the values of specific calls to that anonymous function:
popup = jgraph.createPopup(paper, obj.nodes[i].id);
( function( popup )
{
paper.circle(point_x, point_y, 10).attr({
fill : "#e74c3c",
stroke : "none"
}).hover(function() {
this.animate({
fill : "#3498db",
transform : "s1.5"
}, 900, "elastic");
popup.show().toFront();
}, function() {
this.animate({
fill : "#e74c3c",
transform : "s1"
}, 900, "elastic");
popup.hide();
});
} )( popup );
If I were you, I would probably assign the label to each element using Raphael elements' data method, and then create and destroy the text elements each time a hover is started and ended respectively. That way you don't need to worry about managing many invisible elements at all =)
Here's a fork of your fiddle in working condition.

Related

Trigger mouseover event on a Raphael map

I am using the Raphael plugin for the first time and so far I managed to create a map of Germany and to outline all different regions inside. I found out how to bind mouse-over effects, so when I hover over a region its color changes. Everything looks fine until the moment when I want to trigger the same mouse-over effect from outside the map. There is a list of links to all the regions and each link should color its respective geographical position (path) on the map when hovered. The problem is that I don't know how to trigger the mouse-over effect from outside.
This is the reference guide I used for my code: Clickable France regions map
This is my map initialization:
var regions = [];
var style = {
fill: "#f2f2f2",
stroke: "#aaaaaa",
"stroke-width": 1,
"stroke-linejoin": "round",
cursor: "pointer",
class: "svgclass"
};
var animationSpeed = 500;
var hoverStyle = {
fill: "#dddddd"
}
var map = Raphael("svggroup", "100%", "auto");
map.setViewBox(0, 0, 585.5141, 792.66785, true);
// declaration of all regions (states)
....
var bayern = map.path("M266.49486,..,483.2201999999994Z");
bayern.attr(style).data({ 'href': '/bayern', 'id': 'bayern', 'name': 'Bayern' }).node.setAttribute('data-id', 'bayern');
regions.push(bayern);
This is where my "normal" mouse effects take place:
for (var regionName in regions) {
(function (region) {
region[0].addEventListener("mouseover", function () {
if (region.data('href')) {
region.animate(hoverStyle, animationSpeed);
}
}, true);
region[0].addEventListener("mouseout", function () {
if (region.data('href')) {
region.animate(style, animationSpeed);
}
}, true);
region[0].addEventListener("click", function () {
var url = region.data('href');
if (url){
location.href = url;
}
}, true);
})(regions[regionName]);
}
I have a menu with links and I want to bind each of them to the respective region, so that I can apply the animations, too.
$("ul.menu__navigation li a").on("mouseenter", function (e) {
// this function displays my pop-ups
showLandName($(this).data("id"));
// $(this).data("id") returns the correct ID of the region
});
I would appreciate any ideas! Thanks in advance!
I figured out a way to do it. It surely isn't the most optimal one, especially in terms of performance, but at least it gave me the desired output. I would still value a better answer, but as of right now a new loop inside the mouse-over event does the job.
$("ul.menu__navigation li a").on("mouseenter", function (e) {
// this function displays my pop-ups
showLandName($(this).data("id"));
// animate only the one hovered on the link list
var test = '/' + $(this).data("id");
for (var regionName in regions) {
(function (region) {
if (region.data('href') === test) {
region.animate(hoverStyle, animationSpeed);
}
})(regions[regionName]);
}
});

Simon game javascript, how do I synchronize the lights of the buttons not to step on each other with animation() and delay() JQUERY?

I'm doing a Simon game and I'm taking the opacity from 1 to 0.3 and back to 1 everytime a sequence has place. It's almost working but the case that gives problem is when in the sequence I have the same color in a row. GREEN, GREEN, RED, BLUE for example displays just one green press and one red and one blue. It's a problem with the timing cause the logic works fine. Here's my code
$(document).ready(function(){
let simon={
colors:["green","red","yellow","blue"],
sequence:[],
userSequence:[],
strict:false,
buttonSounds : {},
init: function(){
let objectContext=this;
$("#start").on("click",function(){
objectContext.setSounds();
//executes for the first time
objectContext.emptyAllSequences();
//I generate the first of the sequence
objectContext.generateAndAddRandomColor();
//I display the sequence on the board
objectContext.displaySequenceOnBoard();
});
$("#title").addClass('tada animated');
$("#strict").on("click",function(){
$(this).toggleClass("active");
if($(this).hasClass("active")){
objectContext.strict=true;
}else{
objectContext.strict=false;
}
});
$(".button").on("click",function(){
const color=$(this).attr("data-color");
objectContext.lightButton(color,0);
objectContext.userSequence.push(color);
let isSequenceCorrect=objectContext.checkUserSequence();
/* console.log("sequenceCorrect "+isSequenceCorrect);
console.log("sequence "+objectContext.sequence);
console.log("user sequence "+objectContext.userSequence);
console.log("user sec length "+objectContext.userSequence.length);
console.log("sec length "+objectContext.sequence.length);*/
if(objectContext.userSequence.length===objectContext.sequence.length || !isSequenceCorrect){
if(isSequenceCorrect){
setTimeout(function(){objectContext.generateAndAddRandomColor();
objectContext.displaySequenceOnBoard();
//reset the userSequence to check the whole sequence again
objectContext.emptyUserSequence(); }, 1500);
}else{
//if strict mode is on
if(strict){
//user looses
$("#count").html("Lost");
//wipe sequence array
objectContext.emptyAllSequences();
$("#start").removeClass("active");
}else{
setTimeout(function(){
//change this to generate another sequence instead of displaying the existent
objectContext.displaySequenceOnBoard();
//reset the userSequence to check the whole sequence again
objectContext.emptyUserSequence(); }, 1500);
}
}
}
});
},
setSounds:function(){
this.buttonSounds["green"] = new Audio("https://s3.amazonaws.com/freecodecamp/simonSound1.mp3");
this.buttonSounds["red"] = new Audio("https://s3.amazonaws.com/freecodecamp/simonSound2.mp3");
this.buttonSounds["yellow"] = new Audio("https://s3.amazonaws.com/freecodecamp/simonSound3.mp3");
this.buttonSounds["blue"] = new Audio("https://s3.amazonaws.com/freecodecamp/simonSound4.mp3");
},
emptyUserSequence: function(){
this.userSequence.length=0;
},
emptyAISequence: function(){
this.sequence.length=0;
},
emptyAllSequences:function(){
this.emptyUserSequence();
this.emptyAISequence();
},
updateCount: function(){
$("#count").html(this.sequence.length);
},
checkUserSequence:function(){
for(let i=0,len=this.userSequence.length;i<len;i++){
if(this.userSequence[i]!== this.sequence[i]){
return false;
}
}
return true;
},
generateAndAddRandomColor:function(){
const randColorIndex=Math.floor(Math.random()*4);
const randColor=this.colors[randColorIndex];
this.sequence.push(randColor);
this.updateCount();
},
displaySequenceOnBoard: function(){
for(let i=0,len=this.sequence.length;i<len;i++){
// this.buttonSounds[this.sequence[i]].play();
this.lightButton(this.sequence[i],i);
}//end for
},
lightButton: function(color,i){
var objectContext=this;
$("#"+color).delay(400*i)
.animate({opacity : 0.3}, 300,function(){
objectContext.buttonSounds[color].play();
$("#"+color).animate({opacity : 1}, 150);
});
}
}
simon.init();
});
Here's the codepen. You have to press start to start the game and as the sequence grows and is displayed on the board the problem commented before arises, what could be happening?. Thanks!
http://codepen.io/juanf03/pen/jrEdWz?editors=1111
You're setting multiple delays on the same element at once, and the newest one just replaces the existing one.
Instead of using delays, pass a complete function with your animation, and give it the whole remaining sequence. To illustrate, replace your current displaySequenceOnBoard() with these two functions:
displaySequenceOnBoard: function(){
$(".button").removeClass("enabled");
this.lightButtonsInSequence(this.sequence);
},
lightButtonsInSequence: function(sequence) {
var color = sequence[0]
var remainingSequence = sequence.slice(1)
var lightNextButtonFunction = remainingSequence.length > 0 ? jQuery.proxy(function(){
this.lightButtonsInSequence(sequence.slice(1));
}, this) : null;
console.log("Lighting color: " + color);
$("#"+color).animate({opacity : .3}, 300).animate({opacity : 1}, 150, lightNextButtonFunction)
}
Here it is plugged into your CodePen and working.

Snap SVG animate SVG on hover / reset SVG on leave

I'm using Snap.svg to create some SVGs that animate on hover.
A very simplified jsfiddle is here:
http://jsfiddle.net/62UA7/2/
var s = Snap("#svg");
var smallCircle = s.circle(100, 150, 70);
//animation
function animateSVG(){
smallCircle.animate({cy: 300}, 5000,mina.bounce);
smallCircle.animate({fill:"red"},200);
}
//reset function?
function resetSVG(){
// something here to reset SVG??
}
smallCircle.mouseover(animateSVG,resetSVG);
The hover / animation is working fine.
The intention is to stop the animation and return to original SVG state if the user moves the mouse off the SVG - and this is where I'm currently stuck.
The actual SVG files I'm using are complex, so hoping for a quick way of 'refreshing' the SVG rather than manually moving it back to original position and colour
I'm assuming there's a really easy way of doing this - just can't seem to work it out or find the answer in any documentation anywhere.
Hopefully someone can help - thanks in advance if you can!
If you are only willing to animate between 2 states I found that Codrops animated svg icons did great job with handling this type of snap.svg animations. I have started using their code as a basis for my future exploration of SNAP.SVG. But getting back to the code: the most fun part is that you configure your animation with simple JSON objects such as:
plus : {
url : 'plus',
animation : [
{
el : 'path:nth-child(1)',
animProperties : {
from : { val : '{"transform" : "r0 32 32", "opacity" : 1}' },
to : { val : '{"transform" : "r180 32 32", "opacity" : 0}' }
}
},
{
el : 'path:nth-child(2)',
animProperties : {
from : { val : '{"transform" : "r0 32 32"}' },
to : { val : '{"transform" : "r180 32 32"}' }
}
}
]
},
and you can easily attach any sort of event trigger for animation In/Out. Hope that helps.
Personally I'd probably do it something like the following, storing it in a data element. It depends what problems you are really trying to overcome though, how you are actually animating it (I suspect it could be easier than my solution with certain animations, but trying to think of something that covers most bases), and if you really need it reset, also how many attributes you are animating and if there is other stuff going on...
var smallCircle = s.circle(100, 150, 70);
var saveAttributes = ['fill', 'cy'];
Snap.plugin( function( Snap, Element, Paper, global ) {
Element.prototype.resetSVG = function() {
this.stop();
for( var a=0; a<saveAttributes.length; a++) {
if( this.data( saveAttributes[a] ) ) {
this.attr( saveAttributes[a], this.data( saveAttributes[a] ) );
};
};
};
Element.prototype.storeAttributes = function() {
for( var a=0; a<saveAttributes.length; a++) {
if( this.attr( saveAttributes[a]) ) {
this.data( saveAttributes[a], this.attr( saveAttributes[a] ) );
};
};
};
});
function animateSVG(){
smallCircle.animate({cy: 300}, 5000,mina.bounce);
smallCircle.animate({fill:"red"},200);
};
smallCircle.storeAttributes();
smallCircle.mouseover( animateSVG );
smallCircle.mouseout( smallCircle.resetSVG );
jsfiddle

Make this retrocomputing scanline rock. Scan multiple lines in div is hard

Working fiddle be here:
http://jsfiddle.net/WyXLB/1/
When a HTML element contains multiple lines of text, I want to scan over each one. At the moment, I am just scanning over the bounding rectangle of the entire element.
Some code is :
function run_scan(el,mask,callback) {
// we need to go over each line of text in here
// or somewhere
var el_font_size = $(el).css('font');
$('body').append(sizer);
sizer.css({
'position': 'absolute',
'font' : el_font_size,
'white-space' : 'pre' } );
var real_box = window.getComputedStyle(sizer[0]);
var real_width = parseFloat(real_box.width);
var real_height = parseFloat(real_box.height);
var lines = Math.round($(el).height()/real_height);
var mask_offset = $(mask).offset();
var origin_left = mask_offset.left;
var duration = parseFloat($(mask).width())/2.0;
$(mask).animate( {
'left':origin_left+$(mask).width()
},
{
duration:duration,
complete:callback
} );
}
How can I break the animation over the entire bounding rectangle, to scan over each line. I tried putting an animation->callback recursion in the above function, descending the mask by one real_height each call, but got unpredictable/chaotic behavior.
If you need only loop then it should look like this (inside run_scan)
var i = 0;
function loop() {
if (++i < lines) {
$(mask).animate({
'left':origin_left+$(mask).width()
},{
duration:duration,
complete:loop
});
} else {
// last line
callback();
}
}

Asset.images slow? How do I execute functions so they don't freeze the browser?

I recently downloaded a nice mootools plugin to provide a rating system for search results on my site: MooStarRating.
It works quite well, but it is very slow to initialize. Here are the execution times I have logged (for pulling back 50 search results).
======== starrating ========
init: 0.06ms 50
img: 5.40ms 50
str: 0.54ms 50
each: 3.04ms 50
inject: 0.86ms 50
end: 1.52ms 50
subtotal: 11.42ms 50
-----------------
total: 571.00ms
Here is the initialize function these logs refer to (just for reference):
initialize: function (options) {
lstart("starrating");
// Setup options
this.setOptions(options);
// Fix image folder
if ((this.options.imageFolder.length != 0) && (this.options.imageFolder.substr(-1) != "/"))
this.options.imageFolder += "/";
// Hover image as full if none specified
if (this.options.imageHover == null) this.options.imageHover = this.options.imageFull;
lrec("init");
// Preload images
try {
Asset.images([
this.options.imageEmpty,
this.options.imageFull,
this.options.imageHover
]);
} catch (e) { };
lrec("img");
// Build radio selector
var formQuery = this.options.form;
this.options.form = $(formQuery);
if (!this.options.form) this.options.form = $$('form[name=' + formQuery + "]")[0];
if (this.options.form) {
var uniqueId = 'star_' + String.uniqueID();
this.options.form.addClass(uniqueId);
this.options.selector += 'form.' + uniqueId + ' ';
}
this.options.selector += 'input[type=radio][name=' + this.options.radios + "]";
// Loop elements
var i = 0;
var me = this;
var lastElement = null;
var count = $$(this.options.selector).length;
var width = this.options.width.toInt();
var widthOdd = width;
var height = this.options.height.toInt();
if (this.options.half) {
width = (width / 2).toInt();
widthOdd = widthOdd - width;
}
lrec("str");
$$(this.options.selector).each(function (item) {
// Add item to radio list
this.radios[i] = item;
if (item.get('checked')) this.currentIndex = i;
// If disabled, whole star rating control is disabled
if (item.get('disabled')) this.options.disabled = true;
// Hide and replace
item.setStyle('display', 'none');
this.stars[i] = new Element('a').addClass(this.options.linksClass);
this.stars[i].store('ratingIndex', i);
this.stars[i].setStyles({
'background-image': 'url("' + this.options.imageEmpty + '")',
'background-repeat': 'no-repeat',
'display': 'inline-block',
'width': ((this.options.half && (i % 2)) ? widthOdd : width),
'height': height
});
if (this.options.half)
this.stars[i].setStyle('background-position', ((i % 2) ? '-' + width + 'px 0' : '0 0'));
this.stars[i].addEvents({
'mouseenter': function () { me.starEnter(this.retrieve('ratingIndex')); },
'mouseleave': function () { me.starLeave(); }
});
// Tip
if (this.options.tip) {
var title = this.options.tip;
title = title.replace('[VALUE]', item.get('value'));
title = title.replace('[COUNT]', count);
if (this.options.tipTarget) this.stars[i].store('ratingTip', title);
else this.stars[i].setProperty('title', title);
}
// Click event
var that = this;
this.stars[i].addEvent('click', function () {
if (!that.options.disabled) {
me.setCurrentIndex(this.retrieve('ratingIndex'));
me.fireEvent('click', me.getValue());
}
});
// Go on
lastElement = item;
i++;
}, this);
lrec("each");
// Inject items
$$(this.stars).each(function (star, index) {
star.inject(lastElement, 'after');
lastElement = star;
}, this);
lrec("inject");
// Enable / disable
if (this.options.disabled) this.disable(); else this.enable();
// Fill stars
this.fillStars();
lrec("end");
return this;
},
So, the slowest part of the function is this:
// Preload images
try {
Asset.images([
this.options.imageEmpty,
this.options.imageFull,
this.options.imageHover
]);
} catch (e) { };
Which is strange. What does Asset.images do? Does the script block until these images have been loaded by the browser? Is there a way to preload images that runs faster?
How can I make the scripts on my page execute faster? It is a big problem for them to take 800ms to execute, but 200ms is still quite bad. At the moment, my search results all pop into existence at once. Is it possible to make it so that individual search results are created separately, so that they don't block the browser while being created? Similarly, is it possible to do this for the individual components of the search results, such as the MooStarRating plugin?
no. Asset.images is non-blocking as each of these gets loaded separately and a singular event is being dispatched when all done.
the speed for loading will be dependent on the browser but it be will multi-threaded to whatever capability it has for parallel downloading from the same host.
https://github.com/mootools/mootools-more/blob/master/Source/Utilities/Assets.js#L115-129
immediately, it returns an Element collection with the PROMISE of elements that may still be downloading. that's fine - you can use it to inject els, attach events, classes etc - you just cannot read image properties yet like width, height.
each individual image has it's own onload that fires an onProgress and when all done, an onComplete for the lot - i would advise you to enable that, remove the try/catch block and see which image is creating a delay. you certainly don't need to wait for anything from Asset.images to come back.
you also seem to be using it as a 'prime the cache' method, more than anything - as you are NOT really saving a reference into the class instance. your 'each' iteration can probably be optimised so it uses half the time if objects and functions are cached and so are references. probably more if you can use event delegation.
To answer your questions about not freezing the browser due to the single-threaded nature of javascript, you defer the code via setTimeout (or Function.delay in mootools) with a timer set to 0 or a 10ms due to browser interpretations. You also write the function to to exec a callback when done, in which you can pass the function result, if any (think ajax!).

Categories

Resources