2
\$\begingroup\$

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({
            ready   : function(){
                $(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();
    engine.load('cogs/scenes/choking2.json');
})
// -------------------------------------------------------------------
\$\endgroup\$
  • \$\begingroup\$ please put the code inline in the question (as per the faq) \$\endgroup\$ – seand Apr 17 '12 at 21:07
1
\$\begingroup\$

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;
    
\$\endgroup\$
  • 1
    \$\begingroup\$ 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. \$\endgroup\$ – MLatzke Apr 18 '12 at 14:46
  • \$\begingroup\$ Nice! Very interesting... \$\endgroup\$ – seand Apr 18 '12 at 15:05
1
\$\begingroup\$

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.

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.