jQuery Conference Slides and Demo

By Ryan Florence, published 2010-04-24

Part of the issue Migrated Articles From Original Site Structure..

Just spoke at jQuery conference, the largest javascript conference to date (as stated by Resig.) It really was a great turnout and I’ve met tons of cool people.

I only had a half an hour, which flew by and is hardly enough time to talk about MooTools Class AND the jQuery mutator. Had several people tell me I convinced them to give MooTools a shot with their apps since they can keep jQuery for the DOM. That’s always good news.

Update: moo4q.com

There’s now a site dedicated to this technique: http://moo4q.com, check it out.

Slides and Demo

And now for your viewing pleasure:

Source of the demo

Mutator

Gist

Class.Mutators.jQuery = function(name){
    var self = this;
    jQuery.fn[name] = function(arg){
        var instance = this.data(name);
        if ($type(arg) == 'string'){
            var prop = instance[arg];
            if ($type(prop) == 'function'){
                var returns = prop.apply(instance, Array.slice(arguments, 1));
                return (returns == instance) ? this : returns;
            } else if (arguments.length == 1){
                return prop;
            }
            instance[arg] = arguments[1];
        } else {
            if (instance) return instance;
            this.data(name, new self(this.selector, arg));
        }
        return this;
    };
};

Human

var Human = new Class({

    Implements: [Options, Events, Loop],

        options: {
            rest: {
                frames: 14,
                fileName: 'images/rest/rest_',
                speed: 100
            }
        },

    jQuery: 'human',

    energy: 5,
    isAlive: true,
    animatingOnce: false,

    initialize: function(selector, options){
        this.setOptions(options);
        this.image = $(selector);
        this.name = this.image.attr('name') || 'unkown';
        this.currentAnimation = this.options.rest;
        this.loadImages(this.options.rest);
        this.setLoop(this.animate, this.currentAnimation.speed).startLoop();
    },

    eat: function(){
        if(this.isAlive){
            this.energy++;
            this.fireEvent('eat');
        }
    },

    die: function(killer){
        if(this.isAlive){
            this.energy = 0;
            this.isAlive = false;
            killer.kill(this);
            this.fireEvent('die', killer);
        }
        return this;
    },

    revive: function(){
        if(!this.isAlive){
            this.energy = 1;
            this.isAlive = true;
            this.fireEvent('revive');
        }
        return this;
    },
    
    animate: function(){
        if(this.loopCount > this.currentAnimation.frames) this.loopCount = 1;
        this.image.attr('src', this.currentAnimation.fileName + this.loopCount.zeroPad(4) + '.png');
        return this;
    },

    changeAnimation: function(animation){
        if(!this.animatingOnce){
            this.currentAnimation = animation;
            this.resetLoop().setLoop(this.animate, animation.speed);    
        }
        return this;
    },

    animateOnce: function(animation, stop){
        if(!this.animatingOnce){
            var original = this.currentAnimation;
            this.changeAnimation(animation);
            this.animatingOnce = true;
            var delay = this.currentAnimation.frames * this.currentAnimation.speed;
            var self = this;
            (function(){
                self.animatingOnce = false;
                (stop) ? self.stopLoop() : self.changeAnimation(original);
            }).delay(delay);
        }
        return this;
    },

    loadImages: function(animation){
        (animation.frames - 1).times(function(num){
            $('<img/>', { src: animation.fileName + (num + 1).zeroPad(4) + '.png'});
        }, this);
        return this;
    }

});

Number.implement({
    zeroPad: function(length){
        var str = '' + this;
        while (str.length < length) str = '0' + str;
        return str;
    }
});

Warrior

var Warrior = new Class({

    energy: 1000,
    kills: 0,
    attack: function(target){
        if(!this.animatingOnce) {
            this.fireEvent('attack', target);
            this.animateOnce(this.options.attack);
            if(target.isAlive){
                var delay = this.options.attack.frames / 2.3 * this.options.attack.speed;
                target.getAttacked.delay(delay, target, this);
                this.fireEvent.delay(delay, this, ['attackComplete', target]);
            }
        }
        return this;
    },

    kill: function(victim){
        this.kills++;
        this.fireEvent('kill', victim);
        return this;
    }

});

Ninja

var Ninja = new Class({

    Extends: Human,
    Implements: Warrior,

        options: {
            side: 'evil',
            attack: {
                frames: 44,
                fileName: 'images/attack/attack_',
                speed: 40
            },
            rest:{
                frames: 14,
                fileName: 'images/ninja-rest/rest_',
                speed: 75 
            }
        },

    jQuery: 'ninja',

    initialize: function(selector, options){
        this.parent(selector, options);
        this.loadImages(this.options.attack);
        this.side = this.options.side;
    }

});

Victim

var Victim = new Class({

    getAttacked: function(attacker){
        this.stopLoop();
        this.image.attr('src', this.options.attackedSrc);
        this.startLoop.delay(100, this);
        this.energy--;
        this.fireEvent('getAttacked', attacker);
        if(this.energy == 0) this.die(attacker);
    },

    die: function(killer){
        if(this.isAlive){
            this.parent(killer);
            this.animateOnce(this.options.die, true);
        }
        return this;
    },

    revive: function(){
        if(!this.isAlive){
            this.parent();
            this.changeAnimation(this.options.rest).startLoop();
        }
        return this;
    }

});

Civilian

var Civilian = new Class({

    Extends: Human,
    Implements: Victim,

        options: {
            die: {
                frames: 29,
                fileName: 'images/die/die_',
                speed: 75
            },
            attackedSrc: 'images/die/die_0000.png'
        },

    jQuery: 'civilian',

    initialize: function(selector, options){
        this.parent(selector, options);
        this.loadImages(this.options.die);
    }

});

Domready

$(document).ready(function(){

    // create our humans
    $('#bob').civilian(); // creates instance
    $('#ryu').ninja({side: 'evil'}); // pass in some options

    // keyboard events
    $(document).bind('keydown',function(event){

        if(event.keyCode == 65)
            $('#ryu').ninja('attack', $('#bob').civilian()); 
            // $('#bob').civilian() a second time returns the instance
            // just like `var bob = new Civilian('#bob')`, it returns a object like `bob`
            // so `$('#bob').civilian().die() ~= $('#bob').civilian('die')`
            // except the first returns the object and the second returns the jquery object

        if(event.keyCode == 82) $('#bob').civilian('revive');

        if(event.keyCode == 69) $('#bob').civilian('eat');

    });

    // log function
    var setLog = function(message, className){
        className = className || 'message';
        var top = $('#log')[0].scrollHeight;
        $('#log').append('<p class=' + className + '>' + message + '</p>').stop().animate({scrollTop: top}, 1000);
    }

    // instance events
    // when things happen we can do stuff, like the callbacks you're used to
    // Here we get the instance of civilian by calling the civilian on bob again
    // and add the events to it, but you can also ...
    $('#bob').civilian().addEvents({
        onDie: function(killer){
            // events can have arguments, killer is the instance of $('#ryu').ninja()
            // and `this` is the instance of $('#bob').civilian() in here.
            setLog(this.name + ' dies by the hand of ' + killer.name, 'die'); 
        },
        onRevive: function(){ 
            setLog(this.name + ' is Revived', 'revive'); 
        },
        onGetAttacked: function(attacker){ 
            setLog(this.name + ' is hit! He has ' + this.energy + ' energy left after ' + attacker.name + "'s attack!", 'getAttacked'); 
        },
        onEat: function(){
            setLog(this.name + ' ate some food. His energy is now ' + this.energy, 'eat');
        }
    });

    // ... just use 'addEvents' as an argument to the class to add the events
    $('#ryu').ninja('addEvents',{
        onAttack: function(target){ 
            setLog(this.name + ' attacks ' + target.name + '!', 'attack'); 
        },
        onKill: function(target){
            var plural = (this.kills == 1) ? 'kill' : 'kills';
            setLog(this.name + ' killed ' + target.name + '!! Ryu now has ' + this.kills + ' ' + plural, 'kill');
            setLog.delay(3000, this, ["You find an <span>Emporer's Hairpin</span> on " + target.name, 'find']);
            setLog.delay(3000, this, ["You find a <span>handful of paint brushes</span> on " + target.name, 'find']);
            setLog.delay(3000, this, ["You find <span>4 " + target.name + " skins</span> on " + target.name, 'find']);
        }
    });

});

Hi, I'm Ryan!

Location:
South Jordan, UT
Github:
rpflorence
Twitter:
ryanflorence
Freenode:
rpflo

About Me

I'm a front-end web developer from Salt Lake City, Utah and have been creating websites since the early 90's. I like making awesome user experiences and leaving behind maintainable code. I'm active in the JavaScript community writing plugins, contributing to popular JavaScript libraries, speaking at conferences & meet-ups, and writing about it on the web. I work as the JavaScript guy at Instructure.