# Optimize coloring book built with Raphaël and jQuery

I've been building a coloring book for kids: http://coloringbook.core.ba.lightburncloud.com/

It's got to work on an iPad, so I decided to try out Raphaël JS. I want to try and make this as efficient as possible, and was wondering if anybody had any suggestions on how to tighten things up.

I'm just kind of flying by the seat of my pants here, so if anybody has any best practices/general tips for Raphaël (or jQuery, or JavaScript), it'd help me out a lot.

// Primes the page, gets the scene via JSON
// -------------------------------------------------------------------
var engine = {
paper           : Object,
prime           : function(){
// Makes new instance of scalable raphael
engine.paper = new ScaleRaphael("stage", 960, 525);
engine.handleResize();
$(window).resize(engine.handleResize); }, load : function(json){$.getJSON(json, function(data){

// Collects Scene Elements
scene.set(data.elements);

// Collects Colors
color.set(data.colors);

// Collects Audio Elements
audio.set(data.audio);

// Collects Save State Elements
save.set(data.state);

// Sets up ui
engine.uiAudio();
engine.uiControls();

});
},
handleResize    : function(){

// Resizes stage
var win = $(window); engine.paper.changeSize(win.width(), win.height(), true, false); }, handleClick : function(elem){ if(elem.attr('fill') != color.active){ // Saves the previous state to the history stack history.save(elem.id, elem.attr('fill')); // Fill with active color elem.attr('fill', color.active); //Save ID & Color, eg: //$.post("[url of save function]", { id : elem.id, color : color.active });
//console.log(elem.id + ' ' + color.active)
}

// Gets name, checks to see if there's audio associated with it
audio.check(elem.data("name"));

},
uiAudio         : function(){

$('body').append('<div id="uiPop"></div>');$('#uiPop').jPlayer({
$(this).jPlayer('setMedia', { wav: "cogs/audio/ui/pop.wav", mp3: "cogs/audio/ui/pop.mp3" }); }, swfPath: "cogs/scripts", supplied: "wav, mp3", wmode: "window" }); }, uiControls : function(){ var buttonStyle = { 'stroke-width' : '5px', 'stroke' : '#d1d2d3', 'fill' : '#0082c8' }, shadowStyle = { 'stroke-width' : '0', 'stroke' : 'transparent', 'fill' : 'r#666666-#73afdf' }, iconStyle = { 'stroke-width' : '0', 'stroke' : 'transparent', 'fill' : '#fff' } // Home Button ----------------------------------------------- var homeButton = engine.paper.set(); // shadow engine.paper.ellipse(895, 96, 40, 15).attr(shadowStyle); // button homeButton.push(engine.paper.ellipse(895, 60, 35, 35).attr(buttonStyle).attr('href','http://www.google.com')); homeButton.push(engine.paper.path("M895.025,41.46L875,60.962h4.536v15.383c0,1.213,1.311,2.195,2.927,2.195h25.152 c1.617,0,2.928-0.982,2.928-2.195V60.962H915L895.025,41.46z").attr(iconStyle).attr('href','http://www.google.com')); // events homeButton.hover(function(){ homeButton[0].attr('stroke','#fff'); },function(){ homeButton[0].attr('stroke','#d1d2d3'); }); // ----------------------------------------------------------- // Sound Button ---------------------------------------------- var soundButton = engine.paper.set(); // shadow engine.paper.ellipse(895, 296, 40, 15).attr(shadowStyle); // button soundButton.push(engine.paper.ellipse(895, 260, 35, 35).attr(buttonStyle)); soundButton.push(engine.paper.path("M920.156,260c0-13.871-11.284-25.156-25.156-25.156c-7.395,0-14.057,3.21-18.664,8.31 c-0.012,0.012-0.028,0.021-0.041,0.034c-0.07,0.07-0.134,0.147-0.186,0.228c-3.895,4.431-6.266,10.235-6.266,16.584 c0,13.871,11.286,25.156,25.156,25.156c6.294,0,12.053-2.328,16.47-6.164c0.132-0.068,0.26-0.149,0.368-0.261 c0.074-0.072,0.137-0.152,0.189-0.233C917.022,273.897,920.156,267.309,920.156,260z M917.283,260 c0,6.107-2.472,11.648-6.467,15.678l-31.494-31.494c4.03-3.994,9.57-6.466,15.678-6.466 C907.287,237.717,917.283,247.713,917.283,260z M872.718,260c0-5.146,1.759-9.884,4.697-13.66l31.246,31.245 c-3.775,2.938-8.517,4.698-13.661,4.698C882.714,282.283,872.718,272.286,872.718,260z").attr(iconStyle).hide()); soundButton.push(engine.paper.path("M904.188,255.004c-1.183-2.239-3.318-3.446-5.036-5.218c-1.323-1.365-2.391-2.973-2.918-4.813 c-0.073-0.261-0.371-1.102-0.391-1.632v-0.102c0.005-0.103,0.022-0.19,0.06-0.25c0,0-0.023,0.037-0.06,0.095v-0.095 c0-0.341-0.276-0.618-0.617-0.618s-0.618,0.276-0.618,0.618v27.036c-1.332-0.96-3.544-1.144-5.729-0.32 c-2.979,1.125-4.77,3.693-3.998,5.736c0.77,2.044,3.811,2.789,6.791,1.665c2.449-0.924,4.09-2.822,4.171-4.604 c0-0.001,0-0.002,0-0.002v-22.568l0.059,0.073c0.676,1.572,2.831,2.661,2.831,2.661c2.147,1.083,4.159,3.058,4.864,5.38 c0.625,2.059,0.271,4.383-0.497,6.356c-0.353,0.905-0.795,2.041-1.464,2.776c1.349-1.483,2.639-3.338,3.256-5.26 C905.643,259.583,905.321,257.152,904.188,255.004z").attr(iconStyle));; // events soundButton.hover(function(){ soundButton[0].attr('stroke','#fff'); },function(){ soundButton[0].attr('stroke','#d1d2d3'); }).click(function(){ if(audio.isOn){ audio.isOn = false; soundButton[1].show(); }else{ audio.isOn = true; soundButton[1].hide(); } }); // ----------------------------------------------------------- // Clear Button ---------------------------------------------- var clearButton = engine.paper.set(); // shadow engine.paper.ellipse(895, 396, 40, 15).attr(shadowStyle); //button clearButton.push(engine.paper.ellipse(895, 360, 35, 35).attr(buttonStyle)) clearButton.push(engine.paper.path("M899.326,343.448c0-2.286-1.989-2.286-1.989-2.286h-3.38c-2.585,0-2.286,2.286-2.286,2.286 H899.326z M906.085,344.84c0,0-18.49,0-22.565,0c-4.076,0-3.878,3.877-3.878,3.877h30.718 C910.359,344.542,906.085,344.84,906.085,344.84z M883.966,374.961c0.497,3.777,4.474,3.877,4.474,3.877h12.525 c4.672,0,5.069-4.075,5.069-4.075l2.784-24.355h-27.637C881.182,350.407,883.47,371.184,883.966,374.961z M901.411,353.741 c0.035-0.591,0.539-1.044,1.132-1.007c0.59,0.034,1.041,0.541,1.007,1.131l-1.158,19.947c-0.033,0.569-0.505,1.01-1.068,1.01 c-0.021,0-0.042-0.002-0.063-0.002c-0.59-0.034-1.041-0.541-1.007-1.132L901.411,353.741z M893.93,353.787 c0-0.592,0.479-1.071,1.071-1.071c0.591,0,1.07,0.479,1.07,1.071v19.98c0,0.591-0.479,1.071-1.07,1.071 c-0.593,0-1.071-0.48-1.071-1.071V353.787z M887.533,352.734c0.586-0.036,1.097,0.416,1.13,1.007l1.159,19.947 c0.034,0.591-0.416,1.098-1.007,1.132c-0.021,0-0.042,0.002-0.063,0.002c-0.563,0-1.035-0.44-1.068-1.01l-1.159-19.947 C886.491,353.275,886.942,352.769,887.533,352.734z").attr(iconStyle)); // events clearButton.hover(function(){ clearButton[0].attr('stroke','#fff'); },function(){ clearButton[0].attr('stroke','#d1d2d3'); }).click(function(){ scene.colorable.attr('fill','#fff'); history.clear(); }); // ----------------------------------------------------------- // Undo Button ----------------------------------------------- var undoButton = engine.paper.set(); // shadow engine.paper.ellipse(895, 496, 40, 15).attr(shadowStyle); // button undoButton.push(engine.paper.ellipse(895, 460, 35, 35).attr(buttonStyle)); undoButton.push(engine.paper.path("M917.962,441.608l-15.218-1.075l-1.074,15.219l6.732-5.843c2.508,3.128,3.88,6.97,3.88,11.048 c0,9.78-7.956,17.736-17.736,17.736c-9.779,0-17.736-7.956-17.736-17.736c0-4.736,1.846-9.189,5.195-12.541l-3.374-3.374 c-4.252,4.251-6.593,9.905-6.593,15.915c0,12.412,10.097,22.51,22.508,22.51c12.41,0,22.509-10.098,22.509-22.51 c0-5.244-1.776-10.189-5.038-14.186L917.962,441.608z").attr(iconStyle)); // events undoButton.hover(function(){ undoButton[0].attr('stroke','#fff'); },function(){ undoButton[0].attr('stroke','#d1d2d3'); }).click(function(){ history.recall(); }); // ----------------------------------------------------------- } } // ------------------------------------------------------------------- // Handles colors // ------------------------------------------------------------------- var color = { active : Object, list : Object, set : function(array){ color.list = engine.paper.set(); var shadowStyle = { 'stroke-width' : '0', 'stroke' : 'transparent', 'fill' : 'r#666666-#73afdf' } // Displays Colors for(var i = 0; i < array.length; i++){ // Shadows var x = 96+(100*i); engine.paper.ellipse(65, x, 40, 15).attr(shadowStyle) var y = 60+(100*i); color.list.push( engine.paper.ellipse(65, y, 35, 35).attr({ 'stroke-width' : '5px', 'stroke' : '#d1d2d3', 'fill' : '#'+array[i].hex }).hover(function(){ this.attr('stroke','#fff') }, function(){ if(!this.data('active')){ this.attr('stroke','#d1d2d3'); } }).click(function(){ color.handleClick(this); }) ); } // Sets Active Color color.active = '#'+array[0].hex color.list[0].attr('stroke','#fff').data('active',true); }, handleClick : function(elem){ color.active = elem.attr('fill'); color.list.attr('stroke','#d1d2d3').removeData('active'); elem.attr('stroke','#fff').data('active',true); } } // ------------------------------------------------------------------- // Handles scene // ------------------------------------------------------------------- var scene = { outlines : Object, colorable : Object, set : function(array){ // Initializes the sets scene.outlines = engine.paper.set(); scene.colorable = engine.paper.set(); // Draws Objects, puts outlines in one set, colorable items in the other for(var i = 0; i < array.length; i++){ if(array[i].colorable == 'true'){ scene.colorable.push( eval('engine.paper.' + array[i].type +'('+array[i].coords+').data("name","'+array[i].name+'")') ); }else{ scene.outlines.push( eval('engine.paper.' + array[i].type +'('+array[i].coords+')') ); } } // Colors the outlines scene.outlines.attr({ 'fill' : '#000', 'stroke' : 'transparent', 'stroke-width' : 0 }); // Colors the colorable items scene.colorable.attr({ 'fill' : '#fff', 'stroke' : 'transparent', 'stroke-width' : 0 }); // Listens for clicks on the colorable items scene.colorable.forEach(function(e){ e.click(function(){ engine.handleClick(this); }); }); // Shifts elements over engine.paper.forEach(function(elem){ elem.translate(130,0); }); } } // ------------------------------------------------------------------- // Sets up audio // ------------------------------------------------------------------- var audio = { collection : [], isOn : true, set : function(array){ for(var i = 0; i < array.length; i++){ var a = array[i].wav, b = array[i].mp3, id = 'audio_'+i, rel = array[i].rel;$('body').append('<div id='+id+'></div>');
$('#'+id).jPlayer({ ready : function(){$(this).jPlayer('setMedia', {
wav: a,
mp3: b
});
},
play    : function(){
$(this).jPlayer("pauseOthers"); }, swfPath: "cogs/scripts", supplied: "wav, mp3", wmode: "window" }); audio.collection.push({ "rel" : rel, "id" : id }); } }, check : function(name){ var audID, play = false; for(var i = 0; i < audio.collection.length; i++){ if(audio.collection[i].rel == name){ audID = audio.collection[i].id; play = true; } } if(play && audio.isOn){$('#'+audID).jPlayer('play');
}else if(audio.isOn){
$('#uiPop').jPlayer('play'); } } } // ------------------------------------------------------------------- // Handles Saved data // ------------------------------------------------------------------- var save = { set : function(array){ for(var i = 0; i < array.length; i++){ // Gets elements by saved ID, sets color to saved color engine.paper.getById(array[i].id).attr('fill',array[i].color) } } } // ------------------------------------------------------------------- // History // ------------------------------------------------------------------- var history = { stack : [], save : function(id, color){ // Pushes state onto the stack history.stack.push({ "id" : id, "color" : color }); }, recall : function(){ // Recalls previous state if(history.stack.length){ var prev = history.stack.pop() engine.paper.getById(prev.id).attr({ fill : prev.color }) } }, clear : function(){ // Clears history history.stack.length = 0; } } // ------------------------------------------------------------------- // Start // -------------------------------------------------------------------$(function(){
engine.prime();
})
// -------------------------------------------------------------------

• please put the code inline in the question (as per the faq) Apr 17 '12 at 21:07

First of all, the best way to optimize any code is to run it through a profiler to find the bottlenecks.

Assuming you've already done that, here are a couple very minor things:

1. This code:

for(var i = 0; i < array.length; i++){


results in the length property being queried on each iteration. Obviously this won't amount to much difference if the number of iterations is small, but as a general rule I try to always cache the value:

for(var i = 0, len = array.length; i < len; i++){


And you should be caching property accesses on any objects if you use them in multiple places. For example, instead of using array[i] in several places within a loop, store it in a variable and use the variable instead.

2. I'm not sure how expensive multiplication is compared to addition in JS (and again, if array.length is a small value this won't make a difference), but instead of calculating this value every iteration:

var x = 96+(100*i);


You could just increment it within the loop:

var len = array.length;
for(var i = 0, j = 0; i < len; i++, j += 100){
var x = 96 + j;
var y = 60 + j;

• Thanks a lot for the suggestions! I ran this test for multiplication vs. addition: jsperf.com/addition-vs-multiplication, look like multiplication is faster, at least in Chrome. Apr 18 '12 at 14:46
• Nice! Very interesting... Apr 18 '12 at 15:05

I agree with seand above, I would also add:

\$(window).resize(engine.handleResize);


This might results in more resize events then you would expect, as the user resizes the window many events will be triggered causing needless updates. Coincidently I just released a helper to solve this exact problem, you can see it in action here: http://limit.gotsomething.com

Another (and this is really a preference) is rather than have various colors sprinkled through your code it might be best to declare them all in one spot. Making updating the UI much easier (especially for non developers)

Lastly, your stack for history is a nice feature, however you might consider future proofing it a bit by not only storing the colour but the attribute/method itself. This way should you introduce the ability to change stroke, opacity etc. it will be supported. Just a thought.

Regardless, neat app and the code overall is very good!

Marc.

• Thanks a lot for your suggestions. Your limit helper is pretty slick, I'm sharing it with my coworkers! Apr 18 '12 at 14:51